mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-06-28 19:31:00 -04:00
broadcast-areas: include simple feature
simple feature is a feature where there are no islands and all polygons are capped to 125 points Signed-off-by: Toby Lorne <toby.lornewelch-richards@digital.cabinet-office.gov.uk>
This commit is contained in:
@@ -35,11 +35,12 @@ class GetItemByIdMixin:
|
||||
class BroadcastArea(IdentifiableMixin):
|
||||
|
||||
def __init__(self, row):
|
||||
id, name, feature = row
|
||||
id, name, feature, simple_feature = row
|
||||
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.feature = feature
|
||||
self.simple_feature = simple_feature
|
||||
|
||||
for coordinates in self.polygons:
|
||||
if coordinates[0] != coordinates[-1]:
|
||||
@@ -52,30 +53,44 @@ class BroadcastArea(IdentifiableMixin):
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id
|
||||
|
||||
@property
|
||||
def polygons(self):
|
||||
if self.feature['geometry']['type'] == 'MultiPolygon':
|
||||
def _polygons(self, feature):
|
||||
if feature['geometry']['type'] == 'MultiPolygon':
|
||||
return [
|
||||
polygons[0]
|
||||
for polygons in self.feature['geometry']['coordinates']
|
||||
for polygons in feature['geometry']['coordinates']
|
||||
]
|
||||
if self.feature['geometry']['type'] == 'Polygon':
|
||||
if feature['geometry']['type'] == 'Polygon':
|
||||
return [
|
||||
self.feature['geometry']['coordinates'][0]
|
||||
feature['geometry']['coordinates'][0]
|
||||
]
|
||||
raise TypeError(
|
||||
f'Unknown geometry type {self.feature["geometry"]["type"]} '
|
||||
f'in {self.__class__.__name} {self.name}'
|
||||
)
|
||||
|
||||
@property
|
||||
def unenclosed_polygons(self):
|
||||
def _unenclosed_polygons(self, feature):
|
||||
# Some mapping tools require shapes to be unenclosed, i.e. the
|
||||
# last point joins the first point implicitly
|
||||
return [
|
||||
coordinates[:-1] for coordinates in self.polygons
|
||||
coordinates[:-1] for coordinates in self._polygons(feature)
|
||||
]
|
||||
|
||||
@property
|
||||
def polygons(self):
|
||||
return self._polygons(self.feature)
|
||||
|
||||
@property
|
||||
def unenclosed_polygons(self):
|
||||
return self._unenclosed_polygons(self.feature)
|
||||
|
||||
@property
|
||||
def simple_polygons(self):
|
||||
return self._polygons(self.simple_feature)
|
||||
|
||||
@property
|
||||
def simple_unenclosed_polygons(self):
|
||||
return self._unenclosed_polygons(self.simple_feature)
|
||||
|
||||
|
||||
class BroadcastAreaLibrary(SerialisedModelCollection, IdentifiableMixin, IdFromNameMixin, GetItemByIdMixin):
|
||||
|
||||
|
||||
@@ -1,12 +1,51 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from copy import deepcopy
|
||||
import geojson
|
||||
from pathlib import Path
|
||||
import shapely.geometry as sgeom
|
||||
|
||||
from repo import BroadcastAreasRepository
|
||||
|
||||
package_path = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def simplify_polygon(series):
|
||||
polygon, *_holes = series # discard holes
|
||||
|
||||
approx_metres_to_degree = 111320
|
||||
desired_resolution_metres = 10
|
||||
simplify_degrees = desired_resolution_metres / approx_metres_to_degree
|
||||
|
||||
simplified_polygon = None
|
||||
num_polys = len(polygon)
|
||||
while True:
|
||||
simplified_polygon = sgeom.LineString(polygon)
|
||||
simplified_polygon = simplified_polygon.simplify(simplify_degrees)
|
||||
simplified_polygon = [[c[0], c[1]] for c in simplified_polygon.coords]
|
||||
|
||||
num_polys = len(simplified_polygon)
|
||||
simplify_degrees *= 1.5
|
||||
if num_polys <= 125:
|
||||
break
|
||||
|
||||
return [simplified_polygon]
|
||||
|
||||
|
||||
def simplify_geometry(feature):
|
||||
if feature["type"] == "Polygon":
|
||||
feature["coordinates"] = simplify_polygon(feature["coordinates"])
|
||||
return feature
|
||||
elif feature["type"] == "MultiPolygon":
|
||||
feature["coordinates"] = [
|
||||
simplify_polygon(polygon)
|
||||
for polygon in feature["coordinates"]
|
||||
]
|
||||
return feature
|
||||
else:
|
||||
raise Exception("Unknown type: {}".format(feature["type"]))
|
||||
|
||||
|
||||
repo = BroadcastAreasRepository()
|
||||
|
||||
repo.delete_db()
|
||||
@@ -25,7 +64,10 @@ for dataset_name, name_field in simple_datasets:
|
||||
|
||||
for feature in dataset_geojson["features"]:
|
||||
f_name = feature["properties"][name_field]
|
||||
repo.insert_broadcast_areas([[f_name, dataset_name, feature]])
|
||||
|
||||
simple_feature = deepcopy(feature)
|
||||
simple_feature["geometry"] = simplify_geometry(simple_feature["geometry"])
|
||||
repo.insert_broadcast_areas([[f_name, dataset_name, feature, simple_feature]])
|
||||
|
||||
# https://geoportal.statistics.gov.uk/datasets/wards-may-2020-boundaries-uk-bgc
|
||||
# Converted to geojson manually from SHP because of GeoJSON download limits
|
||||
@@ -53,7 +95,11 @@ for f in geojson.loads(wards_filepath.read_text())["features"]:
|
||||
la_name = ward_code_to_la_mapping[ward_code]
|
||||
|
||||
f_name = "{} - {}".format(la_name, ward_name)
|
||||
areas_to_add.append([f_name, dataset_name, f])
|
||||
|
||||
sf = deepcopy(f)
|
||||
sf["geometry"] = simplify_geometry(sf["geometry"])
|
||||
|
||||
areas_to_add.append([f_name, dataset_name, f, sf])
|
||||
|
||||
except KeyError:
|
||||
print("Skipping", ward_code, ward_name) # noqa: T001
|
||||
|
||||
@@ -30,6 +30,7 @@ class BroadcastAreasRepository(object):
|
||||
name TEXT NOT NULL,
|
||||
broadcast_area_library_id TEXT NOT NULL,
|
||||
feature_geojson TEXT NOT NULL,
|
||||
simple_feature_geojson TEXT NOT NULL,
|
||||
|
||||
FOREIGN KEY (broadcast_area_library_id)
|
||||
REFERENCES broadcast_area_libraries(id)
|
||||
@@ -56,17 +57,21 @@ class BroadcastAreasRepository(object):
|
||||
q = """
|
||||
INSERT INTO broadcast_areas (
|
||||
id, name,
|
||||
broadcast_area_library_id, feature_geojson
|
||||
broadcast_area_library_id, feature_geojson, simple_feature_geojson
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
with self.conn() as conn:
|
||||
for name, area_name, feature in areas:
|
||||
for name, area_name, feature, simple_feature in areas:
|
||||
id = make_string_safe_for_id(name)
|
||||
area_id = make_string_safe_for_id(area_name)
|
||||
|
||||
conn.execute(q, (id, name, area_id, geojson.dumps(feature)))
|
||||
conn.execute(q, (
|
||||
id, name, area_id,
|
||||
geojson.dumps(feature),
|
||||
geojson.dumps(simple_feature),
|
||||
))
|
||||
|
||||
def query(self, sql, *args):
|
||||
with self.conn() as conn:
|
||||
@@ -116,14 +121,15 @@ class BroadcastAreasRepository(object):
|
||||
cursor = conn.cursor()
|
||||
|
||||
q = """
|
||||
SELECT id, name, feature_geojson FROM broadcast_areas
|
||||
SELECT id, name, feature_geojson, simple_feature_geojson
|
||||
FROM broadcast_areas
|
||||
WHERE id IN ({})
|
||||
""".format(("?," * len(*area_ids))[:-1])
|
||||
cursor.execute(q, *area_ids)
|
||||
results = cursor.fetchall()
|
||||
|
||||
areas = [
|
||||
(row[0], row[1], geojson.loads(row[2]))
|
||||
(row[0], row[1], geojson.loads(row[2]), geojson.loads(row[3]))
|
||||
for row in results
|
||||
]
|
||||
|
||||
@@ -131,14 +137,15 @@ class BroadcastAreasRepository(object):
|
||||
|
||||
def get_all_areas_for_library(self, library_id):
|
||||
q = """
|
||||
SELECT id, name, feature_geojson FROM broadcast_areas
|
||||
SELECT id, name, feature_geojson, simple_feature_geojson
|
||||
FROM broadcast_areas
|
||||
WHERE broadcast_area_library_id = ?
|
||||
"""
|
||||
|
||||
results = self.query(q, library_id)
|
||||
|
||||
areas = [
|
||||
(row[0], row[1], geojson.loads(row[2]))
|
||||
(row[0], row[1], geojson.loads(row[2]), geojson.loads(row[3]))
|
||||
for row in results
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user