387 lines
10 KiB
Python
387 lines
10 KiB
Python
|
#
|
||
|
# Copyright 2014 Google Inc. All rights reserved.
|
||
|
#
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||
|
# use this file except in compliance with the License. You may obtain a copy of
|
||
|
# the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||
|
# License for the specific language governing permissions and limitations under
|
||
|
# the License.
|
||
|
#
|
||
|
|
||
|
"""Converts Python types to string representations suitable for Maps API server.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
sydney = {
|
||
|
"lat" : -33.8674869,
|
||
|
"lng" : 151.2069902
|
||
|
}
|
||
|
|
||
|
convert.latlng(sydney)
|
||
|
# '-33.8674869,151.2069902'
|
||
|
"""
|
||
|
|
||
|
|
||
|
def format_float(arg):
|
||
|
"""Formats a float value to be as short as possible.
|
||
|
|
||
|
Truncates float to 8 decimal places and trims extraneous
|
||
|
trailing zeros and period to give API args the best
|
||
|
possible chance of fitting within 2000 char URL length
|
||
|
restrictions.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
format_float(40) -> "40"
|
||
|
format_float(40.0) -> "40"
|
||
|
format_float(40.1) -> "40.1"
|
||
|
format_float(40.001) -> "40.001"
|
||
|
format_float(40.0010) -> "40.001"
|
||
|
format_float(40.000000001) -> "40"
|
||
|
format_float(40.000000009) -> "40.00000001"
|
||
|
|
||
|
:param arg: The lat or lng float.
|
||
|
:type arg: float
|
||
|
|
||
|
:rtype: string
|
||
|
"""
|
||
|
return ("%.8f" % float(arg)).rstrip("0").rstrip(".")
|
||
|
|
||
|
|
||
|
def latlng(arg):
|
||
|
"""Converts a lat/lon pair to a comma-separated string.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
sydney = {
|
||
|
"lat" : -33.8674869,
|
||
|
"lng" : 151.2069902
|
||
|
}
|
||
|
|
||
|
convert.latlng(sydney)
|
||
|
# '-33.8674869,151.2069902'
|
||
|
|
||
|
For convenience, also accepts lat/lon pair as a string, in
|
||
|
which case it's returned unchanged.
|
||
|
|
||
|
:param arg: The lat/lon pair.
|
||
|
:type arg: string or dict or list or tuple
|
||
|
"""
|
||
|
if is_string(arg):
|
||
|
return arg
|
||
|
|
||
|
normalized = normalize_lat_lng(arg)
|
||
|
return "%s,%s" % (format_float(normalized[0]), format_float(normalized[1]))
|
||
|
|
||
|
|
||
|
def normalize_lat_lng(arg):
|
||
|
"""Take the various lat/lng representations and return a tuple.
|
||
|
|
||
|
Accepts various representations:
|
||
|
1) dict with two entries - "lat" and "lng"
|
||
|
2) list or tuple - e.g. (-33, 151) or [-33, 151]
|
||
|
|
||
|
:param arg: The lat/lng pair.
|
||
|
:type arg: dict or list or tuple
|
||
|
|
||
|
:rtype: tuple (lat, lng)
|
||
|
"""
|
||
|
if isinstance(arg, dict):
|
||
|
if "lat" in arg and "lng" in arg:
|
||
|
return arg["lat"], arg["lng"]
|
||
|
if "latitude" in arg and "longitude" in arg:
|
||
|
return arg["latitude"], arg["longitude"]
|
||
|
|
||
|
# List or tuple.
|
||
|
if _is_list(arg):
|
||
|
return arg[0], arg[1]
|
||
|
|
||
|
raise TypeError(
|
||
|
"Expected a lat/lng dict or tuple, "
|
||
|
"but got %s" % type(arg).__name__)
|
||
|
|
||
|
|
||
|
def location_list(arg):
|
||
|
"""Joins a list of locations into a pipe separated string, handling
|
||
|
the various formats supported for lat/lng values.
|
||
|
|
||
|
For example:
|
||
|
p = [{"lat" : -33.867486, "lng" : 151.206990}, "Sydney"]
|
||
|
convert.waypoint(p)
|
||
|
# '-33.867486,151.206990|Sydney'
|
||
|
|
||
|
:param arg: The lat/lng list.
|
||
|
:type arg: list
|
||
|
|
||
|
:rtype: string
|
||
|
"""
|
||
|
if isinstance(arg, tuple):
|
||
|
# Handle the single-tuple lat/lng case.
|
||
|
return latlng(arg)
|
||
|
else:
|
||
|
return "|".join([latlng(location) for location in as_list(arg)])
|
||
|
|
||
|
|
||
|
def join_list(sep, arg):
|
||
|
"""If arg is list-like, then joins it with sep.
|
||
|
|
||
|
:param sep: Separator string.
|
||
|
:type sep: string
|
||
|
|
||
|
:param arg: Value to coerce into a list.
|
||
|
:type arg: string or list of strings
|
||
|
|
||
|
:rtype: string
|
||
|
"""
|
||
|
return sep.join(as_list(arg))
|
||
|
|
||
|
|
||
|
def as_list(arg):
|
||
|
"""Coerces arg into a list. If arg is already list-like, returns arg.
|
||
|
Otherwise, returns a one-element list containing arg.
|
||
|
|
||
|
:rtype: list
|
||
|
"""
|
||
|
if _is_list(arg):
|
||
|
return arg
|
||
|
return [arg]
|
||
|
|
||
|
|
||
|
def _is_list(arg):
|
||
|
"""Checks if arg is list-like. This excludes strings and dicts."""
|
||
|
if isinstance(arg, dict):
|
||
|
return False
|
||
|
if isinstance(arg, str): # Python 3-only, as str has __iter__
|
||
|
return False
|
||
|
return _has_method(arg, "__getitem__") if not _has_method(arg, "strip") else _has_method(arg, "__iter__")
|
||
|
|
||
|
|
||
|
def is_string(val):
|
||
|
"""Determines whether the passed value is a string, safe for 2/3."""
|
||
|
try:
|
||
|
basestring
|
||
|
except NameError:
|
||
|
return isinstance(val, str)
|
||
|
return isinstance(val, basestring)
|
||
|
|
||
|
|
||
|
def time(arg):
|
||
|
"""Converts the value into a unix time (seconds since unix epoch).
|
||
|
|
||
|
For example:
|
||
|
convert.time(datetime.now())
|
||
|
# '1409810596'
|
||
|
|
||
|
:param arg: The time.
|
||
|
:type arg: datetime.datetime or int
|
||
|
"""
|
||
|
# handle datetime instances.
|
||
|
if _has_method(arg, "timestamp"):
|
||
|
arg = arg.timestamp()
|
||
|
|
||
|
if isinstance(arg, float):
|
||
|
arg = int(arg)
|
||
|
|
||
|
return str(arg)
|
||
|
|
||
|
|
||
|
def _has_method(arg, method):
|
||
|
"""Returns true if the given object has a method with the given name.
|
||
|
|
||
|
:param arg: the object
|
||
|
|
||
|
:param method: the method name
|
||
|
:type method: string
|
||
|
|
||
|
:rtype: bool
|
||
|
"""
|
||
|
return hasattr(arg, method) and callable(getattr(arg, method))
|
||
|
|
||
|
|
||
|
def components(arg):
|
||
|
"""Converts a dict of components to the format expected by the Google Maps
|
||
|
server.
|
||
|
|
||
|
For example:
|
||
|
c = {"country": "US", "postal_code": "94043"}
|
||
|
convert.components(c)
|
||
|
# 'country:US|postal_code:94043'
|
||
|
|
||
|
:param arg: The component filter.
|
||
|
:type arg: dict
|
||
|
|
||
|
:rtype: basestring
|
||
|
"""
|
||
|
|
||
|
# Components may have multiple values per type, here we
|
||
|
# expand them into individual key/value items, eg:
|
||
|
# {"country": ["US", "AU"], "foo": 1} -> "country:AU", "country:US", "foo:1"
|
||
|
def expand(arg):
|
||
|
for k, v in arg.items():
|
||
|
for item in as_list(v):
|
||
|
yield "%s:%s" % (k, item)
|
||
|
|
||
|
if isinstance(arg, dict):
|
||
|
return "|".join(sorted(expand(arg)))
|
||
|
|
||
|
raise TypeError(
|
||
|
"Expected a dict for components, "
|
||
|
"but got %s" % type(arg).__name__)
|
||
|
|
||
|
|
||
|
def bounds(arg):
|
||
|
"""Converts a lat/lon bounds to a comma- and pipe-separated string.
|
||
|
|
||
|
Accepts two representations:
|
||
|
1) string: pipe-separated pair of comma-separated lat/lon pairs.
|
||
|
2) dict with two entries - "southwest" and "northeast". See convert.latlng
|
||
|
for information on how these can be represented.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
sydney_bounds = {
|
||
|
"northeast" : {
|
||
|
"lat" : -33.4245981,
|
||
|
"lng" : 151.3426361
|
||
|
},
|
||
|
"southwest" : {
|
||
|
"lat" : -34.1692489,
|
||
|
"lng" : 150.502229
|
||
|
}
|
||
|
}
|
||
|
|
||
|
convert.bounds(sydney_bounds)
|
||
|
# '-34.169249,150.502229|-33.424598,151.342636'
|
||
|
|
||
|
:param arg: The bounds.
|
||
|
:type arg: dict
|
||
|
"""
|
||
|
|
||
|
if is_string(arg) and arg.count("|") == 1 and arg.count(",") == 2:
|
||
|
return arg
|
||
|
elif isinstance(arg, dict):
|
||
|
if "southwest" in arg and "northeast" in arg:
|
||
|
return "%s|%s" % (latlng(arg["southwest"]),
|
||
|
latlng(arg["northeast"]))
|
||
|
|
||
|
raise TypeError(
|
||
|
"Expected a bounds (southwest/northeast) dict, "
|
||
|
"but got %s" % type(arg).__name__)
|
||
|
|
||
|
|
||
|
def size(arg):
|
||
|
if isinstance(arg, int):
|
||
|
return "%sx%s" % (arg, arg)
|
||
|
elif _is_list(arg):
|
||
|
return "%sx%s" % (arg[0], arg[1])
|
||
|
|
||
|
raise TypeError(
|
||
|
"Expected a size int or list, "
|
||
|
"but got %s" % type(arg).__name__)
|
||
|
|
||
|
|
||
|
def decode_polyline(polyline):
|
||
|
"""Decodes a Polyline string into a list of lat/lng dicts.
|
||
|
|
||
|
See the developer docs for a detailed description of this encoding:
|
||
|
https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
||
|
|
||
|
:param polyline: An encoded polyline
|
||
|
:type polyline: string
|
||
|
|
||
|
:rtype: list of dicts with lat/lng keys
|
||
|
"""
|
||
|
points = []
|
||
|
index = lat = lng = 0
|
||
|
|
||
|
while index < len(polyline):
|
||
|
result = 1
|
||
|
shift = 0
|
||
|
while True:
|
||
|
b = ord(polyline[index]) - 63 - 1
|
||
|
index += 1
|
||
|
result += b << shift
|
||
|
shift += 5
|
||
|
if b < 0x1f:
|
||
|
break
|
||
|
lat += (~result >> 1) if (result & 1) != 0 else (result >> 1)
|
||
|
|
||
|
result = 1
|
||
|
shift = 0
|
||
|
while True:
|
||
|
b = ord(polyline[index]) - 63 - 1
|
||
|
index += 1
|
||
|
result += b << shift
|
||
|
shift += 5
|
||
|
if b < 0x1f:
|
||
|
break
|
||
|
lng += ~(result >> 1) if (result & 1) != 0 else (result >> 1)
|
||
|
|
||
|
points.append({"lat": lat * 1e-5, "lng": lng * 1e-5})
|
||
|
|
||
|
return points
|
||
|
|
||
|
|
||
|
def encode_polyline(points):
|
||
|
"""Encodes a list of points into a polyline string.
|
||
|
|
||
|
See the developer docs for a detailed description of this encoding:
|
||
|
https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
||
|
|
||
|
:param points: a list of lat/lng pairs
|
||
|
:type points: list of dicts or tuples
|
||
|
|
||
|
:rtype: string
|
||
|
"""
|
||
|
last_lat = last_lng = 0
|
||
|
result = ""
|
||
|
|
||
|
for point in points:
|
||
|
ll = normalize_lat_lng(point)
|
||
|
lat = int(round(ll[0] * 1e5))
|
||
|
lng = int(round(ll[1] * 1e5))
|
||
|
d_lat = lat - last_lat
|
||
|
d_lng = lng - last_lng
|
||
|
|
||
|
for v in [d_lat, d_lng]:
|
||
|
v = ~(v << 1) if v < 0 else v << 1
|
||
|
while v >= 0x20:
|
||
|
result += (chr((0x20 | (v & 0x1f)) + 63))
|
||
|
v >>= 5
|
||
|
result += (chr(v + 63))
|
||
|
|
||
|
last_lat = lat
|
||
|
last_lng = lng
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
def shortest_path(locations):
|
||
|
"""Returns the shortest representation of the given locations.
|
||
|
|
||
|
The Elevations API limits requests to 2000 characters, and accepts
|
||
|
multiple locations either as pipe-delimited lat/lng values, or
|
||
|
an encoded polyline, so we determine which is shortest and use it.
|
||
|
|
||
|
:param locations: The lat/lng list.
|
||
|
:type locations: list
|
||
|
|
||
|
:rtype: string
|
||
|
"""
|
||
|
if isinstance(locations, tuple):
|
||
|
# Handle the single-tuple lat/lng case.
|
||
|
locations = [locations]
|
||
|
encoded = "enc:%s" % encode_polyline(locations)
|
||
|
unencoded = location_list(locations)
|
||
|
if len(encoded) < len(unencoded):
|
||
|
return encoded
|
||
|
else:
|
||
|
return unencoded
|