diff --git a/notifications_utils/broadcast_areas/__init__.py b/notifications_utils/broadcast_areas/__init__.py index 7a2e408a9..290e6a3f1 100644 --- a/notifications_utils/broadcast_areas/__init__.py +++ b/notifications_utils/broadcast_areas/__init__.py @@ -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): diff --git a/notifications_utils/broadcast_areas/create-broadcast-areas-db.py b/notifications_utils/broadcast_areas/create-broadcast-areas-db.py index f9fd1e575..151c24b45 100755 --- a/notifications_utils/broadcast_areas/create-broadcast-areas-db.py +++ b/notifications_utils/broadcast_areas/create-broadcast-areas-db.py @@ -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 diff --git a/notifications_utils/broadcast_areas/repo.py b/notifications_utils/broadcast_areas/repo.py index 61eb24ea4..fc95bec06 100644 --- a/notifications_utils/broadcast_areas/repo.py +++ b/notifications_utils/broadcast_areas/repo.py @@ -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 ]