# # 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