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], + ]