-
Notifications
You must be signed in to change notification settings - Fork 3
/
plot_google_map.py
358 lines (306 loc) · 12.5 KB
/
plot_google_map.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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
import warnings
import math
import requests
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
from scipy.misc import imresize
def plot_google_map(ax=None, height=640, width=640,
scale=2, resize=1, mapType='roadmap',
alpha=1, showLabels=True, style='', language='en',
markers=None, refresh=True, autoAxis=False,
figureResizeUpdate=True, apiKey='', plotMode=True, **kwargs):
"""Plots a google map on the current axes using the Google Static Maps API
ax - Axis handle
autoAxis - Not working, don't set to True
markers - list of markers
"""
if ax is None:
ax = plt.gca()
if scale < 1 or scale > 2:
raise ValueError('Scale must be 1 or 2')
if height > 640:
height = 640
if width > 640:
width = 640
if markers is None:
markers = []
# Store paramters in axis (for auto refreshing)
ax.pgm_data = {
'ax': ax,
'height': height,
'width': width,
'scale': scale,
'resize': resize,
'mapType': mapType,
'alpha': alpha,
'showLabels': showLabels,
'style': style,
'language': language,
'markers': markers,
'refresh': refresh,
'autoAxis': autoAxis,
'figureResizeUpdate': figureResizeUpdate,
'apiKey': apiKey
}
ax.pgm_data.update(kwargs)
curAxis = list(ax.axis())
if np.max(np.abs(curAxis)) > 500:
return
print('curAxis previous: {}'.format(curAxis))
# Enforce Latitude constraints of EPSG:900913
curAxis[2] = max(curAxis[2], -85) # ymin
curAxis[3] = min(curAxis[3], 85) # ymax
# Enforce longitude constrains
curAxis[0] = max(curAxis[0], -180) # xmin
if curAxis[0] > 180:
curAxis[0] = 0
curAxis[1] = min(curAxis[1], 180) # xmax
if curAxis[1] < -180:
curAxis[1] = 0
if curAxis == [0.0, 1.0, 0.0, 1.0]: # probably an empty figure
# display world map
curAxis = [-200, 200, -85, 85]
ax.axis(curAxis, emit=False)
if autoAxis:
warnings.warn('Auto axis doesn\'t work correctly now')
# adjust current axis limit to avoid strectched maps
xExtent, yExtent = latLonToMeters(curAxis[2:], curAxis[0:2])
xExtent = np.asscalar(np.diff(xExtent)) # just the size of the span
yExtent = np.asscalar(np.diff(yExtent))
# get axes aspect ratio
posBox = ax.get_position()
aspect_ratio = posBox.height / posBox.width
if xExtent * aspect_ratio > yExtent:
centerX = np.mean(curAxis[:2])
centerY = np.mean(curAxis[2:])
spanX = (curAxis[1] - curAxis[0]) / 2
spanY = (curAxis[3] - curAxis[2]) / 2
# enlarge the Y extent
spanY = spanY * xExtent * aspect_ratio / yExtent # new span
if spanY > 85:
spanX = spanX * 85 / spanY
spanY = spanY * 85 / spanY
curAxis = [centerX - spanX, centerX + spanX,
centerY - spanY, centerY + spanY]
elif yExtent > xExtent * aspect_ratio:
centerX = np.mean(curAxis[:2])
centerY = np.mean(curAxis[2:])
spanX = (curAxis[1] - curAxis[0]) / 2
spanY = (curAxis[3] - curAxis[2]) / 2
# enlarge the X extent
spanX = spanX * yExtent / (xExtent * aspect_ratio) # new span
if spanX > 180:
spanY = spanY * 180 / spanX
spanX = spanX * 180 / spanX
curAxis = [centerX - spanX, centerX + spanX,
centerY - spanY, centerY + spanY]
# Enforce Latitude constraints of EPSG:900913
if curAxis[2] < -85:
curAxis[2] += (-85 - curAxis[2])
curAxis[3] += (-85 - curAxis[2])
if curAxis[3] > 85:
curAxis[2] += (85 - curAxis[3])
curAxis[3] += (85 - curAxis[3])
ax.axis(curAxis, emit=False) # update axis as quickly as possible, before downloading new image
print('curAxis: {}'.format(curAxis))
# Delete previous map from plot (if exists)
if plotMode: # only if in plotting mode
curChildren = ax.get_children()
for child in curChildren:
if hasattr(child, 'tag') and child.tag == 'gmap':
child.remove()
# TODO: copy callback functions
# Calculate zoom level for current axis limits
xExtent, yExtent = latLonToMeters(curAxis[2:], curAxis[:2])
minResX = np.asscalar(np.diff(xExtent)) / width
minResY = np.asscalar(np.diff(yExtent)) / height
print('minResX: {} minResY: {}'.format(minResX, minResY))
minRes = max([minResX, minResY])
tileSize = 256
initialResolution = 2 * np.pi * 6378137 / tileSize # 156543.03392804062 for tileSize 256 pixels
zoomlevel = np.floor(np.log2(initialResolution / minRes))
print('Zoom level is {}'.format(np.log2(initialResolution / minRes)))
# Enforce valid zoom levels
if zoomlevel < 0:
zoomlevel = 0
if zoomlevel > 19:
zoomlevel = 19
# Calculate center coordinate in WGS1984
lat = (curAxis[2] + curAxis[3]) / 2
lon = (curAxis[0] + curAxis[1]) / 2
# Construct query URL
preamble = 'http://maps.googleapis.com/maps/api/staticmap'
location = '?center={:.10f},{:.10f}'.format(lat, lon)
zoomStr = '&zoom={:.0f}'.format(zoomlevel)
sizeStr = '&scale={}&size={}x{}'.format(scale, width, height)
mapTypeStr = '&maptype={}'.format(mapType)
if apiKey:
keyStr = '&key={}'.format(apiKey)
else:
keyStr = ''
markersStr = '&markers=' + '%7C'.join(markers)
if language:
languageStr = '&language={}'.format(language)
else:
languageStr = ''
if mapType in ['satellite', 'hybrid']:
filename = 'tmp.jpg'
formatStr = '&format=jpg'
else:
filename = 'tmp.png'
formatStr = '&format=png'
sensorStr = '&sensor=false'
if not showLabels:
if style:
style += '|'
style += 'feature:all|element:labels|visibility:off'
if style:
styleStr = '&style={}'.format(style)
else:
styleStr = ''
url = preamble + location + zoomStr + sizeStr + mapTypeStr + formatStr + markersStr + languageStr + sensorStr + keyStr + styleStr
# Get the image
r = requests.get(url, stream=True)
if r.status_code != 200:
warnings.warn("""Unable to download map from Google servers.
Returned error was: {}
Possible reasons: no network connection, quota exceeded, or some other error.
Consider using an API key if quota problems persist.
To debug, try pasting the following URL in your browser, which may result in a more informative error:
{}""".format(r.reason, r.url))
r.raw.decode_content = True
print(r.url)
imag = mpimg.imread(r.raw)
height, width = imag.shape[:2]
# Resize if needed
if resize != 1:
print('resized')
imag = imresize(imag, float(resize), 'bilinear')
# Calculate a meshgrid of pixel coordinates in EPSG:900913
centerPixelY = np.round(height / 2)
centerPixelX = np.round(width / 2)
centerX, centerY = latLonToMeters(lat, lon) # center coordinates in EPSG:900913
curResolution = initialResolution / 2**zoomlevel / scale / resize # meters/pixel (EPSG:900913)
xVec = centerX + (np.arange(width) - centerPixelX) * curResolution # x vector
yVec = centerY + (np.arange(height)[::-1] - centerPixelY) * curResolution # y vector
xMesh, yMesh = np.meshgrid(xVec, yVec) # construct meshgrid
# convert meshgrid to WGS1984
lonMesh, latMesh = metersToLatLon(xMesh, yMesh)
# Next, project the data into a uniform WGS1984 grid
uniHeight = np.round(height * resize)
uniWidth = np.round(width * resize)
latVect = np.linspace(latMesh[0, 0], latMesh[-1, 0], uniHeight)
lonVect = np.linspace(lonMesh[0, 0], lonMesh[0, -1], uniWidth)
uniLonMesh, uniLatMesh = np.meshgrid(lonVect, latVect)
uniImag = np.zeros((uniHeight, uniWidth, 3))
uniImag = myTurboInterp2(lonMesh, latMesh, imag, uniLonMesh, uniLatMesh)
if plotMode: # plot map
# display image
ax.hold(True)
axImg = ax.imshow(uniImag, alpha=alpha, origin='lower',
extent=[lonMesh[0, 0], lonMesh[0, -1],
latMesh[0, 0], latMesh[-1, 0]])
axImg.tag = 'gmap'
# TODO: add a dummy image to allow pan/zoom out to x2 of the image extent
# move map to bottom (so it doesn't hide previously drawn annotations)
axImg.set_zorder(-1)
ax.axis(curAxis, emit=False) # restore original zoom
# if auto-refresh mode - override zoom callback to allow automatic
# refresh of map upon zoom actions.
fig = ax.figure
if refresh:
xlim_cid = ax.callbacks.connect('xlim_changed', update_google_map)
ylim_cid = ax.callbacks.connect('ylim_changed', update_google_map)
ax.pgm_data['xlim_cid'] = xlim_cid
ax.pgm_data['ylim_cid'] = ylim_cid
else:
if 'xlim_cid' in ax.pgm_data:
ax.callbacks.disconnect(ax.pgm_data['xlim_cid'])
del ax.pgm_data['xlim_cid']
if 'ylim_cid' in ax.pgm_data:
ax.callbacks.disconnect(ax.pgm_data['ylim_cid'])
del ax.pgm_data['ylim_cid']
# set callback for figure resize function, to update extents if figure
# is streched.
if figureResizeUpdate:
resize_cid = fig.canvas.mpl_connect('resize_event', update_google_map_fig)
ax.pgm_data['resize_cid'] = resize_cid
else:
if 'resize_cid' in ax.pgm_data:
fig.canvas.mpl_disconnect(ax.pgm_data['resize_cid'])
del ax.pgm_data['resize_cid']
return axImg
else: # don't plot, only return map
return (lonVect, latVect, uniImag)
# Coordinate transformation functions
def metersToLatLon(x, y):
"""Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum"""
x = np.array(x)
y = np.array(y)
originShift = 2 * np.pi * 6378137 / 2.0 # 20037508.342789244
lon = (x / originShift) * 180
lat = (y / originShift) * 180
lat = 180 / np.pi * (2 * np.arctan( np.exp( lat * np.pi / 180)) - np.pi / 2)
return (lon, lat)
def latLonToMeters(lat, lon):
"""Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913"""
lat = np.array(lat)
lon = np.array(lon)
originShift = 2 * np.pi * 6378137 / 2.0 # 20037508.342789244
x = lon * originShift / 180
y = np.log(np.tan((90 + lat) * np.pi / 360 )) / (np.pi / 180)
y = y * originShift / 180
return (x, y)
def myTurboInterp2(X, Y, Z, XI, YI):
"""An extremely fast nearest neighbour 2D interpolation, assuming both input
and output grids consist only of squares, meaning:
- uniform X for each column
- uniform Y for each row"""
XI = XI[0,:]
X = X[0,:]
YI = YI[:,0]
Y = Y[:,0]
xiPos = np.nan * np.ones(XI.shape)
xLen = X.size
yiPos = np.nan * np.ones(YI.shape)
yLen = Y.size
# find x conversion
xPos = 0
for idx in range(xiPos.size):
if XI[idx] >= X[0] and XI[idx] <= X[-1]:
while xPos < xLen - 1 and X[xPos + 1] < XI[idx]:
xPos += 1
diffs = np.abs(X[xPos:xPos + 2] - XI[idx])
if diffs[0] < diffs[1]:
xiPos[idx] = xPos
else:
xiPos[idx] = xPos + 1
# find y conversion
yPos = 0
for idx in range(xiPos.size):
if YI[idx] <= Y[0] and YI[idx] >= Y[-1]:
while yPos < yLen - 1 and Y[yPos + 1] > YI[idx]:
yPos += 1
diffs = np.abs(Y[yPos:yPos + 2] - YI[idx])
if diffs[0] < diffs[1]:
yiPos[idx] = yPos
else:
yiPos[idx] = yPos + 1
yiPos = yiPos.astype(int).reshape(yiPos.size, 1)
xiPos = xiPos.astype(int).reshape(1, xiPos.size)
return Z[yiPos, xiPos, :]
def update_google_map(ax):
"""callback function for auto-refresh"""
if hasattr(ax, 'pgm_data'):
plot_google_map(**ax.pgm_data)
def update_google_map_fig(event):
"""callback function for auto-refresh"""
for ax in event.canvas.figure.axes:
found = False
for child in ax.get_children():
if hasattr(child, 'tag') and child.tag == 'gmap':
found = True
break
if found:
plot_google_map(**ax.pgm_data)