2020-07-24 11:46:21 +01:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
2020-08-24 18:34:00 +01:00
|
|
|
|
import csv
|
2021-04-13 12:43:28 +01:00
|
|
|
|
import pickle
|
2020-08-24 20:40:42 +01:00
|
|
|
|
import sys
|
2021-06-24 12:16:16 +01:00
|
|
|
|
from math import isclose
|
2020-07-24 11:46:21 +01:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2020-08-10 10:46:31 +01:00
|
|
|
|
import geojson
|
2020-08-24 14:43:28 +01:00
|
|
|
|
from notifications_utils.formatters import formatted_list
|
2021-01-25 13:49:23 +00:00
|
|
|
|
from notifications_utils.polygons import Polygons
|
2020-09-16 11:35:40 +01:00
|
|
|
|
from populations import (
|
2020-09-09 13:29:45 +01:00
|
|
|
|
BRYHER,
|
|
|
|
|
|
CITY_OF_LONDON,
|
|
|
|
|
|
MEDIAN_AGE_RANGE_UK,
|
|
|
|
|
|
MEDIAN_AGE_UK,
|
|
|
|
|
|
SMARTPHONE_OWNERSHIP_BY_AGE_RANGE,
|
2020-09-16 11:33:57 +01:00
|
|
|
|
estimate_number_of_smartphones_for_population,
|
2020-09-09 13:29:45 +01:00
|
|
|
|
)
|
2021-04-13 12:43:28 +01:00
|
|
|
|
from repo import BroadcastAreasRepository, rtree_index_path
|
2021-04-13 16:31:06 +03:00
|
|
|
|
from rtreelib import Rect, RTree
|
2021-06-24 12:16:16 +01:00
|
|
|
|
from shapely import wkt
|
|
|
|
|
|
from shapely.geometry import MultiPolygon, Polygon
|
2020-07-24 16:45:41 +01:00
|
|
|
|
|
2020-08-20 18:54:57 +01:00
|
|
|
|
source_files_path = Path(__file__).resolve().parent / 'source_files'
|
2020-08-24 14:43:28 +01:00
|
|
|
|
point_counts = []
|
2021-06-24 12:16:16 +01:00
|
|
|
|
invalid_polygons = []
|
2021-04-13 12:43:28 +01:00
|
|
|
|
rtree_index = RTree()
|
2020-07-28 18:38:40 +01:00
|
|
|
|
|
2021-07-06 15:33:01 +01:00
|
|
|
|
# The hard limit in the CBCs is 6,000 points per polygon. But we also
|
|
|
|
|
|
# care about optimising how quickjly we can process and display polygons
|
|
|
|
|
|
# so we aim for something lower, i.e. enough to give us a good amount of
|
|
|
|
|
|
# precision relative to the accuracy of a cell broadcast
|
|
|
|
|
|
MAX_NUMBER_OF_POINTS_PER_POLYGON = 250
|
|
|
|
|
|
|
2020-07-28 18:38:40 +01:00
|
|
|
|
|
|
|
|
|
|
def simplify_geometry(feature):
|
|
|
|
|
|
if feature["type"] == "Polygon":
|
2020-08-24 14:43:28 +01:00
|
|
|
|
return [feature["coordinates"][0]]
|
2020-07-28 18:38:40 +01:00
|
|
|
|
elif feature["type"] == "MultiPolygon":
|
2020-08-24 14:43:28 +01:00
|
|
|
|
return [polygon for polygon, *_holes in feature["coordinates"]]
|
2020-07-28 18:38:40 +01:00
|
|
|
|
else:
|
|
|
|
|
|
raise Exception("Unknown type: {}".format(feature["type"]))
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-06-24 12:16:16 +01:00
|
|
|
|
def clean_up_invalid_polygons(polygons, indent=" "):
|
2021-08-03 18:46:30 +01:00
|
|
|
|
"""
|
|
|
|
|
|
This function expects a list of lists of coordinates defined in degrees
|
|
|
|
|
|
"""
|
2021-06-24 12:16:16 +01:00
|
|
|
|
for index, polygon in enumerate(polygons):
|
|
|
|
|
|
shapely_polygon = Polygon(polygon)
|
|
|
|
|
|
|
2021-08-03 18:46:30 +01:00
|
|
|
|
# Some of our data has points which are incredibly close
|
|
|
|
|
|
# together. In some cases they are close enough to be duplicates
|
|
|
|
|
|
# at a given precision, which makes an invalid topology. In
|
|
|
|
|
|
# other cases they are close enough that, when converting from
|
|
|
|
|
|
# one coordinate system to another, they shift about enough to
|
|
|
|
|
|
# create self-intersection. The fix in both cases is to reduce
|
|
|
|
|
|
# the precision of the coordinates and then apply simplification
|
|
|
|
|
|
# with a tolerance of 0.
|
|
|
|
|
|
simplified_polygon = wkt.loads(wkt.dumps(
|
|
|
|
|
|
shapely_polygon,
|
|
|
|
|
|
rounding_precision=Polygons.output_precision_in_decimal_places - 1
|
|
|
|
|
|
)).simplify(0)
|
|
|
|
|
|
|
|
|
|
|
|
if simplified_polygon.is_valid:
|
2021-06-24 12:16:16 +01:00
|
|
|
|
print( # noqa: T001
|
|
|
|
|
|
f"{indent}Polygon {index + 1}/{len(polygons)} is valid"
|
|
|
|
|
|
)
|
2021-08-03 18:46:30 +01:00
|
|
|
|
yield simplified_polygon
|
2021-06-24 12:16:16 +01:00
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
invalid_polygons.append(shapely_polygon)
|
|
|
|
|
|
|
|
|
|
|
|
# We’ve found polygons where all the points line up, so they
|
|
|
|
|
|
# don’t have an area. They wouldn’t contribute to a broadcast
|
|
|
|
|
|
# so we can ignore them.
|
2021-08-03 18:46:30 +01:00
|
|
|
|
if simplified_polygon.area == 0:
|
2021-06-24 12:16:16 +01:00
|
|
|
|
print( # noqa: T001
|
|
|
|
|
|
f"{indent}Polygon {index + 1}/{len(polygons)} has 0 area, skipping"
|
|
|
|
|
|
)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
print( # noqa: T001
|
|
|
|
|
|
f"{indent}Polygon {index + 1}/{len(polygons)} needs fixing..."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Buffering with a size of 0 is a trick to make valid
|
|
|
|
|
|
# geometries from polygons that self intersect
|
|
|
|
|
|
buffered = shapely_polygon.buffer(0)
|
|
|
|
|
|
|
|
|
|
|
|
# If the buffering has caused our polygon to split into
|
|
|
|
|
|
# multiple polygons, we need to recursively check them
|
|
|
|
|
|
# instead
|
|
|
|
|
|
if isinstance(buffered, MultiPolygon):
|
|
|
|
|
|
for sub_polygon in clean_up_invalid_polygons(buffered, indent=" "):
|
|
|
|
|
|
yield sub_polygon
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# We only care about the exterior of the polygon, not an
|
|
|
|
|
|
# holes in it that may have been created by fixing self
|
|
|
|
|
|
# intersection
|
|
|
|
|
|
fixed_polygon = Polygon(buffered.exterior)
|
|
|
|
|
|
|
|
|
|
|
|
# Make sure the polygon is now valid, and that we haven’t
|
|
|
|
|
|
# drastically transformed the polygon by ‘fixing’ it
|
|
|
|
|
|
assert fixed_polygon.is_valid
|
2021-08-03 18:46:30 +01:00
|
|
|
|
assert isclose(fixed_polygon.area, shapely_polygon.area, rel_tol=0.001)
|
2021-06-24 12:16:16 +01:00
|
|
|
|
|
|
|
|
|
|
print( # noqa: T001
|
|
|
|
|
|
f"{indent}Polygon {index + 1}/{len(polygons)} fixed!"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
yield fixed_polygon
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-08-24 14:43:28 +01:00
|
|
|
|
def polygons_and_simplified_polygons(feature):
|
2020-08-24 20:40:42 +01:00
|
|
|
|
if keep_old_polygons:
|
|
|
|
|
|
# cheat and shortcut out
|
|
|
|
|
|
return [], []
|
2020-08-24 14:43:28 +01:00
|
|
|
|
|
2021-08-03 18:46:30 +01:00
|
|
|
|
raw_polygons = simplify_geometry(feature)
|
|
|
|
|
|
clean_raw_polygons = [
|
|
|
|
|
|
[[x, y] for x, y in polygon.exterior.coords]
|
|
|
|
|
|
for polygon in clean_up_invalid_polygons(raw_polygons)
|
|
|
|
|
|
]
|
|
|
|
|
|
polygons = Polygons(clean_raw_polygons)
|
2021-06-24 12:16:16 +01:00
|
|
|
|
|
2020-08-24 14:43:28 +01:00
|
|
|
|
full_resolution = polygons.remove_too_small
|
|
|
|
|
|
smoothed = full_resolution.smooth
|
|
|
|
|
|
simplified = smoothed.simplify
|
|
|
|
|
|
|
2021-08-03 18:46:30 +01:00
|
|
|
|
if not (len(full_resolution) or len(simplified)):
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
'Polygon of 0 size found'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2020-08-24 14:43:28 +01:00
|
|
|
|
print( # noqa: T001
|
|
|
|
|
|
f' Original:{full_resolution.point_count: >5} points'
|
|
|
|
|
|
f' Smoothed:{smoothed.point_count: >5} points'
|
|
|
|
|
|
f' Simplified:{simplified.point_count: >4} points'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
point_counts.append(simplified.point_count)
|
|
|
|
|
|
|
2021-07-06 15:33:01 +01:00
|
|
|
|
if simplified.point_count >= MAX_NUMBER_OF_POINTS_PER_POLYGON:
|
2020-08-24 14:43:28 +01:00
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
'Too many points '
|
|
|
|
|
|
'(adjust Polygons.perimeter_to_simplification_ratio or '
|
|
|
|
|
|
'Polygons.perimeter_to_buffer_ratio)'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2021-06-24 18:26:21 +01:00
|
|
|
|
output = (
|
2020-08-25 16:55:09 +01:00
|
|
|
|
full_resolution.as_coordinate_pairs_long_lat,
|
|
|
|
|
|
simplified.as_coordinate_pairs_long_lat,
|
|
|
|
|
|
)
|
2020-08-24 14:43:28 +01:00
|
|
|
|
|
2021-06-24 18:26:21 +01:00
|
|
|
|
# Check that the simplification process hasn’t introduced bad data
|
|
|
|
|
|
for dataset in output:
|
|
|
|
|
|
for polygon in dataset:
|
|
|
|
|
|
assert Polygon(polygon).is_valid
|
|
|
|
|
|
|
|
|
|
|
|
return output
|
|
|
|
|
|
|
2020-08-24 14:43:28 +01:00
|
|
|
|
|
2020-09-09 13:29:45 +01:00
|
|
|
|
def estimate_number_of_smartphones_in_area(country_or_ward_code):
|
|
|
|
|
|
|
|
|
|
|
|
if country_or_ward_code in CITY_OF_LONDON.WARDS:
|
|
|
|
|
|
# We don’t have population figures for wards of the City of
|
|
|
|
|
|
# London. We’ll leave it empty here and estimate on the fly
|
|
|
|
|
|
# later based on physical area.
|
2021-03-08 15:36:23 +00:00
|
|
|
|
print(' Population: N/A') # noqa: T001
|
2020-09-09 13:29:45 +01:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# For some reason Bryher is the only ward missing population data, so we
|
|
|
|
|
|
# need to hard code it. For simplicity, let’s assume all 84 people who
|
|
|
|
|
|
# live on Bryher are 40 years old
|
|
|
|
|
|
if country_or_ward_code == BRYHER.WD20_CODE:
|
|
|
|
|
|
return BRYHER.POPULATION * SMARTPHONE_OWNERSHIP_BY_AGE_RANGE[MEDIAN_AGE_RANGE_UK]
|
|
|
|
|
|
|
|
|
|
|
|
if country_or_ward_code not in area_to_population_mapping:
|
|
|
|
|
|
raise ValueError(f'No population data for {country_or_ward_code}')
|
|
|
|
|
|
|
2020-09-16 11:33:57 +01:00
|
|
|
|
return estimate_number_of_smartphones_for_population(
|
|
|
|
|
|
area_to_population_mapping[country_or_ward_code]
|
2020-09-09 13:29:45 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-02-19 10:43:24 +00:00
|
|
|
|
test_filepath = source_files_path / "Test.geojson"
|
2021-04-28 13:50:27 +01:00
|
|
|
|
demo_filepath = source_files_path / "Demo.geojson"
|
2020-08-24 19:34:16 +01:00
|
|
|
|
ctry19_filepath = source_files_path / "Countries.geojson"
|
2020-07-24 11:46:21 +01:00
|
|
|
|
|
|
|
|
|
|
# https://geoportal.statistics.gov.uk/datasets/wards-may-2020-boundaries-uk-bgc
|
|
|
|
|
|
# Converted to geojson manually from SHP because of GeoJSON download limits
|
2020-08-24 18:50:17 +01:00
|
|
|
|
wd20_filepath = source_files_path / "Electoral Wards May 2020.geojson"
|
2020-07-24 11:46:21 +01:00
|
|
|
|
|
2020-08-24 18:50:17 +01:00
|
|
|
|
# http://geoportal.statistics.gov.uk/datasets/local-authority-districts-may-2020-boundaries-uk-bgc
|
|
|
|
|
|
lad20_filepath = source_files_path / "Local Authorities May 2020.geojson"
|
2020-07-24 11:46:21 +01:00
|
|
|
|
|
2020-08-24 18:34:00 +01:00
|
|
|
|
# https://geoportal.statistics.gov.uk/datasets/counties-and-unitary-authorities-december-2019-boundaries-uk-bgc
|
2020-08-24 18:50:17 +01:00
|
|
|
|
ctyua19_filepath = source_files_path / "Counties_and_Unitary_Authorities__December_2019__Boundaries_UK_BGC.geojson"
|
|
|
|
|
|
|
2020-09-09 13:29:45 +01:00
|
|
|
|
|
2020-08-24 18:50:17 +01:00
|
|
|
|
# http://geoportal.statistics.gov.uk/datasets/ward-to-westminster-parliamentary-constituency-to-local-authority-district-december-2019-lookup-in-the-united-kingdom/data
|
|
|
|
|
|
wd_lad_map_filepath = source_files_path / "Electoral Wards and Local Authorities 2020.geojson"
|
2020-08-24 18:34:00 +01:00
|
|
|
|
|
|
|
|
|
|
# https://geoportal.statistics.gov.uk/datasets/lower-tier-local-authority-to-upper-tier-local-authority-december-2019-lookup-in-england-and-wales?where=LTLA19CD%20%3D%20%27E06000045%27
|
|
|
|
|
|
ltla_utla_map_filepath = source_files_path / "Lower_Tier_Local_Authority_to_Upper_Tier_Local_Authority__December_2019__Lookup_in_England_and_Wales.csv" # noqa: E501
|
|
|
|
|
|
|
2020-09-09 13:29:45 +01:00
|
|
|
|
# https://www.ons.gov.uk/peoplepopulationandcommunity/populationandmigration/populationestimates/datasets/wardlevelmidyearpopulationestimatesexperimental
|
|
|
|
|
|
population_filepath_england_wales = source_files_path / "Mid-2019_Persons_England_Wales.csv"
|
|
|
|
|
|
# https://www.nrscotland.gov.uk/statistics-and-data/statistics/statistics-by-theme/population/population-estimates/2011-based-special-area-population-estimates/electoral-ward-population-estimates
|
|
|
|
|
|
population_filepath_scotland = source_files_path / "Mid-2019_Persons_Scotland.csv"
|
|
|
|
|
|
population_filepath_northern_ireland = source_files_path / "Ward-2014_Northern_Ireland.csv"
|
|
|
|
|
|
population_filepath_uk = source_files_path / "MYE1-2019.csv"
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-07-24 11:46:21 +01:00
|
|
|
|
ward_code_to_la_mapping = {
|
|
|
|
|
|
f["properties"]["WD19CD"]: f["properties"]["LAD19NM"]
|
2020-08-24 18:50:17 +01:00
|
|
|
|
for f in geojson.loads(wd_lad_map_filepath.read_text())["features"]
|
2020-07-24 11:46:21 +01:00
|
|
|
|
}
|
2020-07-31 13:06:37 +01:00
|
|
|
|
ward_code_to_la_id_mapping = {
|
|
|
|
|
|
f["properties"]["WD19CD"]: f["properties"]["LAD19CD"]
|
2020-08-24 18:50:17 +01:00
|
|
|
|
for f in geojson.loads(wd_lad_map_filepath.read_text())["features"]
|
2020-07-31 13:06:37 +01:00
|
|
|
|
}
|
2020-07-24 11:46:21 +01:00
|
|
|
|
|
2020-08-24 18:34:00 +01:00
|
|
|
|
|
|
|
|
|
|
# the mapping dict is empty for lower tier local authorities that are also upper tier (unitary authorities, etc)
|
|
|
|
|
|
ltla_utla_mapping_csv = csv.DictReader(ltla_utla_map_filepath.open())
|
|
|
|
|
|
la_code_to_cty_id_mapping = {
|
|
|
|
|
|
row['LTLA19CD']: row['UTLA19CD'] for row in ltla_utla_mapping_csv if row['LTLA19CD'] != row['UTLA19CD']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-09-09 13:29:45 +01:00
|
|
|
|
area_to_population_mapping = {}
|
|
|
|
|
|
|
|
|
|
|
|
for population_filepath in (
|
|
|
|
|
|
population_filepath_uk,
|
|
|
|
|
|
population_filepath_england_wales,
|
|
|
|
|
|
population_filepath_northern_ireland,
|
|
|
|
|
|
population_filepath_scotland,
|
|
|
|
|
|
):
|
|
|
|
|
|
area_to_population_csv = csv.DictReader(population_filepath.open())
|
|
|
|
|
|
for row in area_to_population_csv:
|
|
|
|
|
|
area_to_population_mapping[row['ward']] = [
|
|
|
|
|
|
(
|
|
|
|
|
|
int(k) if k.isnumeric() else MEDIAN_AGE_UK,
|
|
|
|
|
|
int(float(v.replace(',', '') or '0'))
|
|
|
|
|
|
)
|
|
|
|
|
|
for k, v in row.items() if k != 'ward'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2020-08-24 18:34:00 +01:00
|
|
|
|
|
2021-02-19 10:43:24 +00:00
|
|
|
|
def add_test_areas():
|
|
|
|
|
|
dataset_id = 'test'
|
|
|
|
|
|
dataset_geojson = geojson.loads(test_filepath.read_text())
|
|
|
|
|
|
repo.insert_broadcast_area_library(
|
|
|
|
|
|
dataset_id,
|
|
|
|
|
|
name='Test areas',
|
|
|
|
|
|
name_singular='test area',
|
|
|
|
|
|
is_group=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
areas_to_add = []
|
|
|
|
|
|
for feature in dataset_geojson["features"]:
|
|
|
|
|
|
f_id = feature["properties"]['id']
|
|
|
|
|
|
f_name = feature["properties"]['name']
|
|
|
|
|
|
|
|
|
|
|
|
print() # noqa: T001
|
|
|
|
|
|
print(f_name) # noqa: T001
|
|
|
|
|
|
|
|
|
|
|
|
feature, _ = polygons_and_simplified_polygons(
|
|
|
|
|
|
feature["geometry"]
|
|
|
|
|
|
)
|
|
|
|
|
|
areas_to_add.append([
|
|
|
|
|
|
f'{dataset_id}-{f_id}', f_name,
|
|
|
|
|
|
dataset_id, None,
|
|
|
|
|
|
feature, feature,
|
|
|
|
|
|
0,
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
repo.insert_broadcast_areas(areas_to_add, keep_old_polygons)
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-04-28 13:50:27 +01:00
|
|
|
|
def add_demo_areas():
|
|
|
|
|
|
dataset_id = 'demo'
|
|
|
|
|
|
dataset_geojson = geojson.loads(demo_filepath.read_text())
|
|
|
|
|
|
repo.insert_broadcast_area_library(
|
|
|
|
|
|
dataset_id,
|
|
|
|
|
|
name='Demo areas',
|
|
|
|
|
|
name_singular='demo area',
|
|
|
|
|
|
is_group=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
areas_to_add = []
|
|
|
|
|
|
for feature in dataset_geojson["features"]:
|
|
|
|
|
|
f_id = feature["properties"]['id']
|
|
|
|
|
|
f_name = feature["properties"]['name']
|
|
|
|
|
|
f_count_of_phones = feature["properties"]['count_of_phones']
|
|
|
|
|
|
|
|
|
|
|
|
print() # noqa: T001
|
|
|
|
|
|
print(f_name) # noqa: T001
|
|
|
|
|
|
|
|
|
|
|
|
feature, _ = polygons_and_simplified_polygons(
|
|
|
|
|
|
feature["geometry"]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
print(' Phones: ', f_count_of_phones) # noqa: T001
|
|
|
|
|
|
|
|
|
|
|
|
areas_to_add.append([
|
|
|
|
|
|
f'{dataset_id}-{f_id}', f_name,
|
|
|
|
|
|
dataset_id, None,
|
|
|
|
|
|
feature, feature,
|
|
|
|
|
|
f_count_of_phones,
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
repo.insert_broadcast_areas(areas_to_add, keep_old_polygons)
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-09-04 17:26:37 +01:00
|
|
|
|
def add_countries():
|
2020-08-24 19:34:16 +01:00
|
|
|
|
dataset_id = 'ctry19'
|
|
|
|
|
|
dataset_geojson = geojson.loads(ctry19_filepath.read_text())
|
|
|
|
|
|
repo.insert_broadcast_area_library(
|
|
|
|
|
|
'ctry19',
|
|
|
|
|
|
name='Countries',
|
|
|
|
|
|
name_singular='country',
|
|
|
|
|
|
is_group=False,
|
2020-08-24 14:43:28 +01:00
|
|
|
|
)
|
2020-07-31 13:06:37 +01:00
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
areas_to_add = []
|
|
|
|
|
|
for feature in dataset_geojson["features"]:
|
2020-09-09 13:29:45 +01:00
|
|
|
|
f_id = feature["properties"]['ctry19cd']
|
2020-08-24 19:34:16 +01:00
|
|
|
|
f_name = feature["properties"]['ctry19nm']
|
|
|
|
|
|
|
|
|
|
|
|
print() # noqa: T001
|
|
|
|
|
|
print(f_name) # noqa: T001
|
|
|
|
|
|
|
|
|
|
|
|
feature, simple_feature = (
|
|
|
|
|
|
polygons_and_simplified_polygons(feature["geometry"])
|
|
|
|
|
|
)
|
|
|
|
|
|
areas_to_add.append([
|
2020-09-09 13:29:45 +01:00
|
|
|
|
f'ctry19-{f_id}', f_name,
|
2020-08-24 19:34:16 +01:00
|
|
|
|
dataset_id, None,
|
|
|
|
|
|
feature, simple_feature,
|
2020-09-09 13:29:45 +01:00
|
|
|
|
estimate_number_of_smartphones_in_area(f_id),
|
2020-08-24 19:34:16 +01:00
|
|
|
|
])
|
|
|
|
|
|
|
2020-08-24 20:40:42 +01:00
|
|
|
|
repo.insert_broadcast_areas(areas_to_add, keep_old_polygons)
|
2020-08-24 19:34:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
2020-09-04 17:26:37 +01:00
|
|
|
|
def add_wards_local_authorities_and_counties():
|
2020-08-24 19:34:16 +01:00
|
|
|
|
dataset_name = "Local authorities"
|
|
|
|
|
|
dataset_name_singular = "local authority"
|
|
|
|
|
|
dataset_id = "wd20-lad20-ctyua19"
|
|
|
|
|
|
repo.insert_broadcast_area_library(
|
2020-08-24 18:34:00 +01:00
|
|
|
|
dataset_id,
|
2020-08-24 19:34:16 +01:00
|
|
|
|
name=dataset_name,
|
|
|
|
|
|
name_singular=dataset_name_singular,
|
|
|
|
|
|
is_group=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
_add_electoral_wards(dataset_id)
|
|
|
|
|
|
_add_local_authorities(dataset_id)
|
2020-09-04 17:26:37 +01:00
|
|
|
|
_add_counties_and_unitary_authorities(dataset_id)
|
2020-08-24 19:34:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _add_electoral_wards(dataset_id):
|
|
|
|
|
|
areas_to_add = []
|
|
|
|
|
|
|
2021-04-13 12:43:28 +01:00
|
|
|
|
for feature in geojson.loads(wd20_filepath.read_text())["features"]:
|
2020-09-04 17:26:37 +01:00
|
|
|
|
ward_code = feature["properties"]["wd20cd"]
|
|
|
|
|
|
ward_name = feature["properties"]["wd20nm"]
|
2020-08-24 19:34:16 +01:00
|
|
|
|
ward_id = "wd20-" + ward_code
|
|
|
|
|
|
|
|
|
|
|
|
print() # noqa: T001
|
|
|
|
|
|
print(ward_name) # noqa: T001
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
la_id = "lad20-" + ward_code_to_la_id_mapping[ward_code]
|
2020-09-04 17:26:37 +01:00
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
feature, simple_feature = (
|
2020-09-04 17:26:37 +01:00
|
|
|
|
polygons_and_simplified_polygons(feature["geometry"])
|
2020-08-24 19:34:16 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2021-03-18 23:02:32 +00:00
|
|
|
|
if feature:
|
2021-04-13 12:43:28 +01:00
|
|
|
|
rtree_index.insert(ward_id, Rect(*Polygons(feature).bounds))
|
2021-03-18 23:02:32 +00:00
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
areas_to_add.append([
|
|
|
|
|
|
ward_id, ward_name,
|
|
|
|
|
|
dataset_id, la_id,
|
2020-09-09 13:29:45 +01:00
|
|
|
|
feature, simple_feature,
|
|
|
|
|
|
estimate_number_of_smartphones_in_area(ward_code),
|
2020-08-24 19:34:16 +01:00
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
print("Skipping", ward_code, ward_name) # noqa: T001
|
|
|
|
|
|
|
2021-04-13 12:43:28 +01:00
|
|
|
|
rtree_index_path.open('wb').write(pickle.dumps(rtree_index))
|
2020-08-24 20:40:42 +01:00
|
|
|
|
repo.insert_broadcast_areas(areas_to_add, keep_old_polygons)
|
2020-08-24 19:34:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _add_local_authorities(dataset_id):
|
|
|
|
|
|
areas_to_add = []
|
|
|
|
|
|
|
|
|
|
|
|
for feature in geojson.loads(lad20_filepath.read_text())["features"]:
|
2020-09-24 14:37:28 +01:00
|
|
|
|
la_id = feature["properties"]["LAD20CD"]
|
|
|
|
|
|
group_name = feature["properties"]["LAD20NM"]
|
2020-08-24 19:34:16 +01:00
|
|
|
|
|
2020-09-04 17:26:37 +01:00
|
|
|
|
print() # noqa: T001
|
|
|
|
|
|
print(group_name) # noqa: T001
|
|
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
group_id = "lad20-" + la_id
|
|
|
|
|
|
|
|
|
|
|
|
feature, simple_feature = (
|
|
|
|
|
|
polygons_and_simplified_polygons(feature["geometry"])
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ctyua_id = la_code_to_cty_id_mapping.get(la_id)
|
|
|
|
|
|
areas_to_add.append([
|
|
|
|
|
|
group_id,
|
|
|
|
|
|
group_name,
|
|
|
|
|
|
dataset_id,
|
|
|
|
|
|
'ctyua19-' + ctyua_id if ctyua_id else None,
|
|
|
|
|
|
feature,
|
2020-09-09 13:29:45 +01:00
|
|
|
|
simple_feature,
|
|
|
|
|
|
None,
|
2020-08-24 19:34:16 +01:00
|
|
|
|
])
|
2020-08-24 20:40:42 +01:00
|
|
|
|
repo.insert_broadcast_areas(areas_to_add, keep_old_polygons)
|
2020-08-24 18:34:00 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# counties and unitary authorities
|
2020-08-24 19:34:16 +01:00
|
|
|
|
def _add_counties_and_unitary_authorities(dataset_id):
|
|
|
|
|
|
areas_to_add = []
|
|
|
|
|
|
for feature in geojson.loads(ctyua19_filepath.read_text())['features']:
|
|
|
|
|
|
ctyua_id = feature["properties"]["ctyua19cd"]
|
|
|
|
|
|
group_name = feature["properties"]["ctyua19nm"]
|
2020-08-24 18:34:00 +01:00
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
la_id = 'lad20-' + ctyua_id
|
|
|
|
|
|
if repo.get_areas([la_id]):
|
|
|
|
|
|
continue
|
2020-08-24 18:34:00 +01:00
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
group_id = "ctyua19-" + ctyua_id
|
2020-08-25 18:09:21 +01:00
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
feature, simple_feature = (
|
|
|
|
|
|
polygons_and_simplified_polygons(feature["geometry"])
|
|
|
|
|
|
)
|
2020-08-24 18:34:00 +01:00
|
|
|
|
|
2020-08-24 19:34:16 +01:00
|
|
|
|
areas_to_add.append([
|
|
|
|
|
|
group_id, group_name,
|
|
|
|
|
|
dataset_id, None,
|
2020-09-09 13:29:45 +01:00
|
|
|
|
feature, simple_feature,
|
|
|
|
|
|
None,
|
2020-08-24 19:34:16 +01:00
|
|
|
|
])
|
|
|
|
|
|
|
2020-08-24 20:40:42 +01:00
|
|
|
|
repo.insert_broadcast_areas(areas_to_add, keep_old_polygons)
|
2020-08-24 19:34:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
2020-08-24 20:40:42 +01:00
|
|
|
|
# cheeky global variable
|
|
|
|
|
|
keep_old_polygons = sys.argv[1:] == ['--keep-old-polygons']
|
|
|
|
|
|
print('keep_old_polygons: ', keep_old_polygons) # noqa: T001
|
|
|
|
|
|
|
|
|
|
|
|
repo = BroadcastAreasRepository()
|
|
|
|
|
|
|
|
|
|
|
|
if keep_old_polygons:
|
|
|
|
|
|
repo.delete_library_data()
|
|
|
|
|
|
else:
|
2020-08-24 19:34:16 +01:00
|
|
|
|
repo.delete_db()
|
|
|
|
|
|
repo.create_tables()
|
2021-02-19 10:43:24 +00:00
|
|
|
|
add_test_areas()
|
2021-04-28 13:50:27 +01:00
|
|
|
|
add_demo_areas()
|
2020-08-24 20:40:42 +01:00
|
|
|
|
add_countries()
|
|
|
|
|
|
add_wards_local_authorities_and_counties()
|
2020-08-24 19:34:16 +01:00
|
|
|
|
|
2020-08-24 20:40:42 +01:00
|
|
|
|
most_detailed_polygons = formatted_list(
|
|
|
|
|
|
sorted(point_counts, reverse=True)[:5],
|
|
|
|
|
|
before_each='',
|
|
|
|
|
|
after_each='',
|
|
|
|
|
|
)
|
2020-09-09 13:29:45 +01:00
|
|
|
|
|
2020-08-24 20:40:42 +01:00
|
|
|
|
print( # noqa: T001
|
|
|
|
|
|
'\n'
|
|
|
|
|
|
'DONE\n'
|
|
|
|
|
|
f' Processed {len(point_counts):,} polygons.\n'
|
2021-06-24 12:16:16 +01:00
|
|
|
|
f' Cleaned up {len(invalid_polygons):,} polygons.\n'
|
2020-08-24 20:40:42 +01:00
|
|
|
|
f' Highest point counts once simplifed: {most_detailed_polygons}\n'
|
|
|
|
|
|
)
|