Vary bleed amount based on population density
There are basically two kinds of 4G masts:
Frequency | Range | Bandwidth
----------|-------------|----------------------------------
800MHz | Long (500m) | Low (can handle a bit of traffic)
1800Mhz | Short (5km) | High (can handle lots of traffic)
The 1800Mhz masts are better in terms of how much traffic they can
handle and how fast a connection they provide. But because they have
quite short range, it’s only economical to install them in very built up
areas†.
In more rural areas the 800MHz masts are better because they cover a
wider area, and have enough bandwidth for the lower population density.
The net effect of this is that cell broadcasts in rural areas are likely
to bleed further, because the masts they are being broadcast from are
less precise.
We can use population density as a proxy for how likely it is to be
covered by 1800Mhz masts, and therefore how much bleed we should expect.
So this commit varies the amount of bleed shown based on the population
density.
I came up with the formula based on 3 fixed points:
- The most remote areas (for example the Scottish Highlands) should have
the highest average bleed, estimated at 5km
- An town, like Crewe, should have about the same bleed as we were
estimating before (1.5km) – Pete D thinks this is about right based on
his knowledge of the area around his office in Crewe
- The most built up areas, like London boroughs, could have as little as
500m of bleed
Based on these three figures I came up with the following formula, which
roughly gives the right bleed distance (`b`) for each of their population
densities (`d`):
```
b = 5900 - (log10(d) × 1_250)
```
Plotted on a curve it looks like this:
This is based on averages – remember that the UI shows where is _likely_
to receive the alert, based on bleed, not where it’s _possible_ to
receive the alert.
Here’s what it looks like on the map:
---
†There are some additional subtleties which make this not strictly true:
- The 800Mhz masts are also used in built up areas to fill in the gaps
between the areas covered by the 1800Mhz masts
- Switching between masts is inefficient, so if you’re moving fast
through a built up area (for example on a train) your phone will only
use the 800MHz masts so that you have to handoff from one mast to
another less often
2021-03-12 09:17:42 +00:00
|
|
|
|
import math
|
2021-03-19 16:21:43 +00:00
|
|
|
|
from abc import ABC, abstractmethod
|
Vary bleed amount based on population density
There are basically two kinds of 4G masts:
Frequency | Range | Bandwidth
----------|-------------|----------------------------------
800MHz | Long (500m) | Low (can handle a bit of traffic)
1800Mhz | Short (5km) | High (can handle lots of traffic)
The 1800Mhz masts are better in terms of how much traffic they can
handle and how fast a connection they provide. But because they have
quite short range, it’s only economical to install them in very built up
areas†.
In more rural areas the 800MHz masts are better because they cover a
wider area, and have enough bandwidth for the lower population density.
The net effect of this is that cell broadcasts in rural areas are likely
to bleed further, because the masts they are being broadcast from are
less precise.
We can use population density as a proxy for how likely it is to be
covered by 1800Mhz masts, and therefore how much bleed we should expect.
So this commit varies the amount of bleed shown based on the population
density.
I came up with the formula based on 3 fixed points:
- The most remote areas (for example the Scottish Highlands) should have
the highest average bleed, estimated at 5km
- An town, like Crewe, should have about the same bleed as we were
estimating before (1.5km) – Pete D thinks this is about right based on
his knowledge of the area around his office in Crewe
- The most built up areas, like London boroughs, could have as little as
500m of bleed
Based on these three figures I came up with the following formula, which
roughly gives the right bleed distance (`b`) for each of their population
densities (`d`):
```
b = 5900 - (log10(d) × 1_250)
```
Plotted on a curve it looks like this:
This is based on averages – remember that the UI shows where is _likely_
to receive the alert, based on bleed, not where it’s _possible_ to
receive the alert.
Here’s what it looks like on the map:
---
†There are some additional subtleties which make this not strictly true:
- The 800Mhz masts are also used in built up areas to fill in the gaps
between the areas covered by the 1800Mhz masts
- Switching between masts is inefficient, so if you’re moving fast
through a built up area (for example on a train) your phone will only
use the 800MHz masts so that you have to handoff from one mast to
another less often
2021-03-12 09:17:42 +00:00
|
|
|
|
|
2020-09-09 17:35:17 +01:00
|
|
|
|
from notifications_utils.formatters import formatted_list
|
2021-01-25 13:49:23 +00:00
|
|
|
|
from notifications_utils.polygons import Polygons
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
from notifications_utils.serialised_model import SerialisedModelCollection
|
2021-04-13 12:43:28 +01:00
|
|
|
|
from rtreelib import Rect
|
2020-08-10 10:46:31 +01:00
|
|
|
|
from werkzeug.utils import cached_property
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
2020-09-16 11:35:40 +01:00
|
|
|
|
from .populations import CITY_OF_LONDON
|
2021-03-18 23:02:32 +00:00
|
|
|
|
from .repo import BroadcastAreasRepository, rtree_index
|
2020-07-24 12:54:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
2020-07-31 13:21:21 +01:00
|
|
|
|
class SortableMixin:
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
2020-07-24 16:47:12 +01:00
|
|
|
|
def __repr__(self):
|
|
|
|
|
|
return f'{self.__class__.__name__}(<{self.id}>)'
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
2020-07-24 16:47:12 +01:00
|
|
|
|
def __lt__(self, other):
|
|
|
|
|
|
# Implementing __lt__ means any classes inheriting from this
|
|
|
|
|
|
# method are sortable
|
2020-07-31 13:21:21 +01:00
|
|
|
|
return self.name < other.name
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
Suggest previously-used areas when adding new area
If you’re adding another area to your broadcast it’s likely to be close
to one of the areas you’ve already added.
But we make you start by choosing a library, then you have to find the
local authority again from the long list. This is clunky, and it
interrupts the task the user is trying to complete.
We thought about redirecting you somewhere deep into the hierarchy,
perhaps by sending you to either:
- the parent of the last area you’d chosen
- the common ancestor of all the areas you’d chosen
This approach would however mean you’d need a way to navigate back up
the hierarchy if we’d dropped you in the wrong place. And we don’t have
a pattern for that at the moment.
So instead this commit adds some ‘shortcuts’ to the chose library page,
giving you a choice of all the parents of the areas you’ve currently
selected. In most cases this will be one (unitary authority) or two
(county and district) choices, but it will scale to adding areas from
multiple different authorities.
It does mean an extra click compared to the redirect approach, but this
is still fewer, easier clicks compared to now.
This meant a couple of under-the-hood changes:
- making `BroadcastArea`s hashable so it’s possible to do
`set([BroadcastArea(…), BroadcastArea(…), BroadcastArea(…)])`
- making `BroadcastArea`s aware of which library they live in, so we can
link to the correct _Choose area_ page
2020-09-21 18:55:46 +01:00
|
|
|
|
def __eq__(self, other):
|
|
|
|
|
|
return self.id == other.id
|
|
|
|
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
|
|
return hash(self.id)
|
|
|
|
|
|
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
|
|
|
|
|
class GetItemByIdMixin:
|
|
|
|
|
|
def get(self, id):
|
|
|
|
|
|
for item in self:
|
|
|
|
|
|
if item.id == id:
|
|
|
|
|
|
return item
|
|
|
|
|
|
raise KeyError(id)
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-03-19 16:21:43 +00:00
|
|
|
|
class BaseBroadcastArea(ABC):
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def simple_polygons(self):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def polygons(self):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def count_of_phones(self):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
|
def simple_polygons_with_bleed(self):
|
|
|
|
|
|
return self.simple_polygons.bleed_by(self.estimated_bleed_in_degrees)
|
|
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
|
def phone_density(self):
|
|
|
|
|
|
if not self.polygons.estimated_area:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
return self.count_of_phones / self.polygons.estimated_area
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def estimated_bleed_in_m(self):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Estimates the amount of bleed based on the population of an
|
|
|
|
|
|
area. Higher density areas tend to have short range masts, so
|
|
|
|
|
|
the bleed is low (down to 500m). Lower density areas have longer
|
|
|
|
|
|
range masts, so the typical bleed will be high (up to 5,000m).
|
|
|
|
|
|
'''
|
|
|
|
|
|
if self.phone_density < 1:
|
|
|
|
|
|
return Polygons.approx_bleed_in_degrees * Polygons.approx_metres_to_degree
|
|
|
|
|
|
estimated_bleed = 5_900 - (math.log(self.phone_density, 10) * 1_250)
|
|
|
|
|
|
return max(500, min(estimated_bleed, 5000))
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def estimated_bleed_in_degrees(self):
|
|
|
|
|
|
return self.estimated_bleed_in_m / Polygons.approx_metres_to_degree
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BroadcastArea(BaseBroadcastArea, SortableMixin):
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
2020-07-24 16:47:12 +01:00
|
|
|
|
def __init__(self, row):
|
Suggest previously-used areas when adding new area
If you’re adding another area to your broadcast it’s likely to be close
to one of the areas you’ve already added.
But we make you start by choosing a library, then you have to find the
local authority again from the long list. This is clunky, and it
interrupts the task the user is trying to complete.
We thought about redirecting you somewhere deep into the hierarchy,
perhaps by sending you to either:
- the parent of the last area you’d chosen
- the common ancestor of all the areas you’d chosen
This approach would however mean you’d need a way to navigate back up
the hierarchy if we’d dropped you in the wrong place. And we don’t have
a pattern for that at the moment.
So instead this commit adds some ‘shortcuts’ to the chose library page,
giving you a choice of all the parents of the areas you’ve currently
selected. In most cases this will be one (unitary authority) or two
(county and district) choices, but it will scale to adding areas from
multiple different authorities.
It does mean an extra click compared to the redirect approach, but this
is still fewer, easier clicks compared to now.
This meant a couple of under-the-hood changes:
- making `BroadcastArea`s hashable so it’s possible to do
`set([BroadcastArea(…), BroadcastArea(…), BroadcastArea(…)])`
- making `BroadcastArea`s aware of which library they live in, so we can
link to the correct _Choose area_ page
2020-09-21 18:55:46 +01:00
|
|
|
|
self.id, self.name, self._count_of_phones, self.library_id = row
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
2020-07-31 14:01:49 +01:00
|
|
|
|
@cached_property
|
2020-08-24 14:43:28 +01:00
|
|
|
|
def polygons(self):
|
2020-08-25 16:55:09 +01:00
|
|
|
|
return Polygons(
|
|
|
|
|
|
BroadcastAreasRepository().get_polygons_for_area(self.id)
|
|
|
|
|
|
)
|
2020-07-30 17:28:08 +01:00
|
|
|
|
|
2020-07-31 14:01:49 +01:00
|
|
|
|
@cached_property
|
2020-08-24 14:43:28 +01:00
|
|
|
|
def simple_polygons(self):
|
2020-08-25 16:55:09 +01:00
|
|
|
|
return Polygons(
|
|
|
|
|
|
BroadcastAreasRepository().get_simple_polygons_for_area(self.id)
|
|
|
|
|
|
)
|
2020-07-30 17:24:20 +01:00
|
|
|
|
|
add counties page
What was previously ward -> local authority is now a ward -> local
authority -> county. County only covers rural counties and not
metropolitan boroughs and other unitary authorities. Previously, there
was a page full of local authorities (unitary authorities and
districts), and each one of those would have a list of electoral wards.
However, now there are counties that contain a list of districts - so
this needs a new page - a checkbox for "select the county" and then a
list of links to district pages.
If you want to select multiple districts, you'll need to go into each
one of those sub-sections in turn and click select all.
Needed to tweak the query to retrieve the list of areas in a list for a
library. Previously, it just returned anything at top level (ie: didn't
have a parent). However, rural districts now have parents (the rural
counties themselves). So the query now returns "everything that isn't a
leaf node", or in more specific terms, everything that has at least
other row referring to it as a parent. So no electoral wards, since
they dont have any children, but yes to districts and counties.
2020-09-04 15:22:32 +01:00
|
|
|
|
@cached_property
|
2020-07-31 13:21:21 +01:00
|
|
|
|
def sub_areas(self):
|
|
|
|
|
|
return [
|
|
|
|
|
|
BroadcastArea(row)
|
|
|
|
|
|
for row in BroadcastAreasRepository().get_all_areas_for_group(self.id)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2020-09-09 13:29:45 +01:00
|
|
|
|
@property
|
|
|
|
|
|
def count_of_phones(self):
|
|
|
|
|
|
if self.id.endswith(CITY_OF_LONDON.WARDS):
|
|
|
|
|
|
return CITY_OF_LONDON.DAYTIME_POPULATION * (
|
|
|
|
|
|
self.polygons.estimated_area / CITY_OF_LONDON.AREA_SQUARE_MILES
|
|
|
|
|
|
)
|
|
|
|
|
|
if self.sub_areas:
|
2020-09-17 11:00:17 +01:00
|
|
|
|
return sum(area.count_of_phones for area in self.sub_areas)
|
|
|
|
|
|
# TODO: remove the `or 0` once missing data is fixed, see
|
|
|
|
|
|
# https://www.pivotaltracker.com/story/show/174837293
|
|
|
|
|
|
return self._count_of_phones or 0
|
2020-09-09 13:29:45 +01:00
|
|
|
|
|
Suggest previously-used areas when adding new area
If you’re adding another area to your broadcast it’s likely to be close
to one of the areas you’ve already added.
But we make you start by choosing a library, then you have to find the
local authority again from the long list. This is clunky, and it
interrupts the task the user is trying to complete.
We thought about redirecting you somewhere deep into the hierarchy,
perhaps by sending you to either:
- the parent of the last area you’d chosen
- the common ancestor of all the areas you’d chosen
This approach would however mean you’d need a way to navigate back up
the hierarchy if we’d dropped you in the wrong place. And we don’t have
a pattern for that at the moment.
So instead this commit adds some ‘shortcuts’ to the chose library page,
giving you a choice of all the parents of the areas you’ve currently
selected. In most cases this will be one (unitary authority) or two
(county and district) choices, but it will scale to adding areas from
multiple different authorities.
It does mean an extra click compared to the redirect approach, but this
is still fewer, easier clicks compared to now.
This meant a couple of under-the-hood changes:
- making `BroadcastArea`s hashable so it’s possible to do
`set([BroadcastArea(…), BroadcastArea(…), BroadcastArea(…)])`
- making `BroadcastArea`s aware of which library they live in, so we can
link to the correct _Choose area_ page
2020-09-21 18:55:46 +01:00
|
|
|
|
@cached_property
|
|
|
|
|
|
def parents(self):
|
|
|
|
|
|
return list(filter(None, self._parents_iterator))
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def _parents_iterator(self):
|
|
|
|
|
|
id = self.id
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
parent = BroadcastAreasRepository().get_parent_for_area(id)
|
|
|
|
|
|
|
|
|
|
|
|
if not parent:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
parent_broadcast_area = BroadcastArea(parent)
|
|
|
|
|
|
|
|
|
|
|
|
yield parent_broadcast_area
|
|
|
|
|
|
|
|
|
|
|
|
id = parent_broadcast_area.id
|
|
|
|
|
|
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
2021-03-19 16:21:43 +00:00
|
|
|
|
class CustomBroadcastArea(BaseBroadcastArea):
|
2021-01-26 10:14:04 +00:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, *, name, polygons=None):
|
|
|
|
|
|
self.name = name
|
|
|
|
|
|
self._polygons = polygons or []
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def polygons(self):
|
|
|
|
|
|
return Polygons(
|
|
|
|
|
|
# Polygons in the DB are stored with the coordinate pair
|
|
|
|
|
|
# order flipped – this flips them back again
|
|
|
|
|
|
Polygons(self._polygons).as_coordinate_pairs_lat_long
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
simple_polygons = polygons
|
|
|
|
|
|
|
2021-03-18 23:02:32 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def overlapping_areas(self):
|
|
|
|
|
|
if not self.polygons:
|
|
|
|
|
|
return []
|
2021-04-13 12:43:28 +01:00
|
|
|
|
return broadcast_area_libraries.get_areas([
|
|
|
|
|
|
overlap.data for overlap in rtree_index.query(
|
|
|
|
|
|
Rect(*self.polygons.bounds)
|
|
|
|
|
|
)
|
|
|
|
|
|
])
|
2021-03-18 23:02:32 +00:00
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
|
def count_of_phones(self):
|
|
|
|
|
|
return sum(
|
|
|
|
|
|
area.polygons.ratio_of_intersection_with(self.polygons) * area.count_of_phones
|
|
|
|
|
|
for area in self.overlapping_areas
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2021-01-26 10:14:04 +00:00
|
|
|
|
|
|
|
|
|
|
class CustomBroadcastAreas(SerialisedModelCollection):
|
|
|
|
|
|
model = CustomBroadcastArea
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, *, areas, polygons):
|
|
|
|
|
|
self.items = areas
|
|
|
|
|
|
self._polygons = polygons
|
|
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, index):
|
|
|
|
|
|
return self.model(
|
|
|
|
|
|
name=self.items[index],
|
|
|
|
|
|
polygons=self._polygons if index == 0 else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-07-31 13:21:21 +01:00
|
|
|
|
class BroadcastAreaLibrary(SerialisedModelCollection, SortableMixin, GetItemByIdMixin):
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
|
|
|
|
|
model = BroadcastArea
|
|
|
|
|
|
|
2020-07-31 13:21:21 +01:00
|
|
|
|
def __init__(self, row):
|
2020-08-13 17:33:58 +01:00
|
|
|
|
id, name, name_singular, is_group = row
|
2020-07-31 13:21:21 +01:00
|
|
|
|
self.id = id
|
|
|
|
|
|
self.name = name
|
2020-08-13 17:33:58 +01:00
|
|
|
|
self.name_singular = name_singular
|
2020-07-31 13:21:21 +01:00
|
|
|
|
self.is_group = bool(is_group)
|
2020-08-12 10:17:45 +01:00
|
|
|
|
self.items = BroadcastAreasRepository().get_all_areas_for_library(self.id)
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
2020-07-24 16:47:12 +01:00
|
|
|
|
def get_examples(self):
|
2020-09-09 17:35:17 +01:00
|
|
|
|
# we show up to four things. three areas, then either a fourth area if there are exactly four, or "and X more".
|
|
|
|
|
|
areas_to_show = sorted(area.name for area in self)[:4]
|
|
|
|
|
|
|
|
|
|
|
|
count_of_areas_not_named = len(self.items) - 3
|
|
|
|
|
|
# if there's exactly one area not named, there are exactly four - we should just show all four.
|
|
|
|
|
|
if count_of_areas_not_named > 1:
|
|
|
|
|
|
areas_to_show = areas_to_show[:3] + [f'{count_of_areas_not_named} more…']
|
|
|
|
|
|
|
|
|
|
|
|
return formatted_list(areas_to_show, before_each='', after_each='')
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BroadcastAreaLibraries(SerialisedModelCollection, GetItemByIdMixin):
|
|
|
|
|
|
|
|
|
|
|
|
model = BroadcastAreaLibrary
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
2020-08-12 10:13:16 +01:00
|
|
|
|
self.items = BroadcastAreasRepository().get_libraries()
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
|
|
|
|
|
def get_areas(self, *area_ids):
|
2020-07-09 19:10:51 +01:00
|
|
|
|
# allow people to call `get_areas('a', 'b') or get_areas(['a', 'b'])`
|
|
|
|
|
|
if len(area_ids) == 1 and isinstance(area_ids[0], list):
|
|
|
|
|
|
area_ids = area_ids[0]
|
|
|
|
|
|
|
2020-07-24 16:47:12 +01:00
|
|
|
|
areas = BroadcastAreasRepository().get_areas(area_ids)
|
|
|
|
|
|
return [BroadcastArea(area) for area in areas]
|
Add broadcast area model, loading from GeoJSON
This commit adds a new model class which can be used by any app to
interact with a broadcast area. A broadcast area is one or more polygons
representing geographical areas.
It also adds some models that make browsing collections of these areas
more straightforward. So the hierarchy looks like:
> **BroadcastAreaLibraries*
> Contains multiple libraries of broadcast area
> > **BroadcastAreaLibrary**
> > A collection of geographic areas, all of the same type, for example
> > counties or electoral wards
> > **BroadcastArea**
> > Contains one or more shapes that make up an area, for example
> > England
> > > **BroadcastArea.polygons[n]**
> > > A single shape, for example the Isle of Wight or Lindisfarne
> > > > **BroadcastArea.polygons[n][o]**
> > > > A single coordinate along a polygons
The classes support iteration, so all the areas in a library can be
looped over, for example if `countries` is an instance of
`BroadcastAreaLibrary` you can do:
```python
for country in countries:
print(country.name)
```
The `BroadcastAreaLibraries` class also provides some useful methods for
quickly getting the polygons for an area or areas, for example to
render them on a map. So if `libraries` is an instance of
`BroadcastAreaLibraries` you can do:
```python
libraries.get_polygons_for_areas_long_lat('england', 'wales')
```
This will give polygons for the Welsh mainland, the Isle of Wight,
Anglesey, etc.
The models load data from GeoJSON files, which is an open standard for
serialising geographic data. I’ve added a few example files taken from
http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
broadcast_area_libraries = BroadcastAreaLibraries()
|