diff --git a/app/broadcast_areas/utils.py b/app/broadcast_areas/utils.py index 26da631cb..fdcc1f83a 100644 --- a/app/broadcast_areas/utils.py +++ b/app/broadcast_areas/utils.py @@ -1,9 +1,12 @@ +from collections import defaultdict + from app.broadcast_areas.models import CustomBroadcastArea def aggregate_areas(areas): areas = _convert_custom_areas_to_wards(areas) areas = _aggregate_wards_by_local_authority(areas) + areas = _aggregate_lower_tier_authorities(areas) return areas @@ -24,3 +27,39 @@ def _aggregate_wards_by_local_authority(areas): area.parent if area.id.startswith('wd20-') else area for area in areas } + + +def _aggregate_lower_tier_authorities(areas): + results = set() + clusters = _cluster_lower_tier_authorities(areas) + + for cluster in clusters: + # always keep lone area as itself + if len(cluster) == 1: + results |= set(cluster) + # aggregate multi-area cluster + elif len(cluster) > 3: + results |= {cluster[0].parent} + # aggregate many small clusters + elif len(clusters) > 1: + area = cluster[0] + results |= {area.parent or area} + # keep one small cluster in full + else: + results |= set(cluster) + + return results + + +def _cluster_lower_tier_authorities(areas): + result = defaultdict(lambda: []) + + for area in areas: + # group lower tier authorities by "county" + if area.id.startswith('lad20-') and area.parent: + result[area.parent] += [area] + # leave countries, unitary authorities as-is + else: + result[area] = [area] + + return result.values() diff --git a/tests/app/broadcast_areas/custom_polygons.py b/tests/app/broadcast_areas/custom_polygons.py index 03a0dcc47..e79360649 100644 --- a/tests/app/broadcast_areas/custom_polygons.py +++ b/tests/app/broadcast_areas/custom_polygons.py @@ -37,3 +37,19 @@ CHELTENHAM = [ [51.9328, -2.0221], [51.9324, -2.1265], ] + +CHELTENHAM_AND_GLOUCESTER = [ + [51.8820, -2.2920], + [51.8234, -2.2570], + [51.8883, -2.0262], + [51.9425, -2.0771], + [51.8820, -2.2920], +] + +SEVERN_ESTUARY = [ + [51.5719, -2.9388], + [51.4180, -2.7259], + [51.8493, -2.1958], + [51.8883, -2.3016], + [51.5719, -2.9388], +] diff --git a/tests/app/broadcast_areas/test_utils.py b/tests/app/broadcast_areas/test_utils.py index 74e940fcc..c95b75eff 100644 --- a/tests/app/broadcast_areas/test_utils.py +++ b/tests/app/broadcast_areas/test_utils.py @@ -7,12 +7,24 @@ from tests.app.broadcast_areas.custom_polygons import ( BRISTOL, BURFORD, CHELTENHAM, + CHELTENHAM_AND_GLOUCESTER, SANTA_A, + SEVERN_ESTUARY, SKYE, ) @pytest.mark.parametrize(('area_ids', 'expected_area_names'), [ + ( + [ + 'wd20-E05009336', # Whitechapel, Tower Hamlets (electoral ward) + 'wd20-E05009372', # Hackney Central, Hackney (electoral ward) + 'wd20-E05009374', # Hackney Wick, Hackney (electoral ward) + ], [ + 'Hackney', # in Greater London* (DB doesn't know this) + 'Tower Hamlets', # in Greater London* + ], + ), ( [ 'wd20-E05004294', # Hester’s Way, Cheltenham (electoral ward) @@ -31,6 +43,16 @@ from tests.app.broadcast_areas.custom_polygons import ( 'Hackney', # in Greater London* (DB doesn't know this) ], ), + ( + [ + 'wd20-E05004294', # Hester’s Way, Cheltenham (electoral ward) + 'wd20-E05010981', # Painswick & Upton, Stroud (electoral ward) + 'wd20-E05009372', # Hackney Central, Hackney (electoral ward) + ], [ + 'Gloucestershire', # upper tier authority + 'Hackney', # in Greater London* (DB doesn't know this) + ], + ), ( [ 'lad20-E07000037', # High Peak (lower tier authority) @@ -49,6 +71,17 @@ from tests.app.broadcast_areas.custom_polygons import ( 'High Peak', # in Derbyshire ] ), + ( + [ + 'lad20-E07000037', # High Peak (lower tier authority) + 'lad20-E07000035', # Derbyshire Dales (lower tier authority) + 'ctyua19-E10000028', # Staffordshire (upper tier authority) + ], + [ + 'Derbyshire', # upper tier authority + 'Staffordshire', + ] + ), ( [ 'ctry19-E92000001', # England @@ -84,6 +117,19 @@ def test_aggregate_areas( 'Bristol, City of', ] ), + ( + [SEVERN_ESTUARY], [ + # Area covers various lower-tier authorities + # with more than three in Gloucestershire so + # we aggregate them into that + 'Bristol, City of', + 'Gloucestershire', + 'Monmouthshire', + 'Newport', + 'North Somerset', + 'South Gloucestershire', + ] + ), ( [CHELTENHAM], [ # Area covers three lower-tier authorities @@ -93,14 +139,30 @@ def test_aggregate_areas( 'Tewkesbury', ] ), + ( + [CHELTENHAM_AND_GLOUCESTER], [ + # Area covers more than 3 lower-tier authorities + # in Gloucestershire so we aggregate them + 'Gloucestershire', + ] + ), ( [BURFORD], [ - # Area covers two lower-tier authorities - # in Gloucestershire (upper tier authority) + # Area covers one lower-tier authority in + # Gloucestershire and one in Oxfordshire (both + # upper tier authorities) 'Cotswold', 'West Oxfordshire', ], ), + ( + [CHELTENHAM_AND_GLOUCESTER, BURFORD], [ + # Area covers many lower-tier authorities + # in Gloucestershire but only one in Oxfordshire + 'Gloucestershire', + 'West Oxfordshire', + ], + ), ( [SANTA_A], [ # Does not overlap with the UK