mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-09 17:45:32 -04:00
Broadcasting is not a precise technology, because: - cell towers are directional - their range varies depending on whether they are 2, 3, 4, or 5G (the higher the bandwidth the shorter the range) - in urban areas the towers are more densely packed, so a phone is likely to have a greater choice of tower to connect to, and will favour a closer one (which has a stronger signal) - topography and even weather can affect the range of a tower So it’s good for us to visually indicate that the broadcast is not as precise as the boundaries of the area, because it gives the person sending the message an indication of how the technology works. At the same time we have a restriction on the number of polygons we think and area can have, so we’ve done some work to make versions of polygons which are simplified and buffered (see https://github.com/alphagov/notifications-utils/pull/769 for context). Serendipitously, the simplified and buffered polygons are larger and smoother than the detailed polygons we’ve got from the GeoJSON files. So they naturally give the impression of covering an area which is wider and less precise. So this commit takes those simple polygons and uses them to render the blue fill. This makes the blue fill extend outside the black stroke, which is still using the detailed polygons direct from the GeoJSON.
167 lines
5.0 KiB
Python
Executable File
167 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
|
|
import geojson
|
|
import shapely.geometry as sgeom
|
|
from notifications_utils.safe_string import make_string_safe_for_id
|
|
|
|
from repo import BroadcastAreasRepository
|
|
|
|
package_path = Path(__file__).resolve().parent
|
|
|
|
|
|
def convert_shape_to_feature(shape):
|
|
return {
|
|
"type": "Feature",
|
|
"properties": {},
|
|
"geometry": sgeom.mapping(shape),
|
|
}
|
|
|
|
|
|
def simplify_polygon(series):
|
|
polygon, *_holes = series # discard holes
|
|
|
|
approx_metres_to_degree = 111320
|
|
# initially buffer (extend area past perimeter) by ~25m
|
|
buffer_degrees = 500 / approx_metres_to_degree
|
|
# initially simplify (snap to closest point) by ~25m
|
|
simplify_degrees = 50.0 / approx_metres_to_degree
|
|
|
|
starting_polygon = sgeom.LineString(polygon).buffer(buffer_degrees)
|
|
simplified_polygon = None
|
|
num_polys = len(polygon)
|
|
last_num_polys = []
|
|
while True:
|
|
simplified_polygon = starting_polygon.simplify(simplify_degrees)
|
|
simplified_polygon = [[c[0], c[1]] for c in simplified_polygon.exterior.coords]
|
|
|
|
num_polys = len(simplified_polygon)
|
|
simplify_degrees *= 2
|
|
|
|
if num_polys <= 99 or last_num_polys[-3:] == [num_polys, num_polys, num_polys]:
|
|
break
|
|
|
|
last_num_polys.append(num_polys)
|
|
|
|
print(".", end="", flush=True) # noqa: T001
|
|
|
|
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()
|
|
repo.create_tables()
|
|
|
|
simple_datasets = [
|
|
("Countries", "ctry19cd", "ctry19nm"),
|
|
("Counties and Unitary Authorities in England and Wales", "ctyua16cd", "ctyua16nm"),
|
|
]
|
|
for dataset_name, id_field, name_field in simple_datasets:
|
|
filepath = package_path / "{}.geojson".format(dataset_name)
|
|
|
|
dataset_id = make_string_safe_for_id(dataset_name)
|
|
dataset_geojson = geojson.loads(filepath.read_text())
|
|
|
|
repo.insert_broadcast_area_library(dataset_id, dataset_name, False)
|
|
|
|
for feature in dataset_geojson["features"]:
|
|
f_id = dataset_id + "-" + feature["properties"][id_field]
|
|
f_name = feature["properties"][name_field]
|
|
|
|
print(f_name) # noqa: T001
|
|
|
|
simple_feature = deepcopy(feature)
|
|
simple_feature["geometry"] = simplify_geometry(simple_feature["geometry"])
|
|
|
|
repo.insert_broadcast_areas([[
|
|
f_id, f_name,
|
|
dataset_id, None,
|
|
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
|
|
wards_filepath = package_path / "Electoral Wards May 2020.geojson"
|
|
|
|
# http://geoportal.statistics.gov.uk/datasets/ward-to-westminster-parliamentary-constituency-to-local-authority-district-december-2019-lookup-in-the-united-kingdom/data
|
|
las_filepath = package_path / "Electoral Wards and Local Authorities 2020.geojson"
|
|
|
|
ward_code_to_la_mapping = {
|
|
f["properties"]["WD19CD"]: f["properties"]["LAD19NM"]
|
|
for f in geojson.loads(las_filepath.read_text())["features"]
|
|
}
|
|
ward_code_to_la_id_mapping = {
|
|
f["properties"]["WD19CD"]: f["properties"]["LAD19CD"]
|
|
for f in geojson.loads(las_filepath.read_text())["features"]
|
|
}
|
|
|
|
dataset_name = "Electoral Wards of the United Kingdom"
|
|
dataset_id = make_string_safe_for_id(dataset_name)
|
|
repo.insert_broadcast_area_library(dataset_id, dataset_name, True)
|
|
|
|
areas_to_add = []
|
|
|
|
for f in geojson.loads(wards_filepath.read_text())["features"]:
|
|
ward_code = f["properties"]["wd20cd"]
|
|
ward_name = f["properties"]["wd20nm"]
|
|
ward_id = dataset_id + "-" + ward_code
|
|
|
|
print(ward_name) # noqa: T001
|
|
|
|
try:
|
|
la_id = dataset_id + "-" + ward_code_to_la_id_mapping[ward_code]
|
|
la_name = ward_code_to_la_mapping[ward_code]
|
|
|
|
sf = deepcopy(f)
|
|
sf["geometry"] = simplify_geometry(sf["geometry"])
|
|
|
|
areas_to_add.append([
|
|
ward_id, ward_name,
|
|
dataset_id, la_id,
|
|
f, sf
|
|
])
|
|
|
|
except KeyError:
|
|
print("Skipping", ward_code, ward_name) # noqa: T001
|
|
|
|
repo.insert_broadcast_areas(areas_to_add)
|
|
areas_to_add = []
|
|
|
|
las_filepath = package_path / "Local Authorities May 2020.geojson"
|
|
|
|
for feature in geojson.loads(las_filepath.read_text())["features"]:
|
|
la_id = feature["properties"]["lad20cd"]
|
|
group_name = feature["properties"]["lad20nm"]
|
|
|
|
print(group_name) # noqa: T001
|
|
|
|
group_id = dataset_id + "-" + la_id
|
|
|
|
simple_feature = deepcopy(feature)
|
|
simple_feature["geometry"] = simplify_geometry(simple_feature["geometry"])
|
|
|
|
areas_to_add.append([
|
|
group_id, group_name,
|
|
dataset_id, None,
|
|
feature, simple_feature
|
|
])
|
|
|
|
repo.insert_broadcast_areas(areas_to_add)
|