-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgoogle_static_maps_api.py
193 lines (156 loc) · 8.14 KB
/
google_static_maps_api.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
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from StringIO import StringIO
import io
import numpy as np
import pandas as pd
import requests
from PIL import Image
TILE_SIZE = 256 # Basic Mercator Google Maps tile is 256 x 256
MAX_SIN_LAT = 1. - 1e-5 # Bound for sinus of latitude
MAX_SIZE = 640 # Max size of the map in pixels
SCALE = 2 # 1 or 2 (free plan), see Google Static Maps API docs
MAPTYPE = 'roadmap' # Default map type
API_KEY = 'key' # Put your API key here, see https://console.developers.google.com
BASE_URL = 'https://maps.googleapis.com/maps/api/staticmap?'
cache = {} # Caching queries to limit API calls / speed them up
class GoogleStaticMapsAPI:
"""
API calls to the Google Static Maps API
Associated transformation between geographic coordinate system / pixel location
See https://developers.google.com/maps/documentation/static-maps/intro for more info.
"""
@classmethod
def map(
cls, center=None, zoom=None, size=(MAX_SIZE, MAX_SIZE), scale=SCALE,
maptype=MAPTYPE, file_format='png32', markers=None):
"""GET query on the Google Static Maps API to retrieve a static image.
:param object center: (required if markers not present) defines the center of the map, equidistant from edges.
This parameter takes a location as either
* a tuple of floats (latitude, longitude)
* or a string address (e.g. "city hall, new york, ny") identifying a unique location
:param int zoom: (required if markers not present) defines the zoom level of the map:
* 1: World
* 5: Landmass/continent
* 10: City
* 15: Streets
* 20: Buildings
:param (int, int) size: (required) defines the rectangular dimensions (pixels) of the map image.
Max size for each dimension is 640 (free account).
:param int scale: (optional), 1 or 2 (free plan). Affects the number of pixels that are returned.
scale=2 returns twice as many pixels as scale=1 while retaining the same coverage area and level of detail
(i.e. the contents of the map don't change).
:param string maptype: (optional) defines the type of map to construct. Several possible values, including
* roadmap (default): specifies a standard roadmap image, as is normally shown on the Google Maps.
* satellite: specifies a satellite image.
* terrain: specifies a physical relief map image, showing terrain and vegetation.
* hybrid: specifies a hybrid of the satellite and roadmap image, showing a transparent layer of
major streets and place names on the satellite image.
:param string file_format: image format
* png8 or png (default) specifies the 8-bit PNG format.
* png32 specifies the 32-bit PNG format.
* gif specifies the GIF format.
* jpg specifies the JPEG compression format.
* jpg-baseline
:param {string: object} markers: points to be marked on the map, under the form of a dict with keys
* 'color': (optional) 24-bit (0xFFFFCC) or predefined from
{black, brown, green, purple, yellow, blue, gray, orange, red, white}
* 'size': (optional) {tiny, mid, small}
* 'label': (optional) specifies a single uppercase alphanumeric character from the set {A-Z, 0-9}.
Only compatible with <mid> size markers
* 'coordinates': list of tuples (lat, long) for which the options are common.
:return: map image
:rtype: PIL.Image
"""
# For now, caching only if no markers are given
should_cache = markers is None
url = BASE_URL
if center:
url += 'center={},{}&'.format(*center) if isinstance(center, tuple) else 'center={}&'.format(center)
if zoom:
url += 'zoom={}&'.format(zoom)
markers = markers if markers else []
for marker in markers:
if 'latitude' in marker and 'longitude' in marker:
url += 'markers='
for key in ['color', 'size', 'label']:
if key in marker:
url += '{}:{}%7C'.format(key, marker[key])
url += '{},{}%7C'.format(marker['latitude'], marker['longitude'])
url += '&'
url += 'scale={}&'.format(scale)
url += 'size={}x{}&'.format(*tuple(min(el, MAX_SIZE) for el in size))
url += 'maptype={}&'.format(maptype)
url += 'format={}&'.format(file_format)
url += 'key={}'.format(API_KEY)
if url in cache:
return cache[url]
img = Image.open(StringIO((requests.get(url).content)))
if should_cache:
cache[url] = img
return img
@classmethod
def to_pixel(cls, latitude, longitude):
"""Transform a pair lat/long in pixel location on a world map without zoom (absolute location).
:param float latitude: latitude of point
:param float longitude: longitude of point
:return: pixel coordinates
:rtype: pandas.Series
"""
siny = np.clip(np.sin(latitude * np.pi / 180), -MAX_SIN_LAT, MAX_SIN_LAT)
return pd.Series(
[
TILE_SIZE * (0.5 + longitude / 360),
TILE_SIZE * (0.5 - np.log((1 + siny) / (1 - siny)) / (4 * np.pi)),
],
index=['x_pixel', 'y_pixel'],
)
@classmethod
def to_pixels(cls, latitudes, longitudes):
"""Transform a set of lat/long coordinates in pixel location on a world map without zoom (absolute location).
:param pandas.Series latitudes: set of latitudes
:param pandas.Series longitudes: set of longitudes
:return: pixel coordinates
:rtype: pandas.DataFrame
"""
siny = np.clip(np.sin(latitudes * np.pi / 180), -MAX_SIN_LAT, MAX_SIN_LAT)
return pd.concat(
[
TILE_SIZE * (0.5 + longitudes / 360),
TILE_SIZE * (0.5 - np.log((1 + siny) / (1 - siny)) / (4 * np.pi)),
],
axis=1, keys=['x_pixel', 'y_pixel'],
)
@classmethod
def to_tile_coordinates(cls, latitudes, longitudes, center_lat, center_long, zoom, size, scale):
"""Transform a set of lat/long coordinates into pixel position in a tile. These coordinates depend on
* the zoom level
* the tile location on the world map
:param pandas.Series latitudes: set of latitudes
:param pandas.Series longitudes: set of longitudes
:param float center_lat: center of the tile (latitude)
:param float center_long: center of the tile (longitude)
:param int zoom: Google maps zoom level
:param int size: size of the tile
:param int scale: 1 or 2 (free plan), see Google Static Maps API docs
:return: pixel coordinates in the tile
:rtype: pandas.DataFrame
"""
pixels = cls.to_pixels(latitudes, longitudes)
return scale * ((pixels - cls.to_pixel(center_lat, center_long)) * 2 ** zoom + size / 2)
@classmethod
def get_zoom(cls, latitudes, longitudes, size, scale):
"""Compute level of zoom needed to display all points in a single tile.
:param pandas.Series latitudes: set of latitudes
:param pandas.Series longitudes: set of longitudes
:param int size: size of the tile
:param int scale: 1 or 2 (free plan), see Google Static Maps API docs
:return: zoom level
:rtype: int
"""
# Extreme pixels
min_pixel = cls.to_pixel(latitudes.min(), longitudes.min())
max_pixel = cls.to_pixel(latitudes.max(), longitudes.max())
# Longitude spans from -180 to +180, latitudes only from -90 to +90
amplitudes = (max_pixel - min_pixel).abs() * pd.Series([2., 1.], index=['x_pixel', 'y_pixel'])
return int(np.log2(2 * size / amplitudes.max()))