Show how a broadcast will overspill selected area

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.
This commit is contained in:
Chris Hill-Scott
2020-08-06 13:38:15 +01:00
parent 513d11eb98
commit 969e7a6dbd
9 changed files with 82 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,12 +36,23 @@
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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
})
);

View File

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

View File

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

View File

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

View File

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