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:
Toby Lorne
2020-07-28 18:38:40 +01:00
parent 7074655b84
commit 6d5593dc32
3 changed files with 88 additions and 20 deletions

View File

@@ -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):

View File

@@ -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

View File

@@ -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
]