-
Notifications
You must be signed in to change notification settings - Fork 5
/
lookup.py
109 lines (89 loc) · 4.26 KB
/
lookup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import datetime
import math
from typing import List, Optional, Tuple, Dict
import pyproj
from flask.json import jsonify
MAX_DISTANCE = 100_000 # maximum allowable error, in meters, for a projection to be considered
MAX_NUM_RESULTS = 10
# list of pyproj.Proj objects is a global for reuse by repeated invocations of cloud functions
global_projs: Optional[Dict[str, pyproj.Proj]] = {}
global_proj_wgs84 = pyproj.Proj('EPSG:4326')
global_geod_wgs84 = pyproj.Geod(ellps='WGS84')
def setup_projs(epsg_only: bool = True) -> Dict[str, pyproj.Proj]:
"""Initialize a list of pyproj.Proj objects, each of which handles one projection."""
# By default only consider EPSG projections, ignoring ESRI and IGNF
authorities = ['EPSG'] if epsg_only else pyproj.database.get_authorities()
# Setting the environment variable PYPROJ_GLOBAL_CONTEXT=ON will make instantiation *much* faster, see
# https://github.com/pyproj4/pyproj/issues/661#issuecomment-653277033 for details.
projs = {}
for authority in authorities:
for code in pyproj.database.get_codes(authority, pyproj.enums.PJType.PROJECTED_CRS, allow_deprecated=True):
try:
projs[f'{authority}:{code}'] = pyproj.Proj(f'{authority}:{code}')
except pyproj.exceptions.ProjError:
pass
return projs
def projection_search(
lng: float, lat: float, x: float, y: float, num_results: int = MAX_NUM_RESULTS
) -> List[Tuple[float, str]]:
"""Brute force search for a projection that places (`x`, `y`) as close as possible to (`lng`, `lat`). This is
typically used when the projection of (`x`, `y`) is unknown or was lost.
Arguments:
- lng, lat: longitude and latitude coordinates of the known point in WGS84
- x, y: coordinates of the point whose CRS should project it close to (`lng`, `lat`)
Returns:
- List of tuples with error distance (in meters) and `authority:code` lookup.
"""
# lazily initialize the global list of projs
global global_projs, global_proj_wgs84 # pylint: disable=global-statement
if not global_projs:
start = datetime.datetime.now()
global_projs = setup_projs()
print(f'Initialized {len(global_projs)} projs in {(datetime.datetime.now() - start).total_seconds()} secs')
results: List[Tuple[float, str]] = []
start = datetime.datetime.now()
for lookup, proj in global_projs.items():
try: # Unproject (x,y) to WGS84 blindly assuming its CRS is `proj`
x_wgs84: float
y_wgs84: float
x_wgs84, y_wgs84 = proj(x, y, inverse=True)
except RuntimeError:
continue
dist: float
_, _, dist = global_geod_wgs84.inv(lng, lat, x_wgs84, y_wgs84) # compute the distance to (lng, lat) in meters
if not math.isnan(dist) and dist < MAX_DISTANCE:
results.append((dist, lookup))
results.sort()
return results[:num_results]
def handler(request):
"""The flask request handler for performing these lookups via cloud function"""
if request.method == 'OPTIONS':
return (
'',
204,
{
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '3600',
},
)
try:
lng = float(request.args['lng'])
lat = float(request.args['lat'])
x = float(request.args['x'])
y = float(request.args['y'])
except: # pylint: disable=bare-except
response = jsonify(dict(error="Required floating point query parameters: 'lng', 'lat', 'x', 'y'"))
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET'
return response, 400
global global_projs # pylint: disable=global-statement
response_dicts = []
for distance, lookup in projection_search(lng, lat, x, y):
proj = global_projs[lookup]
response_dicts.append({'projection': lookup, 'distance': distance, 'name': proj.crs.name})
response = jsonify(dict(projections=response_dicts))
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET'
return response, 200