Coverage for mapology/hull.py: 19.35%
61 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:27:38 +00:00
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:27:38 +00:00
1"""Determine hull curves from point sets."""
3import copy
4import functools
5from typing import no_type_check
7from mapology import log
9THE_HULLS = {
10 'type': 'FeatureCollection',
11 'name': 'Prefix Region Convex Hulls',
12 'crs': {
13 'type': 'name',
14 'properties': {
15 'name': 'urn:ogc:def:crs:OGC:1.3:CRS84',
16 },
17 },
18 'features': [],
19}
20HULL_TEMPLATE = {
21 'type': 'Feature',
22 'id': '',
23 'properties': {'name': ''},
24 'geometry': {'type': 'Polygon', 'coordinates': []},
25}
28@no_type_check
29def convex(coords):
30 """Executes scan to return points in counter-clockwise order that are on the convex hull (Graham)."""
31 turn_left, turn_right, turn_none = 1, -1, 0 # noqa
33 def compare(a, b):
34 return float(a > b) - float(a < b)
36 def turn(p, q, r):
37 return compare((q[0] - p[0]) * (r[1] - p[1]) - (r[0] - p[0]) * (q[1] - p[1]), 0)
39 def keep_left(hull, r):
40 while len(hull) > 1 and turn(hull[-2], hull[-1], r) != turn_left:
41 hull.pop()
42 if not len(hull) or hull[-1] != r:
43 hull.append(r)
44 return hull
46 points = sorted(coords)
47 lower_hull = functools.reduce(keep_left, points, [])
48 upper_hull = functools.reduce(keep_left, reversed(points), [])
49 return lower_hull.extend(upper_hull[i] for i in range(1, len(upper_hull) - 1)) or lower_hull
52@no_type_check
53def extract_prefix_hull(prefix, region_name, coords):
54 prefix_hull = copy.deepcopy(HULL_TEMPLATE)
55 prefix_hull['id'] = prefix
56 prefix_hull['properties']['name'] = region_name
58 hull_coords = [[lon, lat] for lat, lon in convex(coords)]
60 lon_min = min(lon for lon, _ in hull_coords)
61 lon_max = max(lon for lon, _ in hull_coords)
62 if lon_min < -120 and +120 < lon_max:
63 log.info('Problematic region found (convex hull crossing the outer sign change of longitudes): %s' % prefix)
64 lon_sign = tuple(+1 if lon > 0 else -1 for lon, _ in hull_coords)
65 lon_neg_count = sum(-datum for datum in lon_sign if datum < 0)
66 lon_pos_count = sum(datum for datum in lon_sign if datum > 0)
67 if lon_pos_count and lon_neg_count: # Does the polygon cross the longitude sign change?
68 log.info('- (%d positive / %d negative) longitudes: %s' % (lon_pos_count, lon_neg_count, prefix))
69 patch_me_up = lon_neg_count > lon_pos_count
70 add_me = +360 if patch_me_up else -360
71 if patch_me_up:
72 log.info('- patching upwards (adding 360 degrees to longitude where negative): %s' % prefix)
73 hull_coords = [[lon + add_me, lat] if lon < 0 else [lon, lat] for lon, lat in hull_coords]
74 else:
75 log.info('- patching downwards (adding 360 degrees to longitude where positive): %s' % prefix)
76 hull_coords = [[lon + add_me, lat] if lon > 0 else [lon, lat] for lon, lat in hull_coords]
78 if prefix == 'ET':
79 log.info('Patching a North-Eastern ear onto the hull: %s' % prefix)
81 @no_type_check
82 def et_earify(pair):
83 """Monkey patch the ET region to offer an ear to select outside of ED."""
84 lon_ne = 12.27 # 9333333333334,
85 lat_ne = 53.91 # 8166666666664
86 magic_lon = 13.648875
87 magic_lat = 54.551359
88 lon, lat = pair
89 return [magic_lon, magic_lat] if lon > lon_ne and lat > lat_ne else [lon, lat]
91 hull_coords = [et_earify(pair) for pair in hull_coords]
93 # problem regions A1, NZ, NF, PA, UH,
94 prefix_hull['geometry']['coordinates'].append(hull_coords)
95 return prefix_hull
98@no_type_check
99def update_hull_store(hull_store, prefix_hull):
100 """DRY."""
101 hull_store['features'].append(copy.deepcopy(prefix_hull))