From 969e7a6dbdc12d71268b4318e064588225c8f981 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 6 Aug 2020 13:38:15 +0100 Subject: [PATCH] Show how a broadcast will overspill selected area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/broadcast_areas/__init__.py | 12 ++++++++ .../create-broadcast-areas-db.py | 2 +- app/broadcast_areas/plot-areas.py | 2 +- app/models/broadcast_message.py | 21 +++++++++++++ .../views/broadcast/preview-areas.html | 15 ++++++++-- requirements-app.txt | 1 + requirements.txt | 1 + tests/__init__.py | 3 +- tests/app/models/test_broadcast_message.py | 30 +++++++++++++++++++ 9 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 tests/app/models/test_broadcast_message.py diff --git a/app/broadcast_areas/__init__.py b/app/broadcast_areas/__init__.py index a43af5a3b..3e914200f 100644 --- a/app/broadcast_areas/__init__.py +++ b/app/broadcast_areas/__init__.py @@ -138,5 +138,17 @@ class BroadcastAreaLibraries(SerialisedModelCollection, GetItemByIdMixin): for polygon in self.get_polygons_for_areas_long_lat(*area_ids) ] + def get_simple_polygons_for_areas_long_lat(self, *area_ids): + return list(itertools.chain(*( + area.simple_polygons + for area in self.get_areas(*area_ids) + ))) + + def get_simple_polygons_for_areas_lat_long(self, *area_ids): + return [ + [[long, lat] for lat, long in polygon] + for polygon in self.get_simple_polygons_for_areas_long_lat(*area_ids) + ] + broadcast_area_libraries = BroadcastAreaLibraries() diff --git a/app/broadcast_areas/create-broadcast-areas-db.py b/app/broadcast_areas/create-broadcast-areas-db.py index c39d824bf..89d32299e 100755 --- a/app/broadcast_areas/create-broadcast-areas-db.py +++ b/app/broadcast_areas/create-broadcast-areas-db.py @@ -4,9 +4,9 @@ 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 -import shapely.geometry as sgeom from repo import BroadcastAreasRepository package_path = Path(__file__).resolve().parent diff --git a/app/broadcast_areas/plot-areas.py b/app/broadcast_areas/plot-areas.py index a12232025..3527b5b5e 100755 --- a/app/broadcast_areas/plot-areas.py +++ b/app/broadcast_areas/plot-areas.py @@ -3,12 +3,12 @@ from random import sample import geojson +import shapely.geometry as sgeom from notifications_utils.safe_string import make_string_safe_for_id import cartopy.crs as ccrs import cartopy.feature as cfeature import matplotlib.pyplot as plt -import shapely.geometry as sgeom from repo import BroadcastAreasRepository diff --git a/app/models/broadcast_message.py b/app/models/broadcast_message.py index 649349631..0422fc077 100644 --- a/app/models/broadcast_message.py +++ b/app/models/broadcast_message.py @@ -2,6 +2,8 @@ from datetime import datetime from notifications_utils.template import BroadcastPreviewTemplate from orderedset import OrderedSet +from shapely.geometry import MultiPolygon, Polygon +from shapely.ops import unary_union from werkzeug.utils import cached_property from app.broadcast_areas import broadcast_area_libraries @@ -76,6 +78,25 @@ class BroadcastMessage(JSONModel): *self._dict['areas'] ) + @property + def simple_polygons(self): + simple_polygons = broadcast_area_libraries.get_simple_polygons_for_areas_lat_long( + *self._dict['areas'] + ) + unioned_polygons = unary_union([ + Polygon(i) for i in simple_polygons + ]) + if isinstance(unioned_polygons, MultiPolygon): + return [ + [ + [x, y] for x, y in p.exterior.coords + ] + for p in unioned_polygons + ] + return [[ + [x, y] for x, y in unioned_polygons.exterior.coords + ]] + @property def template(self): response = service_api_client.get_service_template( diff --git a/app/templates/views/broadcast/preview-areas.html b/app/templates/views/broadcast/preview-areas.html index 8bb453039..11f019393 100644 --- a/app/templates/views/broadcast/preview-areas.html +++ b/app/templates/views/broadcast/preview-areas.html @@ -36,12 +36,23 @@ attribution: '© OpenStreetMap contributors' }).addTo(mymap); + {% for polygon in broadcast_message.simple_polygons %} + polygons.push( + L.polygon({{polygon}}, { + color: '#2B8CC4', // $light-blue + opacity: 0.4, + fillColor: '#2B8CC4', // $light-blue + fillOpacity: 0.3, + weight: 1 + }) + ); + {% endfor %} + {% for polygon in broadcast_message.polygons %} polygons.push( L.polygon({{polygon}}, { color: '#0b0b0c', // $black - fillColor: '#2B8CC4', // $light-blue - fillOpacity: 0.2, + fillOpacity: 0, weight: 2 }) ); diff --git a/requirements-app.txt b/requirements-app.txt index 3a49facb9..262b5da4c 100644 --- a/requirements-app.txt +++ b/requirements-app.txt @@ -18,6 +18,7 @@ pytz==2020.1 gunicorn==20.0.4 eventlet==0.26.1 notifications-python-client==5.7.0 +Shapely==1.7.0 # PaaS awscli-cwlogs>=1.4,<1.5 diff --git a/requirements.txt b/requirements.txt index 1eadcee8e..f173f6ea8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ pytz==2020.1 gunicorn==20.0.4 eventlet==0.26.1 notifications-python-client==5.7.0 +Shapely==1.7.0 # PaaS awscli-cwlogs>=1.4,<1.5 diff --git a/tests/__init__.py b/tests/__init__.py index d6b042e6d..159b9a7c3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -649,6 +649,7 @@ def broadcast_message_json( cancelled_at=None, approved_by_id=None, cancelled_by_id=None, + areas=None, ): return { 'id': id_, @@ -660,7 +661,7 @@ def broadcast_message_json( 'template_name': 'Example template', 'personalisation': {}, - 'areas': [ + 'areas': areas or [ 'countries-E92000001', 'countries-S92000003', ], diff --git a/tests/app/models/test_broadcast_message.py b/tests/app/models/test_broadcast_message.py new file mode 100644 index 000000000..0776683e3 --- /dev/null +++ b/tests/app/models/test_broadcast_message.py @@ -0,0 +1,30 @@ +from app.models.broadcast_message import BroadcastMessage +from tests import broadcast_message_json + + +def test_simple_polygons(fake_uuid): + broadcast_message = BroadcastMessage(broadcast_message_json( + id_=fake_uuid, + service_id=fake_uuid, + template_id=fake_uuid, + status='draft', + created_by_id=fake_uuid, + areas=[ + # Hackney Central + 'electoral-wards-of-the-united-kingdom-E05009372', + # Hackney Wick + 'electoral-wards-of-the-united-kingdom-E05009374', + ], + )) + + assert [ + [len(polygon) for polygon in broadcast_message.polygons], + [len(polygon) for polygon in broadcast_message.simple_polygons], + ] == [ + # One polygon for each area + [27, 31], + # Because the areas are close to each other, the simplification + # and unioning process results in a single polygon with fewer + # total coordinates + [34], + ]