diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cef6d70 --- /dev/null +++ b/__init__.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +""" +Geocoder +~~~~~~~~ + +Simple and consistent geocoding library written in Python. + +Many online providers such as Google & Bing have geocoding services, +these providers do not include Python libraries and have different +JSON responses between each other. + +Consistant JSON responses from various providers. + + >>> g = geocoder.google('New York City') + >>> g.latlng + [40.7127837, -74.0059413] + >>> g.state + 'New York' + >>> g.json + ... + +""" + +__title__ = 'geocoder' +__author__ = 'Denis Carriere' +__author_email__ = 'carriere.denis@gmail.com' +__version__ = '1.38.1' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2013-2016 Denis Carriere' + +# CORE +from geocoder.api import get, yahoo, bing, geonames, mapquest, google, mapbox # noqa +from geocoder.api import nokia, osm, tomtom, geolytica, arcgis, opencage, locationiq # noqa +from geocoder.api import maxmind, ipinfo, freegeoip, ottawa, here, baidu, gaode, w3w # noqa +from geocoder.api import yandex, mapzen, komoot, tamu, geocodefarm, tgos, uscensus # noqa +from geocoder.api import gisgraphy # noqa + +# EXTRAS +from geocoder.api import timezone, elevation, places, ip, canadapost, reverse, distance, location # noqa + +# CLI +from geocoder.cli import cli # noqa diff --git a/__init__.py:Zone.Identifier b/__init__.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/geocoder/__init__.py b/geocoder/__init__.py new file mode 100644 index 0000000..cef6d70 --- /dev/null +++ b/geocoder/__init__.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +""" +Geocoder +~~~~~~~~ + +Simple and consistent geocoding library written in Python. + +Many online providers such as Google & Bing have geocoding services, +these providers do not include Python libraries and have different +JSON responses between each other. + +Consistant JSON responses from various providers. + + >>> g = geocoder.google('New York City') + >>> g.latlng + [40.7127837, -74.0059413] + >>> g.state + 'New York' + >>> g.json + ... + +""" + +__title__ = 'geocoder' +__author__ = 'Denis Carriere' +__author_email__ = 'carriere.denis@gmail.com' +__version__ = '1.38.1' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2013-2016 Denis Carriere' + +# CORE +from geocoder.api import get, yahoo, bing, geonames, mapquest, google, mapbox # noqa +from geocoder.api import nokia, osm, tomtom, geolytica, arcgis, opencage, locationiq # noqa +from geocoder.api import maxmind, ipinfo, freegeoip, ottawa, here, baidu, gaode, w3w # noqa +from geocoder.api import yandex, mapzen, komoot, tamu, geocodefarm, tgos, uscensus # noqa +from geocoder.api import gisgraphy # noqa + +# EXTRAS +from geocoder.api import timezone, elevation, places, ip, canadapost, reverse, distance, location # noqa + +# CLI +from geocoder.cli import cli # noqa diff --git a/geocoder/__init__.py:Zone.Identifier b/geocoder/__init__.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/__init__.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/api.py b/geocoder/api.py new file mode 100644 index 0000000..546d4cd --- /dev/null +++ b/geocoder/api.py @@ -0,0 +1,661 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +from geocoder.distance import Distance +from geocoder.location import Location + +from geocoder.arcgis import ArcgisQuery +from geocoder.baidu import BaiduQuery +from geocoder.bing import BingQuery, BingQueryDetail +from geocoder.canadapost import CanadapostQuery +from geocoder.freegeoip import FreeGeoIPQuery +from geocoder.gaode import GaodeQuery +from geocoder.geocodefarm import GeocodeFarmQuery +from geocoder.geolytica import GeolyticaQuery +from geocoder.gisgraphy import GisgraphyQuery +from geocoder.here import HereQuery +from geocoder.ipinfo import IpinfoQuery +from geocoder.komoot import KomootQuery +from geocoder.locationiq import LocationIQQuery +from geocoder.mapbox import MapboxQuery +from geocoder.mapquest import MapquestQuery +from geocoder.mapzen import MapzenQuery +from geocoder.maxmind import MaxmindQuery +from geocoder.opencage import OpenCageQuery +from geocoder.osm import OsmQuery, OsmQueryDetail +from geocoder.ottawa import OttawaQuery +from geocoder.tamu import TamuQuery +from geocoder.tomtom import TomtomQuery +from geocoder.tgos import TgosQuery +from geocoder.uscensus import USCensusQuery +from geocoder.yahoo import YahooQuery +from geocoder.yandex import YandexQuery +from geocoder.w3w import W3WQuery + +from geocoder.arcgis_reverse import ArcgisReverse +from geocoder.baidu_reverse import BaiduReverse +from geocoder.bing_reverse import BingReverse +from geocoder.gaode_reverse import GaodeReverse +from geocoder.geocodefarm_reverse import GeocodeFarmReverse +from geocoder.gisgraphy_reverse import GisgraphyReverse +from geocoder.here_reverse import HereReverse +from geocoder.locationiq_reverse import LocationIQReverse +from geocoder.komoot_reverse import KomootReverse +from geocoder.mapbox_reverse import MapboxReverse +from geocoder.mapquest_reverse import MapquestReverse +from geocoder.mapzen_reverse import MapzenReverse +from geocoder.opencage_reverse import OpenCageReverse +from geocoder.osm_reverse import OsmReverse +from geocoder.uscensus_reverse import USCensusReverse +from geocoder.w3w_reverse import W3WReverse +from geocoder.yandex_reverse import YandexReverse + +from geocoder.mapquest_batch import MapquestBatch +from geocoder.bing_batch_forward import BingBatchForward +from geocoder.bing_batch_reverse import BingBatchReverse +from geocoder.uscensus_batch import USCensusBatch + +# Geonames Services +from geocoder.geonames import GeonamesQuery +from geocoder.geonames_details import GeonamesDetails +from geocoder.geonames_children import GeonamesChildren +from geocoder.geonames_hierarchy import GeonamesHierarchy + +# Google Services +from geocoder.google import GoogleQuery +from geocoder.google_timezone import TimezoneQuery +from geocoder.google_reverse import GoogleReverse +from geocoder.google_elevation import ElevationQuery +from geocoder.google_places import PlacesQuery + +options = { + 'osm': { + 'geocode': OsmQuery, + 'details': OsmQueryDetail, + 'reverse': OsmReverse, + }, + 'tgos': { + 'geocode': TgosQuery + }, + 'here': { + 'geocode': HereQuery, + 'reverse': HereReverse, + }, + 'baidu': { + 'geocode': BaiduQuery, + 'reverse': BaiduReverse + }, + 'gaode': { + 'geocode': GaodeQuery, + 'reverse': GaodeReverse + }, + 'yahoo': {'geocode': YahooQuery}, + 'tomtom': {'geocode': TomtomQuery}, + 'arcgis': { + 'geocode': ArcgisQuery, + 'reverse': ArcgisReverse + }, + 'ottawa': {'geocode': OttawaQuery}, + 'mapbox': { + 'geocode': MapboxQuery, + 'reverse': MapboxReverse, + }, + 'maxmind': {'geocode': MaxmindQuery}, + 'ipinfo': {'geocode': IpinfoQuery}, + 'geonames': { + 'geocode': GeonamesQuery, + 'details': GeonamesDetails, + 'timezone': GeonamesDetails, + 'children': GeonamesChildren, + 'hierarchy': GeonamesHierarchy + }, + 'freegeoip': {'geocode': FreeGeoIPQuery}, + 'w3w': { + 'geocode': W3WQuery, + 'reverse': W3WReverse, + }, + 'yandex': { + 'geocode': YandexQuery, + 'reverse': YandexReverse + }, + 'mapquest': { + 'geocode': MapquestQuery, + 'reverse': MapquestReverse, + 'batch': MapquestBatch + }, + 'geolytica': {'geocode': GeolyticaQuery}, + 'canadapost': {'geocode': CanadapostQuery}, + 'opencage': { + 'geocode': OpenCageQuery, + 'reverse': OpenCageReverse, + }, + 'bing': { + 'geocode': BingQuery, + 'details': BingQueryDetail, + 'reverse': BingReverse, + 'batch': BingBatchForward, + 'batch_reverse': BingBatchReverse + }, + 'google': { + 'geocode': GoogleQuery, + 'reverse': GoogleReverse, + 'timezone': TimezoneQuery, + 'elevation': ElevationQuery, + 'places': PlacesQuery, + }, + 'mapzen': { + 'geocode': MapzenQuery, + 'reverse': MapzenReverse, + }, + 'komoot': { + 'geocode': KomootQuery, + 'reverse': KomootReverse, + }, + 'tamu': { + 'geocode': TamuQuery + }, + 'geocodefarm': { + 'geocode': GeocodeFarmQuery, + 'reverse': GeocodeFarmReverse, + }, + 'uscensus': { + 'geocode': USCensusQuery, + 'reverse': USCensusReverse, + 'batch': USCensusBatch + }, + 'locationiq': { + 'geocode': LocationIQQuery, + 'reverse': LocationIQReverse, + }, + 'gisgraphy': { + 'geocode': GisgraphyQuery, + 'reverse': GisgraphyReverse, + }, +} + + +def get(location, **kwargs): + """Get Geocode + + :param ``location``: Your search location you want geocoded. + :param ``provider``: The geocoding engine you want to use. + + :param ``method``: Define the method (geocode, method). + """ + provider = kwargs.get('provider', 'bing').lower().strip() + method = kwargs.get('method', 'geocode').lower().strip() + if isinstance(location, (list, dict)) and method == 'geocode': + raise ValueError("Location should be a string") + + if provider not in options: + raise ValueError("Invalid provider") + + else: + if method not in options[provider]: + raise ValueError("Invalid method") + return options[provider][method](location, **kwargs) + + +def distance(*args, **kwargs): + """Distance tool measures the distance between two or multiple points. + + :param ``location``: (min 2x locations) Your search location you want geocoded. + :param ``units``: (default=kilometers) Unit of measurement. + > kilometers + > miles + > feet + > meters + """ + return Distance(*args, **kwargs) + + +def location(location, **kwargs): + """Parser for different location formats + """ + return Location(location, **kwargs) + + +def google(location, **kwargs): + """Google Provider + + :param ``location``: Your search location you want geocoded. + :param ``method``: (default=geocode) Use the following: + > geocode + > places + > reverse + > batch + > timezone + > elevation + """ + return get(location, provider='google', **kwargs) + + +def mapbox(location, **kwargs): + """Mapbox Provider + + :param ``location``: Your search location you want geocoded. + :param ``proximity``: Search nearby [lat, lng] + :param ``method``: (default=geocode) Use the following: + > geocode + > reverse + > batch + """ + return get(location, provider='mapbox', **kwargs) + + +def yandex(location, **kwargs): + """Yandex Provider + + :param ``location``: Your search location you want geocoded. + :param ``maxRows``: (default=1) Max number of results to fetch + :param ``lang``: Chose the following language: + > ru-RU — Russian (by default) + > uk-UA — Ukrainian + > be-BY — Belarusian + > en-US — American English + > en-BR — British English + > tr-TR — Turkish (only for maps of Turkey) + :param ``kind``: Type of toponym (only for reverse geocoding): + > house - house or building + > street - street + > metro - subway station + > district - city district + > locality - locality (city, town, village, etc.) + """ + return get(location, provider='yandex', **kwargs) + + +def w3w(location, **kwargs): + """what3words Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: W3W API key. + :param ``method``: Chose a method (geocode, method) + """ + return get(location, provider='w3w', **kwargs) + + +def baidu(location, **kwargs): + """Baidu Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: Baidu API key. + :param ``referer``: Baidu API referer website. + """ + return get(location, provider='baidu', **kwargs) + + +def gaode(location, **kwargs): + """Gaode Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: Gaode API key. + :param ``referer``: Gaode API referer website. + """ + return get(location, provider='gaode', **kwargs) + + +def komoot(location, **kwargs): + """Ottawa Provider + + :param ``location``: Your search location you want geocoded. + """ + return get(location, provider='komoot', **kwargs) + + +def ottawa(location, **kwargs): + """Ottawa Provider + + :param ``location``: Your search location you want geocoded. + :param ``maxRows``: (default=1) Max number of results to fetch + """ + return get(location, provider='ottawa', **kwargs) + + +def elevation(location, **kwargs): + """Elevation - Google Provider + + :param ``location``: Your search location you want to retrieve elevation data. + """ + return get(location, method='elevation', provider='google', **kwargs) + + +def places(location, **kwargs): + """Places - Google Provider + + :param ``location``: Your search location you want geocoded. + :param ``proximity``: Search within given area (bbox, bounds, or around latlng) + """ + return get(location, method='places', provider='google', **kwargs) + + +def timezone(location, **kwargs): + """Timezone - Google Provider + + :param ``location``: Your search location you want to retrieve timezone data. + :param ``timestamp``: Define your own specified time to calculate timezone. + """ + return get(location, method='timezone', provider='google', **kwargs) + + +def reverse(location, provider="google", **kwargs): + """Reverse Geocoding + + :param ``location``: Your search location you want to reverse geocode. + :param ``key``: (optional) use your own API Key from Bing. + :param ``provider``: (default=google) Use the following: + > google + > bing + """ + return get(location, method='reverse', provider=provider, **kwargs) + + +def bing(location, **kwargs): + """Bing Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: (optional) use your own API Key from Bing. + :param ``maxRows``: (default=1) Max number of results to fetch + :param ``method``: (default=geocode) Use the following: + > geocode + > reverse + """ + return get(location, provider='bing', **kwargs) + + +def yahoo(location, **kwargs): + """Yahoo Provider + + :param ``location``: Your search location you want geocoded. + """ + return get(location, provider='yahoo', **kwargs) + + +def geolytica(location, **kwargs): + """Geolytica (Geocoder.ca) Provider + + :param ``location``: Your search location you want geocoded. + """ + return get(location, provider='geolytica', **kwargs) + + +def opencage(location, **kwargs): + """Opencage Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: (optional) use your own API Key from OpenCage. + """ + return get(location, provider='opencage', **kwargs) + + +def arcgis(location, **kwargs): + """ArcGIS Provider + + :param ``location``: Your search location you want geocoded. + """ + return get(location, provider='arcgis', **kwargs) + + +def here(location, **kwargs): + """HERE Provider + + :param ``location``: Your search location you want geocoded. + :param ``app_code``: (optional) use your own Application Code from HERE. + :param ``app_id``: (optional) use your own Application ID from HERE. + :param ``maxRows``: (default=1) Max number of results to fetch + :param ``method``: (default=geocode) Use the following: + > geocode + > reverse + """ + return get(location, provider='here', **kwargs) + + +def nokia(location, **kwargs): + """HERE Provider + + :param ``location``: Your search location you want geocoded. + :param ``app_code``: (optional) use your own Application Code from HERE. + :param ``app_id``: (optional) use your own Application ID from HERE. + :param ``method``: (default=geocode) Use the following: + > geocode + > reverse + """ + return get(location, provider='here', **kwargs) + + +def tomtom(location, **kwargs): + """TomTom Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: (optional) use your own API Key from TomTom. + :param ``maxRows``: (default=1) Max number of results to fetch + """ + return get(location, provider='tomtom', **kwargs) + + +def mapquest(location, **kwargs): + """MapQuest Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: (optional) use your own API Key from MapQuest. + :param ``maxRows``: (default=1) Max number of results to fetch + :param ``method``: (default=geocode) Use the following: + > geocode + > reverse + """ + return get(location, provider='mapquest', **kwargs) + + +def osm(location, **kwargs): + """OSM Provider + + :param ``location``: Your search location you want geocoded. + :param ``url``: Custom OSM Server URL location + (ex: http://nominatim.openstreetmap.org/search) + """ + return get(location, provider='osm', **kwargs) + + +def maxmind(location='me', **kwargs): + """MaxMind Provider + + :param ``location``: Your search IP Address you want geocoded. + :param ``location``: (optional) if left blank will return your + current IP address's location. + """ + return get(location, provider='maxmind', **kwargs) + + +def ipinfo(location='', **kwargs): + """IP Info.io Provider + + :param ``location``: Your search IP Address you want geocoded. + :param ``location``: (optional) if left blank will return your + current IP address's location. + """ + return get(location, provider='ipinfo', **kwargs) + + +def freegeoip(location, **kwargs): + """FreeGeoIP Provider + + :param ``location``: Your search IP Address you want geocoded. + :param ``location``: (optional) if left blank will return your + current IP address's location. + """ + return get(location, provider='freegeoip', **kwargs) + + +def ip(location, **kwargs): + """IP Address lookup + + :param ``location``: Your search IP Address you want geocoded. + :param ``location``: (optional) if left blank will return your + current IP address's location. + """ + return get(location, provider='ipinfo', **kwargs) + + +def canadapost(location, **kwargs): + """CanadaPost Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: (optional) API Key from CanadaPost Address Complete. + :param ``language``: (default=en) Output language preference. + :param ``country``: (default=ca) Geofenced query by country. + :param ``maxRows``: (default=1) Max number of results to fetch + """ + return get(location, provider='canadapost', **kwargs) + + +def postal(location, **kwargs): + """CanadaPost Provider + + :param ``location``: Your search location you want geocoded. + :param ``key``: (optional) use your own API Key from + CanadaPost Address Complete. + """ + return get(location, provider='canadapost', **kwargs) + + +def geonames(location, **kwargs): + """GeoNames Provider + + :param ``location``: Your search location you want geocoded. + :param ``geonameid``: The place you want children / hierarchy for. + :param ``key``: (required) geonames *username*: needs to be passed with each request. + :param ``maxRows``: (default=1) Max number of results to fetch + :param ``proximity``: Search within given area (bbox, bounds, or around latlng) + :param ``method``: (default=geocode) Use the following: + > geocode + > details (mainly for administrive data and timezone) + > timezone (alias for details) + > children + > hierarchy + """ + return get(location, provider='geonames', **kwargs) + + +def mapzen(location, **kwargs): + """Mapzen Provider + + :param ``location``: Your search location you want geocoded. + :param ``maxRows``: (default=1) Max number of results to fetch + """ + return get(location, provider='mapzen', **kwargs) + + +def tamu(location, **kwargs): + """TAMU Provider + + Params + ------ + :param ``location``: The street address of the location you want geocoded. + :param ``city``: The city of the location to geocode. + :param ``state``: The state of the location to geocode. + :param ``zipcode``: The zipcode of the location to geocode. + :param ``key``: The API key (use API key "demo" for testing). + + API Reference + ------------- + https://geoservices.tamu.edu/Services/Geocode/WebService + """ + return get(location, provider='tamu', **kwargs) + + +def geocodefarm(location, **kwargs): + """GeocodeFarm Provider + + Params + ------ + :param ``location``: The string to search for. Usually a street address. + :param ``key``: (optional) API Key. Only Required for Paid Users. + :param ``lang``: (optional) 2 digit language code to return results in. Currently only "en"(English) or "de"(German) supported. + :param ``country``: (optional) The country to return results in. Used for biasing purposes and may not fully filter results to this specific country. + :param ``maxRows``: (default=1) Max number of results to fetch + + API Reference + ------------- + https://geocode.farm/geocoding/free-api-documentation/ + """ + return get(location, provider='geocodefarm', **kwargs) + + +def tgos(location, **kwargs): + """TGOS Provider + + :param ``location``: Your search location you want geocoded. + :param ``language``: (default=taiwan) Use the following: + > taiwan + > english + > chinese + :param ``method``: (default=geocode) Use the following: + > geocode + + API Reference + ------------- + http://api.tgos.nat.gov.tw/TGOS_MAP_API/Web/Default.aspx + """ + return get(location, provider='tgos', **kwargs) + + +def uscensus(location, **kwargs): + """US Census Provider + + Params + ------ + :param ``location``: Your search location(s) you want geocoded. + :param ``benchmark``: (default=4) Use the following: + > Public_AR_Current or 4 + > Public_AR_ACSYYYY or 8 + > Public_AR_Census2010 or 9 + :param ``vintage``: (default=4, not available with batch method) Use the following: + > Current_Current or 4 + > Census2010_Current or 410 + > ACS2013_Current or 413 + > ACS2014_Current or 414 + > ACS2015_Current or 415 + > Current_ACS2015 or 8 + > Census2010_ACS2015 or 810 + > ACS2013_ACS2015 or 813 + > ACS2014_ACS2015 or 814 + > ACS2015_ACS2015 or 815 + > Census2010_Census2010 or 910 + > Census2000_Census2010 or 900 + :param ``method``: (default=geocode) Use the following: + > geocode + > reverse + > batch + + API Reference + ------------- + https://geocoding.geo.census.gov/geocoder/Geocoding_Services_API.pdf + """ + return get(location, provider='uscensus', **kwargs) + + +def locationiq(location, **kwargs): + """LocationIQ Provider + + Params + ------ + :param ``location``: Your search location you want geocoded. + :param ``method``: (default=geocode) Use the following: + > geocode + > reverse + + API Reference + ------------- + https://locationiq.org/ + """ + return get(location, provider='locationiq', **kwargs) + + +def gisgraphy(location, **kwargs): + """Gisgraphy Provider + + :param ``location``: Your search location you want geocoded. + """ + return get(location, provider='gisgraphy', **kwargs) diff --git a/geocoder/api.py:Zone.Identifier b/geocoder/api.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/api.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/arcgis.py b/geocoder/arcgis.py new file mode 100644 index 0000000..6aac8fe --- /dev/null +++ b/geocoder/arcgis.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging +import json + +from geocoder.base import OneResult, MultipleResultsQuery + + +class ArcgisResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._feature = json_content.get('feature', {}) + + # proceed with super.__init__ + super(ArcgisResult, self).__init__(json_content) + + @property + def address(self): + return self.raw.get('name', '') + + @property + def lat(self): + return self._feature.get('geometry', {}).get('y') + + @property + def lng(self): + return self._feature.get('geometry', {}).get('x') + + @property + def score(self): + return self._feature.get('attributes', {}).get('Score', '') + + @property + def quality(self): + return self._feature.get('attributes', {}).get('Addr_Type', '') + + @property + def bbox(self): + _extent = self.raw.get('extent') + if _extent: + south = _extent.get('ymin') + west = _extent.get('xmin') + north = _extent.get('ymax') + east = _extent.get('xmax') + return self._get_bbox(south, west, north, east) + + +class ArcgisQuery(MultipleResultsQuery): + """ + ArcGIS REST API + ======================= + The World Geocoding Service finds addresses and places in all supported countries + from a single endpoint. The service can find point locations of addresses, + business names, and so on. The output points can be visualized on a map, + inserted as stops for a route, or loaded as input for a spatial analysis. + an address, retrieving imagery metadata, or creating a route. + + API Reference + ------------- + https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find.htm + """ + provider = 'arcgis' + method = 'geocode' + + _URL = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find' + _RESULT_CLASS = ArcgisResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + # backward compatitibility for 'limit' (now maxRows) + if 'limit' in kwargs: + logging.warning( + "argument 'limit' in OSM is deprecated and should be replaced with maxRows") + kwargs['maxRows'] = kwargs['limit'] + # build params + return { + 'f': 'json', + 'text': location, + 'maxLocations': kwargs.get('maxRows', 1), + } + + def _adapt_results(self, json_response): + return json_response['locations'] + + def _catch_errors(self, json_response): + status = json_response.get('error') + if status: + self.error = status.get('code') + self.message = status.get('message') + self.details = status.get('details') + + return self.error + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = ArcgisQuery('Toronto') + g.debug() + g = ArcgisQuery('Ottawa, Ontario', maxRows=5) + print(json.dumps(g.geojson, indent=4)) + print([result.address for result in g][:3]) diff --git a/geocoder/arcgis.py:Zone.Identifier b/geocoder/arcgis.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/arcgis.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/arcgis_reverse.py b/geocoder/arcgis_reverse.py new file mode 100644 index 0000000..39125d4 --- /dev/null +++ b/geocoder/arcgis_reverse.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult +from geocoder.arcgis import ArcgisQuery +from geocoder.location import Location + + +class ArcgisReverseResult(OneResult): + + @property + def ok(self): + return bool(self.address) + + @property + def lat(self): + return self.raw['location'].get('y') + + @property + def lng(self): + return self.raw['location'].get('x') + + @property + def address(self): + return self.raw['address'].get('Match_addr') + + @property + def city(self): + return self.raw['address'].get('City') + + @property + def neighborhood(self): + return self.raw['address'].get('Neighbourhood') + + @property + def region(self): + return self.raw['address'].get('Region') + + @property + def country(self): + return self.raw['address'].get('CountryCode') + + @property + def postal(self): + return self.raw['address'].get('Postal') + + @property + def state(self): + return self.raw['address'].get('Region') + + +class ArcgisReverse(ArcgisQuery): + """ + ArcGIS REST API + ======================= + The World Geocoding Service finds addresses and places in all supported countries + from a single endpoint. The service can find point locations of addresses, + business names, and so on. The output points can be visualized on a map, + inserted as stops for a route, or loaded as input for a spatial analysis. + an address, retrieving imagery metadata, or creating a route. + + API Reference + ------------- + https://developers.arcgis.com/rest/geocode/api-reference/geocoding-reverse-geocode.htm + """ + provider = 'arcgis' + method = 'reverse' + + _URL = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode' + _RESULT_CLASS = ArcgisReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'location': u'{}, {}'.format(location.lng, location.lat), + 'f': 'pjson', + 'distance': kwargs.get('distance', 50000), + 'outSR': kwargs.get('outSR', '') + } + + def _adapt_results(self, json_response): + return [json_response] + + def _catch_errors(self, json_response): + error = json_response.get('error', None) + if error: + self.error = error['message'] + + return self.error + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = ArcgisReverse("45.404702, -75.704150") + g.debug() diff --git a/geocoder/arcgis_reverse.py:Zone.Identifier b/geocoder/arcgis_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/arcgis_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/baidu.py b/geocoder/baidu.py new file mode 100644 index 0000000..d944851 --- /dev/null +++ b/geocoder/baidu.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +from collections import OrderedDict +import logging +import re +import six + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import baidu_key, baidu_security_key + + +class BaiduResult(OneResult): + + @property + def lat(self): + return self.raw.get('location', {}).get('lat') + + @property + def lng(self): + return self.raw.get('location', {}).get('lng') + + @property + def quality(self): + return self.raw.get('level') + + @property + def confidence(self): + return self.raw.get('confidence') + + +class BaiduQuery(MultipleResultsQuery): + """ + Baidu Geocoding API + =================== + Baidu Maps Geocoding API is a free open the API, the default quota + one million times / day. + + Params + ------ + :param location: Your search location you want geocoded. + :param key: Baidu API key. + :param referer: Baidu API referer website. + + References + ---------- + API Documentation: http://developer.baidu.com/map + Get Baidu Key: http://lbsyun.baidu.com/apiconsole/key + """ + provider = 'baidu' + method = 'geocode' + + _URL = 'http://api.map.baidu.com/geocoder/v2/' + _RESULT_CLASS = BaiduResult + _KEY = baidu_key + + def _build_params(self, location, provider_key, **kwargs): + coordtype = kwargs.get('coordtype', 'wgs84ll') + params = { + 'address': re.sub('[ ,]', '%', location), + 'output': 'json', + 'ret_coordtype': coordtype, + 'ak': provider_key, + } + + # adapt params to authentication method + self.security_key = kwargs.get('sk', baidu_security_key) + if self.security_key: + return self._encode_params(params) + else: + return params + + def _encode_params(self, params): + # maintain the order of the parameters during signature creation when returning the results + # signature is added to the end of the parameters + ordered_params = sorted([(k, v) + for (k, v) in params.items() if v]) + + params = OrderedDict(ordered_params) + + # urlencode with Chinese symbols sabotage the query + params['sn'] = self._sign_url( + '/geocoder/v2/', + params, + self.security_key + ) + + return params + + def _sign_url(self, base_url, params, security_key): + """ + Signs a request url with a security key. + """ + import hashlib + + if six.PY3: + from urllib.parse import urlencode, quote, quote_plus + else: + from urllib import urlencode, quote, quote_plus + + if not base_url or not self.security_key: + return None + + params = params.copy() + address = params.pop('address') + + url = base_url + '?address=' + address + '&' + urlencode(params) + encoded_url = quote(url, safe="/:=&?#+!$,;'@()*[]") + + signature = quote_plus(encoded_url + self.security_key).encode('utf-8') + encoded_signature = hashlib.md5(signature).hexdigest() + + return encoded_signature + + def _build_headers(self, provider_key, **kwargs): + return {'Referer': kwargs.get('referer', 'http://developer.baidu.com')} + + def _adapt_results(self, json_response): + return [json_response['result']] + + def _catch_errors(self, json_response): + status_code = json_response.get('status') + if status_code != 0: + self.status_code = status_code + self.error = json_response.get('message') + + return self.error + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = BaiduQuery('将台路', key='') + g.debug() diff --git a/geocoder/baidu.py:Zone.Identifier b/geocoder/baidu.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/baidu.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/baidu_reverse.py b/geocoder/baidu_reverse.py new file mode 100644 index 0000000..c3816d3 --- /dev/null +++ b/geocoder/baidu_reverse.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.location import Location +from geocoder.base import OneResult +from geocoder.baidu import BaiduQuery + + +class BaiduReverseResult(OneResult): + @property + def ok(self): + return bool(self.address) + + @property + def address(self): + return self.raw['formatted_address'] + + @property + def country(self): + return self.raw['addressComponent']['country'] + + @property + def province(self): + return self.raw['addressComponent']['province'] + + @property + def state(self): + return self.raw['addressComponent']['province'] + + @property + def city(self): + return self.raw['addressComponent']['city'] + + @property + def district(self): + return self.raw['addressComponent']['district'] + + @property + def street(self): + return self.raw['addressComponent']['street'] + + @property + def housenumber(self): + return self.raw['addressComponent']['street_number'] + + +class BaiduReverse(BaiduQuery): + """ + Baidu Geocoding API + =================== + Baidu Maps Geocoding API is a free open the API, the default quota + one million times / day. + + Params + ------ + :param location: Your search location you want geocoded. + :param key: Baidu API key. + :param referer: Baidu API referer website. + + References + ---------- + API Documentation: http://developer.baidu.com/map + Get Baidu Key: http://lbsyun.baidu.com/apiconsole/key + """ + provider = 'baidu' + method = 'reverse' + + _URL = 'http://api.map.baidu.com/geocoder/v2/' + _RESULT_CLASS = BaiduReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + params = { + 'location': str(location), + 'ret_coordtype': kwargs.get('coordtype', 'wgs84ll'), + 'output': 'json', + 'ak': provider_key + } + if ('lang_code' in kwargs): + params['accept-language'] = kwargs['lang_code'] + + return params + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = BaiduReverse("39.983424,116.32298", key='') + g.debug() diff --git a/geocoder/baidu_reverse.py:Zone.Identifier b/geocoder/baidu_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/baidu_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/base.py b/geocoder/base.py new file mode 100644 index 0000000..101c2ea --- /dev/null +++ b/geocoder/base.py @@ -0,0 +1,606 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import, print_function +from builtins import str + +import requests +import sys +import json +import six +import logging +from io import StringIO +from collections import OrderedDict + +is_python2 = sys.version_info < (3, 0) + +if is_python2: + # python 2.7 + from urlparse import urlparse + + class MutableSequence(object): + def index(self, v, **kwargs): return self._list.index(v, **kwargs) # noqa + def count(self, v): return self._list.count(v) # noqa + def pop(self, i=-1): return self._list.pop(i) # noqa + def remove(self, v): self._list.remove(v) # noqa + def __iter__(self): return iter(self._list) # noqa + def __contains__(self, v): return self._list.__contains__(v) # noqa + def __eq__(self, other): return self._list == other # noqa +else: + # python >3.3 + from collections.abc import MutableSequence + from urllib.parse import urlparse + +from geocoder.distance import Distance # noqa + +LOGGER = logging.getLogger(__name__) + + +class OneResult(object): + """ Container for one (JSON) object returned by the various web services""" + + _TO_EXCLUDE = ['parse', 'json', 'url', 'fieldnames', 'help', 'debug', + 'short_name', 'api', 'content', 'params', + 'street_number', 'api_key', 'key', 'id', 'x', 'y', + 'latlng', 'headers', 'timeout', 'wkt', 'locality', + 'province', 'rate_limited_get', 'osm', 'route', 'schema', + 'properties', 'geojson', 'tree', 'error', 'proxies', 'road', + 'xy', 'northeast', 'northwest', 'southeast', 'southwest', + 'road_long', 'city_long', 'state_long', 'country_long', + 'postal_town_long', 'province_long', 'road_long', + 'street_long', 'interpolated', 'method', 'geometry', 'session'] + + def __init__(self, json_content): + + self.raw = json_content + + # attributes required to compute bbox + self.northeast = [] + self.northwest = [] + self.southeast = [] + self.southwest = [] + + # attributes returned in JSON format + self.fieldnames = [] + self.json = {} + self._parse_json_with_fieldnames() + + # Essential attributes for Quality Control + @property # noqa + def lat(self): return '' # noqa + + @property # noqa + def lng(self): return '' # noqa + + @property # noqa + def accuracy(self): return '' # noqa + + @property # noqa + def quality(self): return '' # noqa + + # Bounding Box attributes + @property # noqa + def bbox(self): return {} # noqa + + # Essential attributes for Street Address + @property # noqa + def address(self): return '' # noqa + + @property # noqa + def housenumber(self): return '' # noqa + + @property # noqa + def street(self): return '' # noqa + + @property # noqa + def city(self): return '' # noqa + + @property # noqa + def state(self): return '' # noqa + + @property # noqa + def country(self): return '' # noqa + + @property # noqa + def postal(self): return '' # noqa + + def __repr__(self): + """ Display [address] if available; [lat,lng] otherwise""" + if self.address: + return u'[{0}]'.format(six.text_type(self.address)) + else: + return u'[{0},{1}]'.format(self.lat, self.lng) + + def _parse_json_with_fieldnames(self): + """ Parse the raw JSON with all attributes/methods defined in the class, except for the + ones defined starting with '_' or flagged in cls._TO_EXCLUDE. + + The final result is stored in self.json + """ + for key in dir(self): + if not key.startswith('_') and key not in self._TO_EXCLUDE: + self.fieldnames.append(key) + value = getattr(self, key) + if value: + self.json[key] = value + # Add OK attribute even if value is "False" + self.json['ok'] = self.ok + + @property + def ok(self): + return bool(self.lng and self.lat) + + @property + def status(self): + if self.ok: + return 'OK' + if not self.address: + return 'ERROR - No results found' + return 'ERROR - No Geometry' + + def debug(self, verbose=True): + with StringIO() as output: + print(u'\n', file=output) + print(u'From provider\n', file=output) + print(u'-----------\n', file=output) + print(str(json.dumps(self.raw, indent=4)), file=output) + print(u'\n', file=output) + print(u'Cleaned json\n', file=output) + print(u'-----------\n', file=output) + print(str(json.dumps(self.json, indent=4)), file=output) + print(u'\n', file=output) + print(u'OSM Quality\n', file=output) + print(u'-----------\n', file=output) + osm_count = 0 + for key in self.osm: + if 'addr:' in key: + if self.json.get(key.replace('addr:', '')): + print(u'- [x] {0}\n'.format(key), file=output) + osm_count += 1 + else: + print(u'- [ ] {0}\n'.format(key), file=output) + print(u'({0}/{1})\n'.format(osm_count, len(self.osm) - 2), file=output) + print(u'\n', file=output) + print(u'Fieldnames\n', file=output) + print(u'----------\n', file=output) + fields_count = 0 + for fieldname in self.fieldnames: + if self.json.get(fieldname): + print(u'- [x] {0}\n'.format(fieldname), file=output) + fields_count += 1 + else: + print(u'- [ ] {0}\n'.format(fieldname), file=output) + print(u'({0}/{1})\n'.format(fields_count, len(self.fieldnames)), file=output) + + # print in verbose mode + if verbose: + print(output.getvalue()) + + # return stats + return [osm_count, fields_count] + + def _get_bbox(self, south, west, north, east): + if all([south, east, north, west]): + # South Latitude, West Longitude, North Latitude, East Longitude + self.south = float(south) + self.west = float(west) + self.north = float(north) + self.east = float(east) + + # Bounding Box Corners + self.northeast = [self.north, self.east] + self.northwest = [self.north, self.west] + self.southwest = [self.south, self.west] + self.southeast = [self.south, self.east] + + # GeoJSON bbox + self.westsouth = [self.west, self.south] + self.eastnorth = [self.east, self.north] + + return dict(northeast=self.northeast, southwest=self.southwest) + return {} + + @property + def confidence(self): + if self.bbox: + # Units are measured in Kilometers + distance = Distance(self.northeast, self.southwest, units='km') + for score, maximum in [(10, 0.25), + (9, 0.5), + (8, 1), + (7, 5), + (6, 7.5), + (5, 10), + (4, 15), + (3, 20), + (2, 25)]: + if distance < maximum: + return score + if distance >= 25: + return 1 + # Cannot determine score + return 0 + + @property + def geometry(self): + if self.ok: + return { + 'type': 'Point', + 'coordinates': [self.x, self.y]} + return {} + + @property + def osm(self): + osm = dict() + if self.ok: + osm['x'] = self.x + osm['y'] = self.y + if self.housenumber: + osm['addr:housenumber'] = self.housenumber + if self.road: + osm['addr:street'] = self.road + if self.city: + osm['addr:city'] = self.city + if self.state: + osm['addr:state'] = self.state + if self.country: + osm['addr:country'] = self.country + if self.postal: + osm['addr:postal'] = self.postal + if hasattr(self, 'population'): + if self.population: + osm['population'] = self.population + return osm + + @property + def geojson(self): + feature = { + 'type': 'Feature', + 'properties': self.json, + } + if self.bbox: + feature['bbox'] = [self.west, self.south, self.east, self.north] + feature['properties']['bbox'] = feature['bbox'] + if self.geometry: + feature['geometry'] = self.geometry + return feature + + @property + def wkt(self): + if self.ok: + return 'POINT({x} {y})'.format(x=self.x, y=self.y) + return '' + + @property + def xy(self): + if self.ok: + return [self.lng, self.lat] + return [] + + @property + def latlng(self): + if self.ok: + return [self.lat, self.lng] + return [] + + @property + def y(self): + return self.lat + + @property + def x(self): + return self.lng + + @property + def locality(self): + return self.city + + @property + def province(self): + return self.state + + @property + def street_number(self): + return self.housenumber + + @property + def road(self): + return self.street + + @property + def route(self): + return self.street + + +class MultipleResultsQuery(MutableSequence): + """ Will replace the Base class to support multiple results, with the following differences : + + - split class into 2 parts : + - OneResult to actually store a (JSON) object from provider + - MultipleResultsQuery to manage the query + + - class variables moved into instance + - remaining class variables are names with convention: _CAPITALS + - self.url derived from class var cls.URL, which must be a valid URL + - self.timeout has default value from class var cls.TIMEOUT + """ + + _URL = None + _RESULT_CLASS = None + _KEY = None + _KEY_MANDATORY = True + _TIMEOUT = 5.0 + + @staticmethod + def _is_valid_url(url): + """ Helper function to validate that URLs are well formed, i.e that it contains a valid + protocol and a valid domain. It does not actually check if the URL exists + """ + try: + parsed = urlparse(url) + mandatory_parts = [parsed.scheme, parsed.netloc] + return all(mandatory_parts) + except: + return False + + @classmethod + def _is_valid_result_class(cls): + return issubclass(cls._RESULT_CLASS, OneResult) + + @classmethod + def _get_api_key(cls, key=None): + # Retrieves API Key from method argument first, then from Environment variables + key = key or cls._KEY + + # raise exception if not valid key found + if not key and cls._KEY_MANDATORY: + raise ValueError('Provide API Key') + + return key + + def __init__(self, location, **kwargs): + super(MultipleResultsQuery, self).__init__() + self._list = [] + + # check validity of _URL + if not self._is_valid_url(self._URL): + raise ValueError("Subclass must define a valid URL. Got %s", self._URL) + # override with kwargs IF given AND not empty string + self.url = kwargs.get('url', self._URL) or self._URL + # double check url, just in case it has been overwritten by kwargs + if not self._is_valid_url(self.url): + raise ValueError("url not valid. Got %s", self.url) + + # check validity of Result class + if not self._is_valid_result_class(): + raise ValueError( + "Subclass must define _RESULT_CLASS from 'OneResult'. Got %s", self._RESULT_CLASS) + self.one_result = self._RESULT_CLASS + + # check validity of provider key + provider_key = self._get_api_key(kwargs.pop('key', '')) + + # point to geocode, as a string or coordinates + self.location = location + + # set attributes to manage query + self.encoding = kwargs.get('encoding', 'utf-8') + self.timeout = kwargs.get('timeout', self._TIMEOUT) + self.proxies = kwargs.get('proxies', '') + self.session = kwargs.get('session', requests.Session()) + # headers can be overriden in _build_headers + self.headers = self._build_headers(provider_key, **kwargs).copy() + self.headers.update(kwargs.get('headers', {})) + # params can be overriden in _build_params + # it is an OrderedDict in order to preserve the order of the url query parameters + self.params = OrderedDict(self._build_params(location, provider_key, **kwargs)) + self.params.update(kwargs.get('params', {})) + + # results of query (set by _connect) + self.status_code = None + self.response = None + self.error = False + + # pointer to result where to delegates calls + self.current_result = None + + # hook for children class to finalize their setup before the query + self._before_initialize(location, **kwargs) + + # query and parse results + self._initialize() + + def __getitem__(self, key): + return self._list[key] + + def __setitem__(self, key, value): + self._list[key] = value + + def __delitem__(self, key): + del self._list[key] + + def __len__(self): + return len(self._list) + + def insert(self, index, value): + self._list.insert(index, value) + + def add(self, value): + self._list.append(value) + + def __repr__(self): + base_repr = u'<[{0}] {1} - {2} {{0}}>'.format( + self.status, + self.provider.title(), + self.method.title() + ) + if len(self) == 0: + return base_repr.format(u'[empty]') + elif len(self) == 1: + return base_repr.format(repr(self[0])) + else: + return base_repr.format(u'#%s results' % len(self)) + + def _build_headers(self, provider_key, **kwargs): + """Will be overridden according to the targetted web service""" + return {} + + def _build_params(self, location, provider_key, **kwargs): + """Will be overridden according to the targetted web service""" + return {} + + def _before_initialize(self, location, **kwargs): + """Can be overridden to finalize setup before the query""" + pass + + def _initialize(self): + # query URL and get valid JSON (also stored in self.json) + json_response = self._connect() + + # catch errors + has_error = self._catch_errors( + json_response) if json_response else True + + # creates instances for results + if not has_error: + self._parse_results(json_response) + + def _connect(self): + """ - Query self.url (validated cls._URL) + - Analyse reponse and set status, errors accordingly + - On success: + + returns the content of the response as a JSON object + This object will be passed to self._parse_json_response + """ + self.status_code = 'Unknown' + + try: + # make request and get response + self.response = response = self.rate_limited_get( + self.url, + params=self.params, + headers=self.headers, + timeout=self.timeout, + proxies=self.proxies + ) + + # check that response is ok + self.status_code = response.status_code + response.raise_for_status() + + # rely on json method to get non-empty well formatted JSON + json_response = response.json() + self.url = response.url + LOGGER.info("Requested %s", self.url) + + except requests.exceptions.RequestException as err: + # store real status code and error + self.error = u'ERROR - {}'.format(str(err)) + LOGGER.error("Status code %s from %s: %s", + self.status_code, self.url, self.error) + + # return False + return False + + # return response within its JSON format + return json_response + + def rate_limited_get(self, url, **kwargs): + """ By default, simply wraps a session.get request""" + return self.session.get(url, **kwargs) + + def _adapt_results(self, json_response): + """ Allow children classes to format json_response into an array of objects + OVERRIDE TO FETCH the correct array of objects when necessary + """ + return json_response + + def _parse_results(self, json_response): + """ Creates instances of self.one_result (validated cls._RESULT_CLASS) + from JSON results retrieved by self._connect + + params: array of objects (dictionnaries) + """ + for json_dict in self._adapt_results(json_response): + self.add(self.one_result(json_dict)) + + # set default result to use for delegation + self.current_result = len(self) > 0 and self[0] + + def _catch_errors(self, json_response): + """ Checks the JSON returned from the provider and flag errors if necessary""" + return self.error + + @property + def ok(self): + return len(self) > 0 + + @property + def status(self): + if self.ok: + return 'OK' + elif self.error: + return self.error + elif len(self) == 0: + return 'ERROR - No results found' + else: + return 'ERROR - Unhandled Exception' + + @property + def geojson(self): + geojson_results = [result.geojson for result in self] + features = { + 'type': 'FeatureCollection', + 'features': geojson_results + } + return features + + def debug(self, verbose=True): + with StringIO() as output: + print(u'===\n', file=output) + print(str(repr(self)), file=output) + print(u'===\n', file=output) + print(u'\n', file=output) + print(u'#res: {}\n'.format(len(self)), file=output) + print(u'code: {}\n'.format(self.status_code), file=output) + print(u'url: {}\n'.format(self.url), file=output) + + stats = [] + + if self.ok: + for index, result in enumerate(self): + print(u'\n', file=output) + print(u'Details for result #{}\n'.format(index + 1), file=output) + print(u'---\n', file=output) + stats.append(result.debug()) + else: + print(self.status, file=output) + + if verbose: + print(output.getvalue()) + + return stats + + # Delegation to current result + def set_default_result(self, index): + """ change the result used to delegate the calls to. The provided index should be in the + range of results, otherwise it will raise an exception + """ + self.current_result = self[index] + + def __getattr__(self, name): + """ Called when an attribute lookup has not found the attribute in the usual places (i.e. + it is not an instance attribute nor is it found in the class tree for self). name is + the attribute name. This method should return the (computed) attribute value or raise + an AttributeError exception. + + Note that if the attribute is found through the normal mechanism, __getattr__() is not called. + """ + if not self.ok: + return None + + if self.current_result is None: + raise AttributeError("%s not found on %s, and current_result is None".format( + name, self.__class__.__name__ + )) + return getattr(self.current_result, name) diff --git a/geocoder/base.py:Zone.Identifier b/geocoder/base.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/base.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/bing.py b/geocoder/bing.py new file mode 100644 index 0000000..6f7406d --- /dev/null +++ b/geocoder/bing.py @@ -0,0 +1,180 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import bing_key +import re + + +class BingResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._point = json_content.get('point', {}) + self._address = json_content.get('address', {}) + + # proceed with super.__init__ + super(BingResult, self).__init__(json_content) + + @property + def lat(self): + coord = self._point['coordinates'] + if coord: + return coord[0] + + @property + def lng(self): + coord = self._point['coordinates'] + if coord: + return coord[1] + + @property + def address(self): + return self._address.get('formattedAddress') + + @property + def housenumber(self): + if self.street: + expression = r'\d+' + pattern = re.compile(expression) + match = pattern.search(self.street, re.UNICODE) + if match: + return match.group(0) + + @property + def street(self): + return self._address.get('addressLine') + + @property + def neighborhood(self): + return self._address.get('neighborhood') + + @property + def city(self): + return self._address.get('locality') + + @property + def state(self): + return self._address.get('adminDistrict') + + @property + def country(self): + return self._address.get('countryRegion') + + @property + def quality(self): + return self.raw.get('entityType') + + @property + def accuracy(self): + return self.raw.get('calculationMethod') + + @property + def postal(self): + return self._address.get('postalCode') + + @property + def bbox(self): + _bbox = self.raw.get('bbox') + if _bbox: + south = _bbox[0] + north = _bbox[2] + west = _bbox[1] + east = _bbox[3] + return self._get_bbox(south, west, north, east) + + +class BingQuery(MultipleResultsQuery): + """ + Bing Maps REST Services + ======================= + The Bing™ Maps REST Services Application Programming Interface (API) + provides a Representational State Transfer (REST) interface to + perform tasks such as creating a static map with pushpins, geocoding + an address, retrieving imagery metadata, or creating a route. + + API Reference + ------------- + http://msdn.microsoft.com/en-us/library/ff701714.aspx + + Get Bing key + ------------ + https://www.bingmapsportal.com/ + """ + provider = 'bing' + method = 'geocode' + + _URL = 'http://dev.virtualearth.net/REST/v1/Locations' + _RESULT_CLASS = BingResult + _KEY = bing_key + + def _build_headers(self, provider_key, **kwargs): + return { + 'Referer': "http://addxy.com/", + 'User-agent': 'Mozilla/5.0' + } + + def _build_params(self, location, provider_key, **kwargs): + return { + 'q': location, + 'o': 'json', + 'inclnb': 1, + 'key': provider_key, + 'maxResults': kwargs.get('maxRows', 1) + } + + def _catch_errors(self, json_response): + status = json_response['statusDescription'] + if not status == 'OK': + self.error = status + + return self.error + + def _adapt_results(self, json_response): + # extract the array of JSON objects + sets = json_response['resourceSets'] + if sets: + return sets[0]['resources'] + return [] + + +class BingQueryDetail(MultipleResultsQuery): + provider = 'bing' + method = 'details' + + _URL = 'http://dev.virtualearth.net/REST/v1/Locations' + _RESULT_CLASS = BingResult + _KEY = bing_key + + def _build_params(self, location, provider_key, **kwargs): + return { + 'adminDistrict': kwargs.get('adminDistrict'), + 'countryRegion': kwargs.get('countryRegion'), + 'locality': kwargs.get('locality'), + 'postalCode': kwargs.get('countryRegion'), + 'addressLine': kwargs.get('addressLine', location), + 'o': 'json', + 'inclnb': 1, + 'key': provider_key, + 'maxResults': kwargs.get('maxRows', 1) + } + + def _catch_errors(self, json_response): + status = json_response['statusDescription'] + if not status == 'OK': + self.error = status + + return self.error + + def _adapt_results(self, json_response): + # extract the array of JSON objects + sets = json_response['resourceSets'] + if sets: + return sets[0]['resources'] + return [] + + +if __name__ == '__main__': + g = BingQuery('453 Booth Street, Ottawa Ontario') + g.debug() diff --git a/geocoder/bing.py:Zone.Identifier b/geocoder/bing.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/bing.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/bing_batch.py b/geocoder/bing_batch.py new file mode 100644 index 0000000..241007e --- /dev/null +++ b/geocoder/bing_batch.py @@ -0,0 +1,179 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import, print_function +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import bing_key + +import time +import io +import requests +import logging +import sys + +PY2 = sys.version_info < (3, 0) +csv_io = io.BytesIO if PY2 else io.StringIO + +LOGGER = logging.getLogger(__name__) + + +class BingBatchResult(OneResult): + + def __init__(self, content): + self._content = content + + @property + def lat(self): + coord = self._content + if coord: + return coord[0] + + @property + def lng(self): + coord = self._content + if coord: + return coord[1] + + def debug(self, verbose=True): + with csv_io() as output: + print('\n', file=output) + print('{} result\n'.format(self.__class__.__name__), file=output) + print('-----------\n', file=output) + print(self._content, file=output) + + if verbose: + print(output.getvalue()) + + return [None, None] + + +class BingBatch(MultipleResultsQuery): + """ + Bing Maps REST Services + ======================= + The Bing™ Maps REST Services Application Programming Interface (API) + provides a Representational State Transfer (REST) interface to + perform tasks such as creating a static map with pushpins, geocoding + an address, retrieving imagery metadata, or creating a route. + + API Reference + ------------- + http://msdn.microsoft.com/en-us/library/ff701714.aspx + + Dataflow Reference + ------------------ + https://msdn.microsoft.com/en-us/library/ff701733.aspx + + """ + provider = 'bing' + + _URL = u'http://spatial.virtualearth.net/REST/v1/Dataflows/Geocode' + _BATCH_TIMEOUT = 60 + _BATCH_WAIT = 5 + + _RESULT_CLASS = BingBatchResult + _KEY = bing_key + + def extract_resource_id(self, response): + for rs in response['resourceSets']: + for resource in rs['resources']: + if 'id' in resource: + return resource['id'] + + raise LookupError('No job ID returned from Bing batch call') + + def is_job_done(self, job_id): + url = u'http://spatial.virtualearth.net/REST/v1/Dataflows/Geocode/{}'.format(job_id) + response = self.session.get( + url, + params={'key': self.provider_key}, + timeout=self.timeout, + proxies=self.proxies + ) + + for rs in response.json()['resourceSets']: + for resource in rs['resources']: + if resource['id'] == job_id: + if resource['status'] == 'Aborted': + raise LookupError('Bing job aborted') + return resource['status'] == 'Completed' + + raise LookupError('Job ID not found in Bing answer - something is wrong') + + def get_job_result(self, job_id): + url = u'http://spatial.virtualearth.net/REST/v1/Dataflows/Geocode/{}/output/succeeded'.format(job_id) + response = self.session.get( + url, + params={'key': self.provider_key}, + timeout=self.timeout, + proxies=self.proxies + ) + + return response.content + + def _build_params(self, locations, provider_key, **kwargs): + self.batch = self.generate_batch(locations) + self.locations_length = len(locations) + self.provider_key = provider_key + self._BATCH_TIMEOUT = kwargs.get('timeout', 60) + + return { + 'input': 'csv', + 'key': provider_key + } + + def _build_headers(self, provider_key, **kwargs): + return {'Content-Type': 'text/plain'} + + def _connect(self): + self.status_code = 'Unknown' + + try: + self.response = response = self.session.post( + self.url, + data=self.batch, + params=self.params, + headers=self.headers, + timeout=self.timeout, + proxies=self.proxies + ) + + # check that response is ok + self.status_code = response.status_code + response.raise_for_status() + + # rely on json method to get non-empty well formatted JSON + json_response = response.json() + self.url = response.url + LOGGER.info("Requested %s", self.url) + + # get the resource/job id + resource_id = self.extract_resource_id(json_response) + elapsed = 0 + + # try for _BATCH_TIMEOUT seconds to retrieve the results of that job + while (elapsed < self._BATCH_TIMEOUT): + if self.is_job_done(resource_id): + return self.get_job_result(resource_id) + + elapsed = elapsed + self._BATCH_WAIT + time.sleep(self._BATCH_WAIT) + + LOGGER.error("Job was not finished in time.") + + except (requests.exceptions.RequestException, LookupError) as err: + # store real status code and error + self.error = u'ERROR - {}'.format(str(err)) + LOGGER.error("Status code %s from %s: %s", + self.status_code, self.url, self.error) + + return False + + def _parse_results(self, response): + rows = self._adapt_results(response) + + # re looping through the results to give them back in their original order + for idx in range(0, self.locations_length): + self.add(self.one_result(rows.get(str(idx), None))) + + self.current_result = len(self) > 0 and self[0] diff --git a/geocoder/bing_batch.py:Zone.Identifier b/geocoder/bing_batch.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/bing_batch.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/bing_batch_forward.py b/geocoder/bing_batch_forward.py new file mode 100644 index 0000000..2a84459 --- /dev/null +++ b/geocoder/bing_batch_forward.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import, print_function +from geocoder.bing_batch import BingBatch, BingBatchResult + +import io +import csv +import sys + +PY2 = sys.version_info < (3, 0) +csv_io = io.BytesIO if PY2 else io.StringIO +csv_encode = (lambda input: input) if PY2 else (lambda input: input.encode('utf-8')) +csv_decode = (lambda input: input) if PY2 else (lambda input: input.decode('utf-8')) + + +class BingBatchForwardResult(BingBatchResult): + + @property + def lat(self): + coord = self._content + if coord: + return float(coord[0]) + + @property + def lng(self): + coord = self._content + if coord: + return float(coord[1]) + + @property + def ok(self): + return bool(self._content) + + def debug(self, verbose=True): + with csv_io() as output: + print('\n', file=output) + print('Bing Batch result\n', file=output) + print('-----------\n', file=output) + print(self._content, file=output) + + if verbose: + print(output.getvalue()) + + return [None, None] + + +class BingBatchForward(BingBatch): + method = 'batch' + _RESULT_CLASS = BingBatchForwardResult + + def generate_batch(self, addresses): + out = csv_io() + writer = csv.writer(out) + writer.writerow([ + 'Id', + 'GeocodeRequest/Query', + 'GeocodeResponse/Point/Latitude', + 'GeocodeResponse/Point/Longitude' + ]) + + for idx, address in enumerate(addresses): + writer.writerow([idx, address, None, None]) + + return csv_encode("Bing Spatial Data Services, 2.0\n{}".format(out.getvalue())) + + def _adapt_results(self, response): + result = csv_io(csv_decode(response)) + # Skipping first line with Bing header + next(result) + + rows = {} + for row in csv.DictReader(result): + rows[row['Id']] = [row['GeocodeResponse/Point/Latitude'], row['GeocodeResponse/Point/Longitude']] + + return rows + + +if __name__ == '__main__': + g = BingBatchForward(['Denver,CO', 'Boulder,CO'], key=None) + g.debug() diff --git a/geocoder/bing_batch_forward.py:Zone.Identifier b/geocoder/bing_batch_forward.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/bing_batch_forward.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/bing_batch_reverse.py b/geocoder/bing_batch_reverse.py new file mode 100644 index 0000000..198649c --- /dev/null +++ b/geocoder/bing_batch_reverse.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import, print_function +from geocoder.bing_batch import BingBatch, BingBatchResult + +import io +import csv +import sys + +PY2 = sys.version_info < (3, 0) +csv_io = io.BytesIO if PY2 else io.StringIO +csv_encode = (lambda input: input) if PY2 else (lambda input: input.encode('utf-8')) +csv_decode = (lambda input: input) if PY2 else (lambda input: input.decode('utf-8')) + + +class BingBatchReverseResult(BingBatchResult): + + @property + def address(self): + address = self._content + if address: + return address[0] + + @property + def city(self): + city = self._content + if city: + return city[1] + + @property + def postal(self): + postal = self._content + if postal: + return postal[2] + + @property + def state(self): + state = self._content + if state: + return state[3] + + @property + def country(self): + country = self._content + if country: + return country[4] + + @property + def ok(self): + return bool(self._content) + + def debug(self, verbose=True): + with csv_io() as output: + print('\n', file=output) + print('Bing Batch result\n', file=output) + print('-----------\n', file=output) + print(self._content, file=output) + + if verbose: + print(output.getvalue()) + + return [None, None] + + +class BingBatchReverse(BingBatch): + + method = 'batch_reverse' + _RESULT_CLASS = BingBatchReverseResult + + def generate_batch(self, locations): + out = csv_io() + writer = csv.writer(out) + writer.writerow([ + 'Id', + 'ReverseGeocodeRequest/Location/Latitude', + 'ReverseGeocodeRequest/Location/Longitude', + 'GeocodeResponse/Address/FormattedAddress', + 'GeocodeResponse/Address/Locality', + 'GeocodeResponse/Address/PostalCode', + 'GeocodeResponse/Address/AdminDistrict', + 'GeocodeResponse/Address/CountryRegion' + ]) + + for idx, location in enumerate(locations): + writer.writerow([idx, location[0], location[1], None, None, None, None, None]) + + return csv_encode("Bing Spatial Data Services, 2.0\n{}".format(out.getvalue())) + + def _adapt_results(self, response): + # print(type(response)) + result = csv_io(csv_decode(response)) + # Skipping first line with Bing header + next(result) + + rows = {} + for row in csv.DictReader(result): + rows[row['Id']] = [ + row['GeocodeResponse/Address/FormattedAddress'], + row['GeocodeResponse/Address/Locality'], + row['GeocodeResponse/Address/PostalCode'], + row['GeocodeResponse/Address/AdminDistrict'], + row['GeocodeResponse/Address/CountryRegion'] + ] + + return rows + + +if __name__ == '__main__': + g = BingBatchReverse([(40.7943, -73.970859), (48.845580, 2.321807)], key=None) + g.debug() diff --git a/geocoder/bing_batch_reverse.py:Zone.Identifier b/geocoder/bing_batch_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/bing_batch_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/bing_reverse.py b/geocoder/bing_reverse.py new file mode 100644 index 0000000..ac83130 --- /dev/null +++ b/geocoder/bing_reverse.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.bing import BingResult, BingQuery +from geocoder.location import Location + + +class BingReverseResult(BingResult): + + @property + def ok(self): + return bool(self.address) + + +class BingReverse(BingQuery): + """ + Bing Maps REST Services + ======================= + The Bing™ Maps REST Services Application Programming Interface (API) + provides a Representational State Transfer (REST) interface to + perform tasks such as creating a static map with pushpins, geocoding + an address, retrieving imagery metadata, or creating a route. + + API Reference + ------------- + http://msdn.microsoft.com/en-us/library/ff701714.aspx + + """ + provider = 'bing' + method = 'reverse' + + _URL = u'http://dev.virtualearth.net/REST/v1/Locations/{0}' + + def _build_params(self, location, provider_key, **kwargs): + return { + 'o': 'json', + 'key': provider_key, + 'maxResults': 1, + } + + def _before_initialize(self, location, **kwargs): + self.url = self.url.format(str(Location(location))) + + +if __name__ == '__main__': + g = BingReverse([45.4049053, -75.7077965], key=None) + g.debug() diff --git a/geocoder/bing_reverse.py:Zone.Identifier b/geocoder/bing_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/bing_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/canadapost.py b/geocoder/canadapost.py new file mode 100644 index 0000000..79a646a --- /dev/null +++ b/geocoder/canadapost.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import canadapost_key_getter + + +class CanadapostIdResult(OneResult): + + @property + def ok(self): + return bool(self.item_id) + + @property + def item_id(self): + return self.raw.get('Id') + + @property + def next_action(self): + return self.raw.get('Next') + + +class CanadapostIdQuery(MultipleResultsQuery): + + provider = 'canadapost' + method = 'id' + + _URL = 'https://ws1.postescanada-canadapost.ca/AddressComplete/Interactive/Find/v2.10/json3ex.ws' + _RESULT_CLASS = CanadapostIdResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + if not provider_key: + provider_key = canadapost_key_getter(**kwargs) + + return { + 'Key': provider_key, + 'LastId': kwargs.get('last_id', ''), + 'Country': kwargs.get('country', 'ca'), + 'SearchFor': 'Everything', + 'SearchTerm': location, + 'LanguagePreference': kwargs.get('language', 'en'), + '$cache': 'true' + } + + def _adapt_results(self, json_response): + return json_response['Items'] + + +class CanadapostResult(OneResult): + + @property + def ok(self): + return bool(self.postal) + + @property + def quality(self): + return self.raw.get('Type') + + @property + def accuracy(self): + return self.raw.get('DataLevel') + + @property + def address(self): + return self.raw.get('Line1') + + @property + def postal(self): + return self.raw.get('PostalCode') + + @property + def housenumber(self): + return self.raw.get('BuildingNumber') + + @property + def street(self): + return self.raw.get('Street') + + @property + def city(self): + return self.raw.get('City') + + @property + def state(self): + return self.raw.get('ProvinceName') + + @property + def country(self): + return self.raw.get('CountryName') + + @property + def unit(self): + return self.raw.get('SubBuilding') + + @property + def domesticId(self): + return self.raw.get('DomesticId') + + @property + def label(self): + return self.raw.get('Label') + + +class CanadapostQuery(MultipleResultsQuery): + """ + Addres Complete API + ======================= + The next generation of address finders, AddressComplete uses + intelligent, fast searching to improve data accuracy and relevancy. + Simply start typing a business name, address or Postal Code + and AddressComplete will suggest results as you go. + + Params + ------ + :param ``location``: Your search location you want geocoded. + :param ``key``: (optional) API Key from CanadaPost Address Complete. + :param ``language``: (default=en) Output language preference. + :param ``country``: (default=ca) Geofenced query by country. + + API Reference + ------------- + https://www.canadapost.ca/pca/ + """ + provider = 'canadapost' + method = 'geocode' + + _URL = 'https://ws1.postescanada-canadapost.ca/AddressComplete/Interactive/RetrieveFormatted/v2.10/json3ex.ws' + _RESULT_CLASS = CanadapostResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + if not provider_key: + provider_key = canadapost_key_getter(**kwargs) + + self.key = provider_key + + last_id = '' + next_action = 'Find' + while next_action == 'Find': + ids = CanadapostIdQuery(location, key=provider_key, last_id=last_id, **kwargs) + next_action = ids.next_action + last_id = ids.item_id + + if not ids.item_id: + raise ValueError("Could not get any Id for given location") + + return { + 'Key': provider_key, + 'Id': ids.item_id, + 'Source': '', + 'MaxResults': kwargs.get('maxRows', 1), + 'cache': 'true' + } + + def _adapt_results(self, json_response): + return json_response['Items'] + + @property + def canadapost_api_key(self): + return self.key + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = CanadapostQuery("453 Booth Street, ON") + g.debug() diff --git a/geocoder/canadapost.py:Zone.Identifier b/geocoder/canadapost.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/canadapost.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/cli.py b/geocoder/cli.py new file mode 100644 index 0000000..9afb8c9 --- /dev/null +++ b/geocoder/cli.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import click +import json +import geocoder +import os +import fileinput + +from geocoder.api import options + + +providers = sorted(options.keys()) +methods = ['geocode', 'reverse', 'elevation', 'timezone', 'places'] +outputs = ['json', 'osm', 'geojson', 'wkt'] +units = ['kilometers', 'miles', 'feet', 'meters'] + + +@click.command() +@click.argument('location', nargs=-1) +@click.option('--provider', '-p', default='osm', type=click.Choice(providers)) +@click.option('--method', '-m', default='geocode', type=click.Choice(methods)) +@click.option('--output', '-o', default='json', type=click.Choice(outputs)) +@click.option('--units', '-u', default='kilometers', type=click.Choice(units)) +@click.option('--timeout', '-t', default=5.0) +@click.option('--distance', is_flag=True) +@click.option('--language', default='') +@click.option('--url', default='') +@click.option('--proxies') +@click.option('--key') +# following are for Tamu provider +@click.option('--city', '-c', default='') +@click.option('--state', '-s', default='') +@click.option('--zipcode', '-z', default='') +def cli(location, **kwargs): + """Geocode an arbitrary number of strings from Command Line.""" + + locations = [] + + # Read Standard Input + # $ cat foo.txt | geocode + try: + for line in fileinput.input(): + locations.append(line.strip()) + except: + pass + + # Read multiple files & user input location + for item in location: + if os.path.exists(item): + with open(item, 'rb') as f: + locations += f.read().splitlines() + else: + locations.append(item) + + # Distance calculation + if kwargs['distance']: + d = geocoder.distance(locations, **kwargs) + click.echo(d) + return + + # Geocode results from user input + for location in locations: + g = geocoder.get(location.strip(), **kwargs) + try: + click.echo(json.dumps(getattr(g, kwargs['output']))) + except IOError: + # When invalid command is entered a broken pipe error occurs + return + + +if __name__ == '__main__': + cli() diff --git a/geocoder/cli.py:Zone.Identifier b/geocoder/cli.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/cli.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/distance.py b/geocoder/distance.py new file mode 100644 index 0000000..85dade5 --- /dev/null +++ b/geocoder/distance.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from math import radians, cos, sin, asin, sqrt +from geocoder.location import Location + +AVG_EARTH_RADIUS = 6371 # in km + + +def Distance(*args, **kwargs): + total = 0.0 + last = None + + if len(args) == 1 and isinstance(args, (list, tuple)): + args = args[0] + + if len(args) <= 1: + raise ValueError("Distance needs at least two locations") + + for location in args: + if last: + distance = haversine(Location(last), Location(location), **kwargs) + if distance: + total += distance + last = location + + return total + + +def haversine(point1, point2, **kwargs): + """ Calculate the great-circle distance bewteen two points on the Earth surface. + + :input: two 2-tuples, containing the latitude and longitude of each point + in decimal degrees. + + Example: haversine((45.7597, 4.8422), (48.8567, 2.3508)) + + :output: Returns the distance bewteen the two points. + The default unit is kilometers. Miles can be returned + if the ``miles`` parameter is set to True. + + """ + + lookup_units = { + 'miles': 'miles', + 'mile': 'miles', + 'mi': 'miles', + 'ml': 'miles', + 'kilometers': 'kilometers', + 'kilometres': 'kilometers', + 'kilometer': 'kilometers', + 'kilometre': 'kilometers', + 'km': 'kilometers', + 'meters': 'meters', + 'metres': 'meters', + 'meter': 'meters', + 'metre': 'meters', + 'm': 'meters', + 'feet': 'feet', + 'f': 'feet', + 'ft': 'feet', + } + + if point1.ok and point2.ok: + # convert all latitudes/longitudes from decimal degrees to radians + lat1, lng1, lat2, lng2 = list(map(radians, point1.latlng + point2.latlng)) + + # calculate haversine + lat = lat2 - lat1 + lng = lng2 - lng1 + d = sin(lat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(lng / 2) ** 2 + h = 2 * AVG_EARTH_RADIUS * asin(sqrt(d)) + + # Measurements + units = kwargs.get('units', 'kilometers').lower() + units_calculation = { + 'miles': h * 0.621371, + 'feet': h * 0.621371 * 5280, + 'meters': h * 1000, + 'kilometers': h, + } + + if units in lookup_units: + return units_calculation[lookup_units[units]] + else: + raise ValueError("Unknown units of measurement") + + else: + print(u'[WARNING] Error calculating the following two locations.\n' + 'Points: {0} to {1}'.format(point1.location, point2.location)) + +if __name__ == '__main__': + d = Distance('Ottawa, ON', 'Toronto, ON', 'Montreal, QC') + print(d) diff --git a/geocoder/distance.py:Zone.Identifier b/geocoder/distance.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/distance.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/freegeoip.py b/geocoder/freegeoip.py new file mode 100644 index 0000000..9ad83a2 --- /dev/null +++ b/geocoder/freegeoip.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging +import requests +import ratelim + +from geocoder.base import OneResult, MultipleResultsQuery + + +class FreeGeoIPResult(OneResult): + + @property + def lat(self): + return self.raw.get('latitude') + + @property + def lng(self): + return self.raw.get('longitude') + + @property + def address(self): + if self.city: + return u'{0}, {1} {2}'.format(self.city, self.state, self.country) + elif self.state: + return u'{0}, {1}'.format(self.state, self.country) + elif self.country: + return u'{0}'.format(self.country) + return u'' + + @property + def postal(self): + zip_code = self.raw.get('zip_code') + postal_code = self.raw.get('postal_code') + if zip_code: + return zip_code + if postal_code: + return postal_code + + @property + def city(self): + return self.raw.get('city') + + @property + def state(self): + return self.raw.get('region') + + @property + def region_code(self): + return self.raw.get('region_code') + + @property + def country(self): + return self.raw.get('country_name') + + @property + def country_code3(self): + return self.raw.get('country_code3') + + @property + def continent(self): + return self.raw.get('continent') + + @property + def timezone(self): + return self.raw.get('timezone') + + @property + def area_code(self): + return self.raw.get('area_code') + + @property + def dma_code(self): + return self.raw.get('dma_code') + + @property + def offset(self): + return self.raw.get('offset') + + @property + def organization(self): + return self.raw.get('organization') + + @property + def ip(self): + return self.raw.get('ip') + + @property + def time_zone(self): + return self.raw.get('time_zone') + + +class FreeGeoIPQuery(MultipleResultsQuery): + """ + FreeGeoIP.net + ============= + freegeoip.net provides a public HTTP API for software developers to + search the geolocation of IP addresses. It uses a database of IP addresses + that are associated to cities along with other relevant information like + time zone, latitude and longitude. + + You're allowed up to 10,000 queries per hour by default. Once this + limit is reached, all of your requests will result in HTTP 403, + forbidden, until your quota is cleared. + + API Reference + ------------- + http://freegeoip.net/ + """ + provider = 'freegeoip' + method = 'geocode' + + _URL = 'https://freegeoip.net/json/' + _RESULT_CLASS = FreeGeoIPResult + _KEY_MANDATORY = False + + def _before_initialize(self, location, **kwargs): + self.url += location + + @staticmethod + @ratelim.greedy(10000, 60 * 60) + def rate_limited_get(*args, **kwargs): + return requests.get(*args, **kwargs) + + def _adapt_results(self, json_response): + return [json_response] + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = FreeGeoIPQuery('99.240.181.199') + g.debug() diff --git a/geocoder/freegeoip.py:Zone.Identifier b/geocoder/freegeoip.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/freegeoip.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/gaode.py b/geocoder/gaode.py new file mode 100644 index 0000000..f2f0fb6 --- /dev/null +++ b/geocoder/gaode.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import gaode_key + + +class GaodeResult(OneResult): + + @property + def lat(self): + return float(self.raw.get('location', '0,0').replace("'", '').split(',')[1]) + + @property + def lng(self): + return float(self.raw.get('location', '0,0').replace("'", '').split(',')[0]) + + @property + def quality(self): + return self.raw.get('level') + + @property + def address(self): + return self.raw.get('formatted_address') + + @property + def country(self): + return '中国' + + @property + def province(self): + return self.raw.get('province') + + @property + def state(self): + return self.raw.get('province') + + @property + def city(self): + return self.raw.get('city') + + @property + def district(self): + return self.raw.get('district') + + @property + def street(self): + return self.raw.get('street') + + @property + def adcode(self): + return self.raw.get('adcode') + + @property + def housenumber(self): + return self.raw.get('number') + + +class GaodeQuery(MultipleResultsQuery): + """ + Gaode AMap Geocoding API + =================== + Gaode Maps Geocoding API is a free open the API, the default quota + 2000 times / day. + + Params + ------ + :param location: Your search location you want geocoded. + :param key: Gaode API key. + + References + ---------- + API Documentation: http://lbs.amap.com/api/webservice/guide/api/georegeo + Get AMap Key: http://lbs.amap.com/dev/ + """ + provider = 'gaode' + method = 'geocode' + + _URL = 'http://restapi.amap.com/v3/geocode/geo' + _RESULT_CLASS = GaodeResult + _KEY = gaode_key + + def _build_params(self, location, provider_key, **kwargs): + return { + 'address': location, + 'output': 'JSON', + 'key': provider_key, + } + + def _build_headers(self, provider_key, **kwargs): + return {'Referer': kwargs.get('referer', '')} + + def _adapt_results(self, json_response): + return json_response['geocodes'] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = GaodeQuery('将台路') + g.debug() diff --git a/geocoder/gaode.py:Zone.Identifier b/geocoder/gaode.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/gaode.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/gaode_reverse.py b/geocoder/gaode_reverse.py new file mode 100644 index 0000000..1fc1901 --- /dev/null +++ b/geocoder/gaode_reverse.py @@ -0,0 +1,106 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.location import Location +from geocoder.base import OneResult +from geocoder.gaode import GaodeQuery + + +class GaodeReverseResult(OneResult): + + @property + def ok(self): + return bool(self.address) + + @property + def address(self): + return self.raw['formatted_address'] + + @property + def country(self): + return self.raw['addressComponent']['country'] + + @property + def province(self): + return self.raw['addressComponent']['province'] + + @property + def state(self): + return self.raw['addressComponent']['province'] + + @property + def city(self): + if len(self.raw['addressComponent']['city']) == 0: + return self.raw['addressComponent']['province'] + else: + return self.raw['addressComponent']['city'] + + @property + def district(self): + return self.raw['addressComponent']['district'] + + @property + def street(self): + return self.raw['addressComponent']['streetNumber']['street'] + + @property + def adcode(self): + return self.raw['addressComponent']['adcode'] + + @property + def township(self): + return self.raw['addressComponent']['township'] + + @property + def towncode(self): + return self.raw['addressComponent']['towncode'] + + @property + def housenumber(self): + return self.raw['addressComponent']['streetNumber']['number'] + + +class GaodeReverse(GaodeQuery): + """ + Gaode GeoReverse API + =================== + Gaode Maps GeoReverse API is a free open the API, the default quota + 2000 times / day. + + Params + ------ + :param location: Your search location you want geocoded. + :param key: Gaode API key. + :param referer: Gaode API referer website. + + References + ---------- + API Documentation: http://lbs.amap.com/api/webservice/guide/api/georegeo + Get Gaode AMap Key: http://lbs.amap.com/dev/ + """ + provider = 'gaode' + method = 'reverse' + + _URL = 'http://restapi.amap.com/v3/geocode/regeo' + _RESULT_CLASS = GaodeReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'location': str(location.lng) + ',' + str(location.lat), + 'output': 'json', + 'key': provider_key, + } + + def _adapt_results(self, json_response): + return [json_response['regeocode']] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = GaodeReverse("39.971577, 116.506142") + g.debug() diff --git a/geocoder/gaode_reverse.py:Zone.Identifier b/geocoder/gaode_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/gaode_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/geocodefarm.py b/geocoder/geocodefarm.py new file mode 100644 index 0000000..ab15680 --- /dev/null +++ b/geocoder/geocodefarm.py @@ -0,0 +1,202 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import geocodefarm_key + + +class GeocodeFarmResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._coordinates = json_content.get('COORDINATES', {}) + self._boundaries = json_content.get('BOUNDARIES', {}) + self._address = json_content.get('ADDRESS', {}) + self._location_details = json_content.get('LOCATION_DETAILS', {}) + + # proceed with super.__init__ + super(GeocodeFarmResult, self).__init__(json_content) + + @property + def lat(self): + lat = self._coordinates.get('latitude') + if lat: + return float(lat) + + @property + def lng(self): + lng = self._coordinates.get('longitude') + if lng: + return float(lng) + + @property + def accuracy(self): + return self.raw.get('accuracy') + + @property + def bbox(self): + south = self._boundaries.get('southwest_latitude') + west = self._boundaries.get('southwest_longitude') + north = self._boundaries.get('northeast_latitude') + east = self._boundaries.get('northeast_longitude') + return self._get_bbox(south, west, north, east) + + @property + def address(self): + return self.raw.get('formatted_address') + + @property + def housenumber(self): + return self._address.get('street_number') + + @property + def street(self): + return self._address.get('street_name') + + @property + def neighborhood(self): + return self._address.get('neighborhood') + + @property + def city(self): + return self._address.get('locality') + + @property + def county(self): + return self._address.get('admin_2') + + @property + def state(self): + return self._address.get('admin_1') + + @property + def country(self): + return self._address.get('country') + + @property + def postal(self): + return self._address.get('postal_code') + + @property + def elevation(self): + return self._location_details.get('elevation') + + @property + def timezone_long(self): + return self._location_details.get('timezone_long') + + @property + def timezone_short(self): + return self._location_details.get('timezone_short') + + +class GeocodeFarmQuery(MultipleResultsQuery): + """ + Geocode.Farm + ============ + Geocode.Farm is one of the few providers that provide this highly + specialized service for free. We also have affordable paid plans, of + course, but our free services are of the same quality and provide the same + results. The major difference between our affordable paid plans and our + free API service is the limitations. On one of our affordable paid plans + your limit is set based on the plan you signed up for, starting at 25,000 + query requests per day (API calls). On our free API offering, you are + limited to 250 query requests per day (API calls). + + Params + ------ + :param location: The string to search for. Usually a street address. + :param key: (optional) API Key. Only Required for Paid Users. + :param lang: (optional) 2 digit lanuage code to return results in. Currently only "en"(English) or "de"(German) supported. + :param country: (optional) The country to return results in. Used for biasing purposes and may not fully filter results to this specific country. + + API Reference + ------------- + https://geocode.farm/geocoding/free-api-documentation/ + """ + provider = 'geocodefarm' + method = 'geocode' + + _URL = 'https://www.geocode.farm/v3/json/forward/' + _RESULT_CLASS = GeocodeFarmResult + _KEY = geocodefarm_key + _KEY_MANDATORY = False + + def __init__(self, location, **kwargs): + super(GeocodeFarmQuery, self).__init__(location, **kwargs) + + self.api_status = {} + self.api_account = {} + + def _build_params(self, location, provider_key, **kwargs): + return { + 'addr': location, + 'key': provider_key, + 'lang': kwargs.get('lang', ''), + 'country': kwargs.get('country', ''), + 'count': kwargs.get('maxRows', 1), + } + + def _catch_errors(self, json_response): + status = json_response['geocoding_results']['STATUS'].get('status') + if not status == 'SUCCESS': + self.error = status + + return self.error + + def _adapt_results(self, json_response): + return json_response['geocoding_results']['RESULTS'] + + def _parse_results(self, json_response): + super(GeocodeFarmQuery, self)._parse_results(json_response) + + # store status and account details + self.api_status = json_response['geocoding_results']['STATUS'] + self.api_account = json_response['geocoding_results']['ACCOUNT'] + + @property + def access(self): + return self.api_status.get('access') + + @property + def address_provided(self): + return self.api_status.get('address_provided') + + @property + def ip_address(self): + return self.api_account.get('ip_address') + + @property + def distribution_license(self): + return self.api_account.get('distribution_license') + + @property + def usage_limit(self): + usage_limit = self.api_account.get('usage_limit') + if usage_limit: + return int(usage_limit) + + @property + def used_today(self): + used_today = self.api_account.get('used_today') + if used_today: + return int(used_today) + + @property + def used_total(self): + used_total = self.api_account.get('used_total') + if used_total: + return int(used_total) + + @property + def first_used(self): + return self.api_account.get('first_used') + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = GeocodeFarmQuery("New York City") + g.debug() diff --git a/geocoder/geocodefarm.py:Zone.Identifier b/geocoder/geocodefarm.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/geocodefarm.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/geocodefarm_reverse.py b/geocoder/geocodefarm_reverse.py new file mode 100644 index 0000000..0add428 --- /dev/null +++ b/geocoder/geocodefarm_reverse.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.location import Location +from geocoder.geocodefarm import GeocodeFarmQuery + + +class GeocodeFarmReverse(GeocodeFarmQuery): + """ + Geocode.Farm + ======================= + Geocode.Farm is one of the few providers that provide this highly + specialized service for free. We also have affordable paid plans, of + course, but our free services are of the same quality and provide the same + results. The major difference between our affordable paid plans and our + free API service is the limitations. On one of our affordable paid plans + your limit is set based on the plan you signed up for, starting at 25,000 + query requests per day (API calls). On our free API offering, you are + limited to 250 query requests per day (API calls). + + Params + ------ + :param lat: The numerical latitude value for which you wish to obtain the closest, human-readable address. + :param lon: The numerical longitude value for which you wish to obtain the closest, human-readable address. + :param key: (optional) API Key. Only Required for Paid Users. + :param lang: (optional) 2 digit lanuage code to return results in. Currently only "en"(English) or "de"(German) supported. + :param country: (optional) The country to return results in. Used for biasing purposes and may not fully filter results to this specific country. + + API Reference + ------------- + https://geocode.farm/geocoding/free-api-documentation/ + """ + provider = 'geocodefarm' + method = 'reverse' + + _URL = 'https://www.geocode.farm/v3/json/reverse/' + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'lat': location.latitude, + 'lon': location.longitude, + 'key': provider_key, + 'lang': kwargs.get('lang', ''), + 'country': kwargs.get('country', ''), + } + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = GeocodeFarmReverse([45.3, -75.4]) + g.debug() diff --git a/geocoder/geocodefarm_reverse.py:Zone.Identifier b/geocoder/geocodefarm_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/geocodefarm_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/geolytica.py b/geocoder/geolytica.py new file mode 100644 index 0000000..7822090 --- /dev/null +++ b/geocoder/geolytica.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery + + +class GeolyticaResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._standard = json_content.get('standard', {}) + + # proceed with super.__init__ + super(GeolyticaResult, self).__init__(json_content) + + @property + def lat(self): + lat = self.raw.get('latt', '').strip() + if lat: + return float(lat) + + @property + def lng(self): + lng = self.raw.get('longt', '').strip() + if lng: + return float(lng) + + @property + def postal(self): + return self.raw.get('postal', '').strip() + + @property + def housenumber(self): + return self._standard.get('stnumber', '').strip() + + @property + def street(self): + return self._standard.get('staddress', '').strip() + + @property + def city(self): + return self._standard.get('city', '').strip() + + @property + def state(self): + return self._standard.get('prov', '').strip() + + @property + def address(self): + if self.street_number: + return u'{0} {1}, {2}'.format(self.street_number, self.route, self.locality) + elif self.route and self.route != 'un-known': + return u'{0}, {1}'.format(self.route, self.locality) + else: + return self.locality + + +class GeolyticaQuery(MultipleResultsQuery): + """ + Geocoder.ca + =========== + A Canadian and US location geocoder. + + API Reference + ------------- + http://geocoder.ca/?api=1 + """ + provider = 'geolytica' + method = 'geocode' + + _URL = 'http://geocoder.ca' + _RESULT_CLASS = GeolyticaResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + params = { + 'json': 1, + 'locate': location, + 'geoit': 'xml' + } + if 'strictmode' in kwargs: + params.update({'strictmode': kwargs.pop('strictmode')}) + if 'strict' in kwargs: + params.update({'strict': kwargs.pop('strict')}) + if 'auth' in kwargs: + params.update({'auth': kwargs.pop('auth')}) + return params + + def _adapt_results(self, json_response): + return [json_response] + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = GeolyticaQuery('1552 Payette dr., Ottawa') + g.debug() diff --git a/geocoder/geolytica.py:Zone.Identifier b/geocoder/geolytica.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/geolytica.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/geonames.py b/geocoder/geonames.py new file mode 100644 index 0000000..255f8a6 --- /dev/null +++ b/geocoder/geonames.py @@ -0,0 +1,151 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import json +import logging + +from geocoder.base import MultipleResultsQuery, OneResult +from geocoder.keys import geonames_username +from geocoder.location import BBox + +LOGGER = logging.getLogger(__name__) + + +class GeonamesResult(OneResult): + + @property + def lat(self): + return self.raw.get('lat') + + @property + def lng(self): + return self.raw.get('lng') + + @property + def geonames_id(self): + return self.raw.get('geonameId') + + @property + def address(self): + return self.raw.get('name') + + @property + def feature_class(self): + return self.raw.get('fcl') + + @property + def class_description(self): + return self.raw.get('fclName') + + @property + def code(self): + return self.raw.get('fcode') + + @property + def description(self): + return self.raw.get('fcodeName') + + @property + def state(self): + return self.raw.get('adminName1') + + @property + def state_code(self): + return self.raw.get('adminCode1') + + @property + def country(self): + return self.raw.get('countryName') + + @property + def country_code(self): + return self.raw.get('countryCode') + + @property + def population(self): + return self.raw.get('population') + + +class GeonamesQuery(MultipleResultsQuery): + """ + GeoNames REST Web Services + ========================== + GeoNames is mainly using REST webservices. Find nearby postal codes / reverse geocoding + This service comes in two flavors.You can either pass the lat/long or a postalcode/placename. + + API Reference + ------------- + http://www.geonames.org/export/web-services.html + """ + provider = 'geonames' + method = 'geocode' + + _URL = 'http://api.geonames.org/searchJSON' + _RESULT_CLASS = GeonamesResult + _KEY = geonames_username + + def _build_params(self, location, provider_key, **kwargs): + """Will be overridden according to the targetted web service""" + base_kwargs = { + 'q': location, + 'fuzzy': kwargs.get('fuzzy', 1.0), + 'username': provider_key, + 'maxRows': kwargs.get('maxRows', 1), + } + # check out for bbox in kwargs + bbox = kwargs.pop('proximity', None) + if bbox is not None: + bbox = BBox.factory(bbox) + base_kwargs.update( + {'east': bbox.east, 'west': bbox.west, + 'north': bbox.north, 'south': bbox.south}) + + # look out for valid extra kwargs + supported_kwargs = set(( + 'name', 'name_equals', 'name_startsWith', 'startRow', + 'country', 'countryBias', 'continentCode', + 'adminCode1', 'adminCode2', 'adminCode3', 'cities', + 'featureClass', 'featureCode', + 'lang', 'type', 'style', + 'isNameRequired', 'tag', 'operator', 'charset', + 'east', 'west', 'north', 'south', + 'orderby', 'inclBbox', + )) + found_kwargs = supported_kwargs & set(kwargs.keys()) + LOGGER.debug("Adding extra kwargs %s", found_kwargs) + + # update base kwargs with extra ones + base_kwargs.update(dict( + [(extra, kwargs[extra]) for extra in found_kwargs] + )) + return base_kwargs + + def _catch_errors(self, json_response): + """ Changed: removed check on number of elements: + - totalResultsCount not sytematically returned (e.g in hierarchy) + - done in base.py + """ + status = json_response.get('status') + if status: + message = status.get('message') + value = status.get('value') + custom_messages = { + 10: 'Invalid credentials', + 18: 'Do not use the demo account for your application', + } + self.error = custom_messages.get(value, message) + LOGGER.error("Error %s from JSON %s", self.error, json_response) + + return self.error + + def _adapt_results(self, json_response): + # extract the array of JSON objects + return json_response['geonames'] + + +if __name__ == '__main__': + g = GeonamesQuery('Ottawa, Ontario', maxRows=1) + print(json.dumps(g.geojson, indent=4)) + g.debug() diff --git a/geocoder/geonames.py:Zone.Identifier b/geocoder/geonames.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/geonames.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/geonames_children.py b/geocoder/geonames_children.py new file mode 100644 index 0000000..66ef6cf --- /dev/null +++ b/geocoder/geonames_children.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import + +from geocoder.geonames import GeonamesQuery + + +class GeonamesChildren(GeonamesQuery): + """ Children: + http://api.geonames.org/childrenJSON?formatted=true&geonameId=6094817 + """ + + provider = 'geonames' + method = 'children' + + _URL = 'http://api.geonames.org/childrenJSON' + + def _build_params(self, location, provider_key, **kwargs): + """Will be overridden according to the targetted web service""" + return { + 'geonameId': location, + 'username': provider_key, + } + + +if __name__ == '__main__': + print("Searching Ottawa...") + g = GeonamesQuery('Ottawa, Ontario') + g.debug() + print("Searching its children...") + c = GeonamesChildren(g.pop().geonames_id) + c.debug() diff --git a/geocoder/geonames_children.py:Zone.Identifier b/geocoder/geonames_children.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/geonames_children.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/geonames_details.py b/geocoder/geonames_details.py new file mode 100644 index 0000000..d9fe6c4 --- /dev/null +++ b/geocoder/geonames_details.py @@ -0,0 +1,122 @@ +from __future__ import absolute_import + +from geocoder.geonames import GeonamesQuery, GeonamesResult + + +class GeonamesFullResult(GeonamesResult): + """ Get more information for given geonames_id, e.g timzone and administrative hierarchy""" + + @property + def continent(self): + return self.raw.get('continentCode', "") + + @property + def country_geonames_id(self): + return self.raw.get('countryId', 0) + + @property + def state_geonames_id(self): + return self.raw.get('adminId1', 0) + + @property + def admin2(self): + return self.raw.get('adminName2', "") + + @property + def admin2_geonames_id(self): + return self.raw.get('adminId2', "") + + @property + def admin3(self): + return self.raw.get('adminName3', "") + + @property + def admin3_geonames_id(self): + return self.raw.get('adminId3', "") + + @property + def admin4(self): + return self.raw.get('adminName4', "") + + @property + def admin4_geonames_id(self): + return self.raw.get('adminId4', "") + + @property + def admin5(self): + return self.raw.get('adminName5', "") + + @property + def admin5_geonames_id(self): + return self.raw.get('adminId5', "") + + @property + def srtm3(self): + return self.raw.get('srtm3', 0) + + @property + def wikipedia(self): + return self.raw.get('wikipediaURL', "") + + @property + def timeZoneId(self): + timezone = self.raw.get('timezone') + if timezone: + return timezone.get('timeZoneId') + + @property + def timeZoneName(self): + timezone = self.raw.get('timezone') + if timezone: + return timezone.get('timeZoneId') + + @property + def rawOffset(self): + timezone = self.raw.get('timezone') + if timezone: + return timezone.get('gmtOffset') + + @property + def dstOffset(self): + timezone = self.raw.get('timezone') + if timezone: + return timezone.get('dstOffset') + + @property + def bbox(self): + bbox = self.raw.get('bbox', {}) + south = bbox.get('south') + west = bbox.get('west') + north = bbox.get('north') + east = bbox.get('east') + return self._get_bbox(south, west, north, east) + + +class GeonamesDetails(GeonamesQuery): + """ Details: + http://api.geonames.org/getJSON?geonameId=6094817&style=full + """ + + provider = 'geonames' + method = 'details' + + _URL = 'http://api.geonames.org/getJSON' + _RESULT_CLASS = GeonamesFullResult + + def _build_params(self, location, provider_key, **kwargs): + """Will be overridden according to the targetted web service""" + return { + 'geonameId': location, + 'username': provider_key, + 'style': 'full' + } + + def _adapt_results(self, json_response): + # the returned JSON contains the object. + # Need to wrap it into an array + return [json_response] + + +if __name__ == '__main__': + c = GeonamesDetails(6094817) + c.debug() diff --git a/geocoder/geonames_details.py:Zone.Identifier b/geocoder/geonames_details.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/geonames_details.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/geonames_hierarchy.py b/geocoder/geonames_hierarchy.py new file mode 100644 index 0000000..a9184d8 --- /dev/null +++ b/geocoder/geonames_hierarchy.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import + +from geocoder.geonames_children import GeonamesChildren + + +class GeonamesHierarchy(GeonamesChildren): + """ Hierarchy: + http://api.geonames.org/hierarchyJSON?formatted=true&geonameId=6094817 + """ + + provider = 'geonames' + method = 'hierarchy' + + _URL = 'http://api.geonames.org/hierarchyJSON' + + +if __name__ == '__main__': + print("Searching Ottawa's hierarchy...") + c = GeonamesHierarchy(6094817) + c.debug() diff --git a/geocoder/geonames_hierarchy.py:Zone.Identifier b/geocoder/geonames_hierarchy.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/geonames_hierarchy.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/gisgraphy.py b/geocoder/gisgraphy.py new file mode 100644 index 0000000..2814f8c --- /dev/null +++ b/geocoder/gisgraphy.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery + + +class GisgraphyResult(OneResult): + + @property + def lat(self): + return self.raw.get('lat') + + @property + def lng(self): + return self.raw.get('lng') + + @property + def address(self): + return self.raw.get('formatedFull', '') + + @property + def country(self): + return self.raw.get('countryCode', '') + + @property + def state(self): + return self.raw.get('state', '') + + @property + def city(self): + return self.raw.get('city', '') + + @property + def street(self): + return self.raw.get('streetName', '') + + @property + def housenumber(self): + return self.raw.get('houseNumber', '') + + @property + def postal(self): + return self.raw.get('zipCode', '') + + +class GisgraphyQuery(MultipleResultsQuery): + """ + Gisgraphy REST API + ======================= + + API Reference + ------------- + http://www.gisgraphy.com/documentation/user-guide.php + """ + provider = 'gisgraphy' + method = 'geocode' + + _URL = 'https://services.gisgraphy.com/geocoding/' + _RESULT_CLASS = GisgraphyResult + _KEY_MANDATORY = False + + def _build_headers(self, provider_key, **kwargs): + return { + 'Referer': "https://services.gisgraphy.com", + 'User-agent': 'geocoder-converter' + } + + def _build_params(self, location, provider_key, **kwargs): + return { + 'address': location, + 'limitnbresult': kwargs.get('maxRows', 1), + 'format': 'json', + } + + def _adapt_results(self, json_response): + return json_response['result'] + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = GisgraphyQuery('Ottawa Ontario', maxRows=3) + g.debug() diff --git a/geocoder/gisgraphy.py:Zone.Identifier b/geocoder/gisgraphy.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/gisgraphy.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/gisgraphy_reverse.py b/geocoder/gisgraphy_reverse.py new file mode 100644 index 0000000..e2f85ea --- /dev/null +++ b/geocoder/gisgraphy_reverse.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.location import Location +from geocoder.gisgraphy import GisgraphyResult, GisgraphyQuery + + +class GisgraphyReverseResult(GisgraphyResult): + + @property + def ok(self): + return bool(self.address) + + +class GisgraphyReverse(GisgraphyQuery): + """ + Gisgraphy REST API + ======================= + + API Reference + ------------- + http://www.gisgraphy.com/documentation/user-guide.php + """ + provider = 'gisgraphy' + method = 'reverse' + + _URL = 'https://services.gisgraphy.com/reversegeocoding/' + _RESULT_CLASS = GisgraphyReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'lat': location.lat, + 'lng': location.lng, + 'format': 'json', + } + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = GisgraphyReverse("45.4 -75.7") + g.debug() diff --git a/geocoder/gisgraphy_reverse.py:Zone.Identifier b/geocoder/gisgraphy_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/gisgraphy_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/google.py b/geocoder/google.py new file mode 100644 index 0000000..ea8160a --- /dev/null +++ b/geocoder/google.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +import six +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import google_key, google_client, google_client_secret +from collections import OrderedDict +import ratelim + + +class GoogleResult(OneResult): + + def __init__(self, json_content): + # flatten geometry + geometry = json_content.get('geometry', {}) + self._location = geometry.get('location', {}) + self._location_type = geometry.get('location_type', {}) + self._viewport = geometry.get('viewport', {}) + + # Parse address components with short & long names + for item in json_content['address_components']: + for category in item['types']: + json_content.setdefault(category, {}) + json_content[category]['long_name'] = item['long_name'] + json_content[category]['short_name'] = item['short_name'] + + # proceed with super.__init__ + super(GoogleResult, self).__init__(json_content) + + @property + def lat(self): + return self._location.get('lat') + + @property + def lng(self): + return self._location.get('lng') + + @property + def place(self): + return self.raw.get('place_id') + + @property + def quality(self): + quality = self.raw.get('types') + if quality: + return quality[0] + + @property + def accuracy(self): + return self._location_type + + @property + def bbox(self): + south = self._viewport.get('southwest', {}).get('lat') + west = self._viewport.get('southwest', {}).get('lng') + north = self._viewport.get('northeast', {}).get('lat') + east = self._viewport.get('northeast', {}).get('lng') + return self._get_bbox(south, west, north, east) + + @property + def address(self): + return self.raw.get('formatted_address') + + @property + def postal(self): + return self.raw.get('postal_code', {}).get('short_name') + + @property + def subpremise(self): + return self.raw.get('subpremise', {}).get('short_name') + + @property + def housenumber(self): + return self.raw.get('street_number', {}).get('short_name') + + @property + def street(self): + return self.raw.get('route', {}).get('short_name') + + @property + def street_long(self): + return self.raw.get('route', {}).get('long_name') + + @property + def road_long(self): + return self.street_long + + @property + def neighborhood(self): + return self.raw.get('neighborhood', {}).get('short_name') + + @property + def sublocality(self): + return self.raw.get('sublocality', {}).get('short_name') + + @property + def city(self): + return self.raw.get('locality', {}).get('short_name') or self.postal_town + + @property + def city_long(self): + return self.raw.get('locality', {}).get('long_name') or self.postal_town_long + + @property + def postal_town(self): + return self.raw.get('postal_town', {}).get('short_name') + + @property + def postal_town_long(self): + return self.raw.get('postal_town', {}).get('long_name') + + @property + def county(self): + return self.raw.get('administrative_area_level_2', {}).get('short_name') + + @property + def state(self): + return self.raw.get('administrative_area_level_1', {}).get('short_name') + + @property + def state_long(self): + return self.raw.get('administrative_area_level_1', {}).get('long_name') + + @property + def province_long(self): + return self.state_long + + @property + def country(self): + return self.raw.get('country', {}).get('short_name') + + @property + def country_long(self): + return self.raw.get('country', {}).get('long_name') + + +class GoogleQuery(MultipleResultsQuery): + """ + Google Geocoding API + ==================== + Geocoding is the process of converting addresses into geographic + coordinates (like latitude 37.423021 and longitude -122.083739), + which you can use to place markers or position the map. + API Reference + ------------- + https://developers.google.com/maps/documentation/geocoding + + For ambiguous queries or 'nearby' type queries, use the Places Text Search instead. + https://developers.google.com/maps/documentation/geocoding/best-practices#automated-system + + Parameters + ---------- + :param location: Your search location you want geocoded. + :param components: Component Filtering + :param method: (default=geocode) Use the following: + > geocode + > places + > reverse + > timezone + > elevation + :param key: Your Google developers free key. + :param language: 2-letter code of preferred language of returned address elements. + :param client: Google for Work client ID. Use with client_secret. Cannot use with key parameter + :param client_secret: Google for Work client secret. Use with client. + """ + provider = 'google' + method = 'geocode' + + _URL = 'https://maps.googleapis.com/maps/api/geocode/json' + _RESULT_CLASS = GoogleResult + _KEY = google_key + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + params = self._location_init(location, **kwargs) + params['language'] = kwargs.get('language', '') + self.rate_limit = kwargs.get('rate_limit', True) + + # adapt params to authentication method + # either with client / secret + self.client = kwargs.get('client', google_client) + self.client_secret = kwargs.get('client_secret', google_client_secret) + + if self.client and self.client_secret: + params['client'] = self.client + return self._encode_params(params) + # or API key + else: + # provider_key is computed in base.py: + # either cls._KEY (google_key) or kwargs['key'] if provided + params['key'] = provider_key + return params + + def _location_init(self, location, **kwargs): + return { + 'address': location, + 'bounds': kwargs.get('bounds', ''), + 'components': kwargs.get('components', ''), + 'region': kwargs.get('region', ''), + } + + def _encode_params(self, params): + # turn non-empty params into sorted list in order to maintain signature validity. + # Requests will honor the order. + ordered_params = sorted([(k, v) + for (k, v) in params.items() if v]) + params = OrderedDict(ordered_params) + + # the signature parameter needs to come in the end of the url + params['signature'] = self._sign_url( + self.url, ordered_params, self.client_secret) + + return params + + def _sign_url(self, base_url=None, params=None, client_secret=None): + """ Sign a request URL with a Crypto Key. + Usage: + from urlsigner import sign_url + signed_url = sign_url(base_url=my_url, + params=url_params, + client_secret=CLIENT_SECRET) + Args: + base_url - The trunk of the URL to sign. E.g. https://maps.googleapis.com/maps/api/geocode/json + params - List of tuples of URL parameters INCLUDING YOUR CLIENT ID ('client','gme-...') + client_secret - Your Crypto Key from Google for Work + Returns: + The signature as a dictionary #signed request URL + """ + import hashlib + import hmac + import base64 + if six.PY3: + from urllib.parse import urlparse, urlencode + else: + from urllib import urlencode + from urlparse import urlparse + + # Return if any parameters aren't given + if not base_url or not self.client_secret or not self.client: + return None + + # assuming parameters will be submitted to Requests in identical order! + url = urlparse(base_url + "?" + urlencode(params)) + + # We only need to sign the path+query part of the string + url_to_sign = (url.path + "?" + url.query).encode('utf-8') + + # Decode the private key into its binary format + # We need to decode the URL-encoded private key + decoded_key = base64.urlsafe_b64decode(client_secret) + + # Create a signature using the private key and the URL-encoded + # string using HMAC SHA1. This signature will be binary. + signature = hmac.new(decoded_key, url_to_sign, hashlib.sha1) + + # Encode the binary signature into base64 for use within a URL + encoded_signature = base64.urlsafe_b64encode(signature.digest()) + + # Return signature (to be appended as a 'signature' in params) + return encoded_signature + + def rate_limited_get(self, *args, **kwargs): + if not self.rate_limit: + return super(GoogleQuery, self).rate_limited_get(*args, **kwargs) + elif self.client and self.client_secret: + return self.rate_limited_get_for_work(*args, **kwargs) + else: + return self.rate_limited_get_for_dev(*args, **kwargs) + + @ratelim.greedy(2500, 60 * 60 * 24) + @ratelim.greedy(10, 1) + def rate_limited_get_for_dev(self, *args, **kwargs): + return super(GoogleQuery, self).rate_limited_get(*args, **kwargs) + + @ratelim.greedy(100000, 60 * 60 * 24) # Google for Work daily limit + @ratelim.greedy(50, 1) # Google for Work limit per second + def rate_limited_get_for_work(self, *args, **kwargs): + return super(GoogleQuery, self).rate_limited_get(*args, **kwargs) + + def _catch_errors(self, json_response): + status = json_response.get('status') + if not status == 'OK': + self.error = status + + return self.error + + def _adapt_results(self, json_response): + return json_response.get('results', []) + + +if __name__ == '__main__': + g = GoogleQuery('11 Wall Street, New York') + g.debug() diff --git a/geocoder/google.py:Zone.Identifier b/geocoder/google.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/google.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/google_elevation.py b/geocoder/google_elevation.py new file mode 100644 index 0000000..d7bd4b6 --- /dev/null +++ b/geocoder/google_elevation.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import google_key +from geocoder.location import Location + + +class ElevationResult(OneResult): + + @property + def status(self): + if self.elevation: + return 'OK' + else: + return 'ERROR - No Elevation found' + + @property + def ok(self): + return bool(self.elevation) + + @property + def meters(self): + if self.elevation: + return round(self.elevation, 1) + + @property + def feet(self): + if self.elevation: + return round(self.elevation * 3.28084, 1) + + @property + def elevation(self): + return self.raw.get('elevation') + + @property + def resolution(self): + return self.raw.get('resolution') + + +class ElevationQuery(MultipleResultsQuery): + """ + Google Elevation API + ==================== + The Elevation API provides elevation data for all locations on the surface of the + earth, including depth locations on the ocean floor (which return negative values). + In those cases where Google does not possess exact elevation measurements at the + precise location you request, the service will interpolate and return an averaged + value using the four nearest locations. + + API Reference + ------------- + https://developers.google.com/maps/documentation/elevation/ + """ + provider = 'google' + method = 'elevation' + + _URL = 'https://maps.googleapis.com/maps/api/elevation/json' + _RESULT_CLASS = ElevationResult + _KEY = google_key + + def _build_params(self, location, provider_key, **kwargs): + return { + 'locations': str(Location(location)), + } + + def _adapt_results(self, json_response): + return json_response['results'] + + +if __name__ == '__main__': + g = ElevationQuery([45.123, -76.123]) + g.debug() diff --git a/geocoder/google_elevation.py:Zone.Identifier b/geocoder/google_elevation.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/google_elevation.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/google_places.py b/geocoder/google_places.py new file mode 100644 index 0000000..9844872 --- /dev/null +++ b/geocoder/google_places.py @@ -0,0 +1,181 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import google_key +from geocoder.location import BBox + +# todo: Paging (pagetoken) is not fully supported since we only return the first result. Need to return all results to the user so paging will make sense +# todo: Add support for missing results fields html_attributions, opening_hours, photos, scope, alt_ids, types [not just the first one] +# todo: Add support for nearbysearch and radarsearch variations of the Google Places API + + +class PlacesResult(OneResult): + + def __init__(self, json_content): + # flatten geometry + geometry = json_content.get('geometry', {}) + self._location = geometry.get('location', {}) + json_content['northeast'] = geometry.get( + 'viewport', {}).get('northeast', {}) + json_content['southwest'] = geometry.get( + 'viewport', {}).get('southwest', {}) + + # proceed with super.__init__ + super(PlacesResult, self).__init__(json_content) + + @property + def lat(self): + return self._location.get('lat') + + @property + def lng(self): + return self._location.get('lng') + + @property + def id(self): + return self.raw.get('id') + + @property + def reference(self): + return self.raw.get('reference') + + @property + def place_id(self): + return self.raw.get('place_id') + + @property + def type(self): + type = self.raw.get('types') + if type: + return type[0] + + @property + def address(self): + return self.raw.get('formatted_address') + + @property + def icon(self): + return self.raw.get('icon') + + @property + def name(self): + return self.raw.get('name') + + @property + def vicinity(self): + return self.raw.get('vicinity') + + @property + def price_level(self): + return self.raw.get('price_level') + + @property + def rating(self): + return self.raw.get('rating') + + +class PlacesQuery(MultipleResultsQuery): + """ + Google Places API + ==================== + The Google Places API Web Service allows you to query for place information on a variety of categories, + such as: establishments, prominent points of interest, geographic locations, and more. + You can search for places either by proximity or a text string. + A Place Search returns a list of places along with summary information about each place; additional + information is available via a Place Details query. + + At this time, only the "Text Search" is supported by this library. "Text Search" can be used + when you don't have pristine formatted addresses required by the regular Google Maps Geocoding API + or when you want to do 'nearby' searches like 'restaurants near Sydney'. + + The Geocoding best practices reference indicates that when you have 'ambiguous queries in an automated system + you would be better served using the Places API Text Search than the Maps Geocoding API + https://developers.google.com/maps/documentation/geocoding/best-practices + + API Reference + ------------- + https://developers.google.com/places/web-service/intro + https://developers.google.com/places/web-service/search + + l = geocoder.google('Elm Plaza Shopping Center, Enfield, CT 06082', method='places') + l = geocoder.google('food near white house', method='places') + l = geocoder.google('1st and main', method='places') + + Parameters + ---------- + :param location: Your search location or phrase you want geocoded. + :param key: Your Google developers free key. + + :param proximity: (optional) lat,lng point around which results will be given preference + :param radius: (optional) in meters, used with proximity + :param language: (optional) 2-letter code of preferred language of returned address elements. + :param minprice: (optional) 0 (most affordable) to 4 (most expensive) + :param maxprice: (optional) 0 (most affordable) to 4 (most expensive) + :param opennow: (optional) value is ignored. when present, closed places and places without opening hours will be omitted + :param pagetoken: (optional) get next 20 results from previously run search. when set, other criteria are ignored + :param type: (optional) restrict results to one type of place + """ + provider = 'google' + method = 'places' + + _URL = 'https://maps.googleapis.com/maps/api/place/textsearch/json' + _RESULT_CLASS = PlacesResult + _KEY = google_key + + def __init__(self, location, **kwargs): + super(PlacesQuery, self).__init__(location, **kwargs) + + self.next_page_token = None + + def _build_params(self, location, provider_key, **kwargs): + # handle specific case of proximity (aka 'location' for google) + bbox = kwargs.get('proximity', '') + if bbox: + bbox = BBox.factory(bbox) + # do not forget to convert bbox to google expectations... + bbox = bbox.latlng + + # define all + params = { + # required + 'query': location, + 'key': provider_key, + + # optional + 'location': bbox, + 'radius': kwargs.get('radius', ''), + 'language': kwargs.get('language', ''), + 'minprice': kwargs.get('minprice', ''), + 'maxprice': kwargs.get('maxprice', ''), + 'type': kwargs.get('type', ''), + } + + # optional, don't send unless needed + if 'opennow' in kwargs: + params['opennow'] = '' + + # optional, don't send unless needed + if 'pagetoken' in kwargs: + params['pagetoken'] = kwargs['pagetoken'] + + return params + + def _parse_results(self, json_response): + super(PlacesQuery, self)._parse_results(json_response) + + # store page token if any + self.next_page_token = json_response.get('next_page_token') + + def _adapt_results(self, json_response): + return json_response['results'] + + @property + def query(self): + return self.location + + +if __name__ == '__main__': + g = PlacesQuery('rail station, Ottawa') + g.debug() diff --git a/geocoder/google_places.py:Zone.Identifier b/geocoder/google_places.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/google_places.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/google_reverse.py b/geocoder/google_reverse.py new file mode 100644 index 0000000..606c83b --- /dev/null +++ b/geocoder/google_reverse.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.google import GoogleResult, GoogleQuery +from geocoder.location import Location + + +class GoogleReverseResult(GoogleResult): + + @property + def ok(self): + return bool(self.address) + + +class GoogleReverse(GoogleQuery): + """ + Google Geocoding API + ==================== + Geocoding is the process of converting addresses (like "1600 Amphitheatre + Parkway, Mountain View, CA") into geographic coordinates (like latitude + 37.423021 and longitude -122.083739), which you can use to place markers or + position the map. + + API Reference + ------------- + https://developers.google.com/maps/documentation/geocoding/ + """ + provider = 'google' + method = 'reverse' + + def _location_init(self, location, **kwargs): + return { + 'latlng': str(Location(location)), + 'sensor': 'false', + } + + +if __name__ == '__main__': + g = GoogleReverse((45.4215296, -75.6971930)) + g.debug() diff --git a/geocoder/google_reverse.py:Zone.Identifier b/geocoder/google_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/google_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/google_timezone.py b/geocoder/google_timezone.py new file mode 100644 index 0000000..a54d813 --- /dev/null +++ b/geocoder/google_timezone.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +import time +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import google_key +from geocoder.location import Location + + +class TimezoneResult(OneResult): + + def __repr__(self): + return u'<[{}] [{}]>'.format(self.status, self.timeZoneName) + + @property + def ok(self): + return bool(self.timeZoneName) + + @property + def timeZoneId(self): + return self.raw.get('timeZoneId') + + @property + def timeZoneName(self): + return self.raw.get('timeZoneName') + + @property + def rawOffset(self): + return self.raw.get('rawOffset') + + @property + def dstOffset(self): + return self.raw.get('dstOffset') + + +class TimezoneQuery(MultipleResultsQuery): + """ + Google Time Zone API + ==================== + The Time Zone API provides time offset data for locations on the surface of the earth. + Requesting the time zone information for a specific Latitude/Longitude pair will + return the name of that time zone, the time offset from UTC, and the Daylight Savings offset. + + API Reference + ------------- + https://developers.google.com/maps/documentation/timezone/ + """ + provider = 'google' + method = 'timezone' + + _URL = 'https://maps.googleapis.com/maps/api/timezone/json' + _RESULT_CLASS = TimezoneResult + _KEY = google_key + + def _build_params(self, location, provider_key, **kwargs): + return { + 'location': str(Location(location)), + 'timestamp': kwargs.get('timestamp', time.time()), + } + + def _adapt_results(self, json_response): + return [json_response] + + +if __name__ == '__main__': + g = TimezoneQuery([45.5375801, -75.2465979]) + g.debug() diff --git a/geocoder/google_timezone.py:Zone.Identifier b/geocoder/google_timezone.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/google_timezone.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/here.py b/geocoder/here.py new file mode 100644 index 0000000..8ac74f5 --- /dev/null +++ b/geocoder/here.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import here_app_id, here_app_code + +from geocoder.location import BBox + + +class HereResult(OneResult): + + def __init__(self, json_content): + self._display_position = json_content.get('DisplayPosition', {}) + self._address = json_content.get('Address', {}) + self._mapview = json_content.get('MapView', {}) + + for item in json_content['Address']['AdditionalData']: + json_content[item['key']] = item['value'] + + super(HereResult, self).__init__(json_content) + + @property + def lat(self): + return self._display_position.get('Latitude') + + @property + def lng(self): + return self._display_position.get('Longitude') + + @property + def address(self): + return self._address.get('Label') + + @property + def postal(self): + return self._address.get('PostalCode') + + @property + def housenumber(self): + return self._address.get('HouseNumber') + + @property + def street(self): + return self._address.get('Street') + + @property + def neighborhood(self): + return self.district + + @property + def district(self): + return self._address.get('District') + + @property + def city(self): + return self._address.get('City') + + @property + def county(self): + return self._address.get('County') + + @property + def state(self): + return self._address.get('State') + + @property + def country(self): + return self._address.get('Country') + + @property + def quality(self): + return self.raw.get('MatchLevel') + + @property + def accuracy(self): + return self.raw.get('MatchType') + + @property + def bbox(self): + south = self._mapview['BottomRight'].get('Latitude') + north = self._mapview['TopLeft'].get('Latitude') + west = self._mapview['TopLeft'].get('Longitude') + east = self._mapview['BottomRight'].get('Longitude') + return self._get_bbox(south, west, north, east) + + +class HereQuery(MultipleResultsQuery): + """ + HERE Geocoding REST API + ======================= + Send a request to the geocode endpoint to find an address + using a combination of country, state, county, city, + postal code, district, street and house number. + + API Reference + ------------- + https://developer.here.com/rest-apis/documentation/geocoder + """ + provider = 'here' + method = 'geocode' + qualified_address = ['city', 'district', 'postal', 'state', 'country'] + + _URL = 'http://geocoder.cit.api.here.com/6.2/geocode.json' + _RESULT_CLASS = HereResult + + @classmethod + def _get_api_key(cls, key=None): + # API key is split between app_id and app_code -> managed in _build_params + pass + + def _build_params(self, location, provider_key, **kwargs): + # HERE Credentials + app_id = kwargs.get('app_id', here_app_id) + app_code = kwargs.get('app_code', here_app_code) + if not bool(app_id and app_code): + raise ValueError("Provide app_id & app_code") + + # URL Params + params = { + 'searchtext': location, + 'app_id': app_id, + 'app_code': app_code, + 'gen': 9, + 'maxresults': kwargs.get('maxRows', 1), + 'language': kwargs.get('language', 'en'), + } + + # bounding box if present + bbox = kwargs.get('bbox') + if bbox: + bbox = BBox(bbox=bbox) + # do not forget to convert bbox to mapbox expectations... + params['bbox'] = u'{north},{west};{south},{east}'.format( + west=bbox.west, + east=bbox.east, + south=bbox.south, + north=bbox.north + ) + + for value in self.qualified_address: + if kwargs.get(value) is not None: + params[value] = kwargs.get(value) + + return params + + def _catch_errors(self, json_response): + status = json_response.get('type') + if not status == 'OK': + self.error = status + + return self.error + + def _adapt_results(self, json_response): + # Build intial Tree with results + return [item['Location'] + for item in json_response['Response']['View'][0]['Result']] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = HereQuery("New York City") + g.debug() diff --git a/geocoder/here.py:Zone.Identifier b/geocoder/here.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/here.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/here_reverse.py b/geocoder/here_reverse.py new file mode 100644 index 0000000..e23c36d --- /dev/null +++ b/geocoder/here_reverse.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +from geocoder.location import Location +from geocoder.here import HereResult, HereQuery + + +class HereReverseResult(HereResult): + + @property + def ok(self): + return bool(self.address) + + +class HereReverse(HereQuery): + """ + HERE Geocoding REST API + ======================= + Send a request to the geocode endpoint to find an address + using a combination of country, state, county, city, + postal code, district, street and house number. + + API Reference + ------------- + https://developer.here.com/rest-apis/documentation/geocoder + """ + provider = 'here' + method = 'reverse' + + _RESULT_CLASS = HereReverseResult + _URL = 'http://reverse.geocoder.cit.api.here.com/6.2/reversegeocode.json' + + def _build_params(self, location, provider_key, **kwargs): + params = super(HereReverse, self)._build_params(location, provider_key, **kwargs) + del params['searchtext'] + + location = str(Location(location)) + params.update({ + 'prox': location, + 'mode': 'retrieveAddresses', + 'gen': 8, + }) + return params + + +if __name__ == '__main__': + g = HereReverse([45.4049053, -75.7077965]) + g.debug() diff --git a/geocoder/here_reverse.py:Zone.Identifier b/geocoder/here_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/here_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/ipinfo.py b/geocoder/ipinfo.py new file mode 100644 index 0000000..3cc5095 --- /dev/null +++ b/geocoder/ipinfo.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.location import Location + + +class IpinfoResult(OneResult): + + @property + def lat(self): + loc = self.raw.get('loc') + if loc: + return Location(loc).lat + + @property + def lng(self): + loc = self.raw.get('loc') + if loc: + return Location(loc).lng + + @property + def address(self): + if self.city: + return u'{0}, {1}, {2}'.format(self.city, self.state, self.country) + elif self.state: + return u'{0}, {1}'.format(self.state, self.country) + elif self.country: + return u'{0}'.format(self.country) + else: + return u'' + + @property + def postal(self): + return self.raw.get('postal') + + @property + def city(self): + return self.raw.get('city') + + @property + def state(self): + return self.raw.get('region') + + @property + def country(self): + return self.raw.get('country') + + @property + def hostname(self): + return self.raw.get('hostname') + + @property + def ip(self): + return self.raw.get('ip') + + @property + def org(self): + return self.raw.get('org') + + +class IpinfoQuery(MultipleResultsQuery): + """ + API Reference + ------------- + https://ipinfo.io + """ + provider = 'ipinfo' + method = 'geocode' + + _URL = 'http://ipinfo.io/json' + _RESULT_CLASS = IpinfoResult + _KEY_MANDATORY = False + + def _before_initialize(self, location, **kwargs): + if location.lower() == 'me' or location == '': + self.url = 'http://ipinfo.io/json' + else: + self.url = 'http://ipinfo.io/{0}/json'.format(self.location) + + def _adapt_results(self, json_response): + return [json_response] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = IpinfoQuery('8.8.8.8') + g.debug() diff --git a/geocoder/ipinfo.py:Zone.Identifier b/geocoder/ipinfo.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/ipinfo.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/keys.py b/geocoder/keys.py new file mode 100644 index 0000000..6d42c2d --- /dev/null +++ b/geocoder/keys.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# coding: utf8 +import os +import re +import requests + + +bing_key = os.environ.get('BING_API_KEY') +tomtom_key = os.environ.get('TOMTOM_API_KEY') +here_app_id = os.environ.get('HERE_APP_ID') +here_app_code = os.environ.get('HERE_APP_CODE') +geonames_username = os.environ.get('GEONAMES_USERNAME') +opencage_key = os.environ.get('OPENCAGE_API_KEY') +mapquest_key = os.environ.get('MAPQUEST_API_KEY') +baidu_key = os.environ.get('BAIDU_API_KEY') +baidu_security_key = os.environ.get('BAIDU_SECURITY_KEY') +gaode_key = os.environ.get('GAODE_API_KEY') +w3w_key = os.environ.get('W3W_API_KEY') +mapbox_access_token = os.environ.get('MAPBOX_ACCESS_TOKEN') +google_key = os.environ.get('GOOGLE_API_KEY') +google_client = os.environ.get('GOOGLE_CLIENT') +google_client_secret = os.environ.get('GOOGLE_CLIENT_SECRET') +mapzen_key = os.environ.get('MAPZEN_API_KEY') +tamu_key = os.environ.get('TAMU_API_KEY') +geocodefarm_key = os.environ.get('GEOCODEFARM_API_KEY') +tgos_key = os.environ.get('TGOS_API_KEY') +locationiq_key = os.environ.get('LOCATIONIQ_API_KEY') + + +class CanadapostKeyLazySingleton(object): + + CANADAPOST_KEY_REGEX = re.compile(r"'(....-....-....-....)';") + + def __init__(self): + self._key = None + + def __call__(self, **kwargs): + if self._key is None: + self._key = self.retrieve_key(**kwargs) + return self._key + + @classmethod + def retrieve_key(cls, **kwargs): + # get key with traditionnal mechanism + key = kwargs.get('key') + canadapost_key = os.environ.get('CANADAPOST_API_KEY') + if key or canadapost_key: + return key if key else canadapost_key + + # fallback + try: + url = 'http://www.canadapost.ca/cpo/mc/personal/postalcode/fpc.jsf' + timeout = kwargs.get('timeout', 5.0) + proxies = kwargs.get('proxies', '') + r = requests.get(url, timeout=timeout, proxies=proxies) + match = cls.CANADAPOST_KEY_REGEX.search(r.text) + if match: + return match.group(1) + else: + raise ValueError('No API Key found') + except Exception as err: + raise ValueError('Could not retrieve API Key: %s' % err) + + +canadapost_key_getter = CanadapostKeyLazySingleton() diff --git a/geocoder/keys.py:Zone.Identifier b/geocoder/keys.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/keys.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/komoot.py b/geocoder/komoot.py new file mode 100644 index 0000000..4a647d4 --- /dev/null +++ b/geocoder/komoot.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.location import BBox +from geocoder.base import OneResult, MultipleResultsQuery + + +class KomootResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._geometry = json_content.get('geometry', {}) + self._properties = json_content.get('properties', {}) + + # proceed with super.__init__ + super(KomootResult, self).__init__(json_content) + + @property + def lat(self): + return self._geometry['coordinates'][1] + + @property + def lng(self): + return self._geometry['coordinates'][0] + + @property + def bbox(self): + extent = self._properties.get('extent') + if extent and all(extent): + west = extent[0] + north = extent[1] + east = extent[2] + south = extent[3] + return BBox.factory([south, west, north, east]).as_dict + + @property + def address(self): + # Ontario, Canada + address = ', '.join([self.state, self.country]) + + # 453 Booth street, Ottawa ON, Canada + if self.housenumber: + middle = ', '.join([self.street, self.city]) + address = ' '.join([self.housenumber, middle, address]) + + # 453 Booth street, Ottawa ON, Canada + elif self.street: + middle = ', '.join([self.street, self.city]) + address = ' '.join([middle, address]) + + # Ottawa ON, Canada + elif self.city: + address = ' '.join([self.city, address]) + + return address + + @property + def country(self): + return self._properties.get('country', '') + + @property + def state(self): + if self.osm_value == 'state': + return self._properties.get('name', '') + return self._properties.get('state', '') + + @property + def city(self): + if self.osm_value == 'city': + return self._properties.get('name', '') + return self._properties.get('city', '') + + @property + def street(self): + return self._properties.get('street', '') + + @property + def housenumber(self): + return self._properties.get('housenumber', '') + + @property + def postal(self): + return self._properties.get('postcode', '') + + @property + def osm_id(self): + return self._properties.get('osm_id', '') + + @property + def osm_value(self): + return self._properties.get('osm_value', '') + + @property + def osm_key(self): + return self._properties.get('osm_key', '') + + @property + def osm_type(self): + return self._properties.get('osm_type', '') + + +class KomootQuery(MultipleResultsQuery): + """ + Komoot REST API + ======================= + + API Reference + ------------- + http://photon.komoot.de + """ + provider = 'komoot' + method = 'geocode' + + _URL = 'http://photon.komoot.de/api' + _RESULT_CLASS = KomootResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + return { + 'q': location, + 'limit': kwargs.get('maxRows', 1), + 'lang': 'en', + } + + def _adapt_results(self, json_response): + return json_response['features'] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = KomootQuery('Ottawa Ontario', maxRows=3) + g.debug() diff --git a/geocoder/komoot.py:Zone.Identifier b/geocoder/komoot.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/komoot.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/komoot_reverse.py b/geocoder/komoot_reverse.py new file mode 100644 index 0000000..70574b4 --- /dev/null +++ b/geocoder/komoot_reverse.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.location import Location +from geocoder.komoot import KomootResult, KomootQuery + + +class KomootReverseResult(KomootResult): + + @property + def ok(self): + return bool(self.address) + + +class KomootReverse(KomootQuery): + """ + Komoot REST API + ======================= + + API Reference + ------------- + http://photon.komoot.de + """ + provider = 'komoot' + method = 'reverse' + + _URL = 'https://photon.komoot.de/reverse' + _RESULT_CLASS = KomootReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'lat': location.lat, + 'lon': location.lng, + } + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = KomootReverse("45.4 -75.7") + g.debug() diff --git a/geocoder/komoot_reverse.py:Zone.Identifier b/geocoder/komoot_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/komoot_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/location.py b/geocoder/location.py new file mode 100644 index 0000000..e1beccf --- /dev/null +++ b/geocoder/location.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# coding: utf8 + +import re +import geocoder +from six import string_types +try: + from statistics import mean +except ImportError: + def mean(args): + return sum(args) / len(args) + + +class Location(object): + """ Location container """ + lat = None + lng = None + + def __init__(self, location, **kwargs): + self.location = location + self.kwargs = kwargs + self._check_input(location) + + @property + def ok(self): + return bool(self.latlng) + + @staticmethod + def _convert_float(number): + try: + return float(number) + except ValueError: + return None + + def _check_input(self, location): + # Checking for a LatLng String + if isinstance(location, string_types): + expression = r"[-]?\d+[.]?[-]?[\d]+" + pattern = re.compile(expression) + match = pattern.findall(location) + if len(match) == 2: + lat, lng = match + self._check_for_list([lat, lng]) + else: + # Check for string to Geocode using a provider + provider = self.kwargs.get('provider', 'osm') + g = geocoder.get(location, provider=provider) + if g.ok: + self.lat, self.lng = g.lat, g.lng + + # Checking for List of Tuple + elif isinstance(location, (list, tuple)): + self._check_for_list(location) + + # Checking for Dictionary + elif isinstance(location, dict): + self._check_for_dict(location) + + # Checking for a Geocoder Class + elif hasattr(location, 'latlng'): + if location.latlng: + self.lat, self.lng = location.latlng + + # Result into Error + else: + raise ValueError("Unknown location: %s" % location) + + def _check_for_list(self, location): + # Standard LatLng list or tuple with 2 number values + if len(location) == 2: + lat = self._convert_float(location[0]) + lng = self._convert_float(location[1]) + condition_1 = isinstance(lat, float) + condition_2 = isinstance(lng, float) + + # Check if input are Floats + if condition_1 and condition_2: + condition_3 = -90 <= lat <= 90 + condition_4 = -180 <= lng <= 180 + + # Check if inputs are within the World Geographical + # boundary (90,180,-90,-180) + if condition_3 and condition_4: + self.lat = lat + self.lng = lng + return self.lat, self.lng + else: + raise ValueError( + "Coords are not within the world's geographical boundary") + else: + raise ValueError("Coordinates must be numbers") + + def _check_for_dict(self, location): + # Standard LatLng list or tuple with 2 number values + if 'lat' in location and 'lng' in location: + lat = location['lat'] + lng = location['lng'] + self._check_for_list([lat, lng]) + + if 'y' in location and 'x' in location: + lat = location['y'] + lng = location['x'] + self._check_for_list([lat, lng]) + + @property + def latlng(self): + if isinstance(self.lat, float) and isinstance(self.lng, float): + return [self.lat, self.lng] + return [] + + @property + def latitude(self): + return self.lat + + @property + def longitude(self): + return self.lng + + @property + def xy(self): + if isinstance(self.lat, float) and isinstance(self.lng, float): + return [self.lng, self.lat] + return [] + + def __str__(self): + if self.ok: + return u'{0}, {1}'.format(self.lat, self.lng) + return u'' + + +class BBox(object): + """BBox container""" + + DEGREES_TOLERANCE = 0.5 + + @classmethod + def factory(cls, arg): + # validate input first + if not isinstance(arg, (list, dict)): + raise ValueError( + "BBox factory only accept a dict or a list as argument") + # we have a dict... just check which fields are given + if isinstance(arg, dict): + if 'southwest' in arg: + return cls(bounds=arg) + elif 'bbox' in arg: + return cls(bbox=arg['bbox']) + elif 'bounds' in arg: + return cls(bounds=arg['bounds']) + elif 'lat' in arg: + return cls(lat=arg['lat'], lng=arg['lng']) + elif 'west' in arg: + return cls(west=arg['west'], south=arg['south'], + east=arg['east'], north=arg['north']) + else: + raise ValueError( + "Could not found valid values in dict to create a bbox") + # we have a list... guess what to call according to the number of parameters given: + if len(arg) == 2: + lat, lng = arg + return cls(lat=lat, lng=lng) + elif len(arg) == 4: + return cls(bbox=arg) + else: + raise ValueError( + "Could not found valid values in list to create a bbox") + + def __init__(self, bbox=None, bounds=None, + lat=None, lng=None, + west=None, south=None, east=None, north=None): + if bounds is not None and bounds.get('southwest') and bounds.get('northeast'): + self.south, self.west = map(float, bounds['southwest']) + self.north, self.east = map(float, bounds['northeast']) + elif bbox is not None and all(bbox): + self.west, self.south, self.east, self.north = map(float, bbox) + elif lat is not None and lng is not None: + self.south = float(lat) - self.DEGREES_TOLERANCE + self.north = float(lat) + self.DEGREES_TOLERANCE + self.west = float(lng) - self.DEGREES_TOLERANCE + self.east = float(lng) + self.DEGREES_TOLERANCE + elif all([west, south, east, north]): + self.west, self.south, self.east, self.north = map( + float, [west, south, east, north]) + else: + raise ValueError("Could not create BBox/Bounds from given arguments") + + @property + def lat(self): + return mean([self.south, self.north]) + + @property + def lng(self): + return mean([self.west, self.east]) + + @property + def latlng(self): + if isinstance(self.lat, float) and isinstance(self.lng, float): + return [self.lat, self.lng] + return [] + + @property + def latitude(self): + return self.lat + + @property + def longitude(self): + return self.lng + + @property + def xy(self): + if isinstance(self.lat, float) and isinstance(self.lng, float): + return [self.lng, self.lat] + return [] + + @property + def as_dict(self): + return { + 'northeast': [self.north, self.east], + 'southwest': [self.south, self.west] + } + + +if __name__ == '__main__': + l = Location([0.0, 0.0]) + print(l.lng) diff --git a/geocoder/location.py:Zone.Identifier b/geocoder/location.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/location.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/locationiq.py b/geocoder/locationiq.py new file mode 100644 index 0000000..5755316 --- /dev/null +++ b/geocoder/locationiq.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging +import json + +from geocoder.osm import OsmResult, OsmQuery +from geocoder.keys import locationiq_key + + +class LocationIQResult(OsmResult): + pass + + +class LocationIQQuery(OsmQuery): + provider = 'locationiq' + method = 'geocode' + + _URL = 'https://locationiq.org/v1/search.php' + _RESULT_CLASS = LocationIQResult + _KEY = locationiq_key + _KEY_MANDATORY = True + + def _build_params(self, location, provider_key, **kwargs): + if 'limit' in kwargs: + kwargs['maxRows'] = kwargs['limit'] + return { + 'key': provider_key, + 'q': location, + 'format': 'json', + 'addressdetails': 1, + 'limit': kwargs.get('maxRows', 1), + } + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = LocationIQQuery('Ottawa, Ontario') + g.debug() + g = LocationIQQuery('Ottawa, Ontario', maxRows=5) + print(json.dumps(g.geojson, indent=4)) diff --git a/geocoder/locationiq.py:Zone.Identifier b/geocoder/locationiq.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/locationiq.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/locationiq_reverse.py b/geocoder/locationiq_reverse.py new file mode 100644 index 0000000..5f0e138 --- /dev/null +++ b/geocoder/locationiq_reverse.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.locationiq import LocationIQQuery + + +class LocationIQReverse(LocationIQQuery): + provider = 'locationiq' + method = 'reverse' + +if __name__ == '__main__': + g = LocationIQReverse("45.3, -75.4") + g.debug() diff --git a/geocoder/locationiq_reverse.py:Zone.Identifier b/geocoder/locationiq_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/locationiq_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/mapbox.py b/geocoder/mapbox.py new file mode 100644 index 0000000..9009533 --- /dev/null +++ b/geocoder/mapbox.py @@ -0,0 +1,151 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import mapbox_access_token +from geocoder.location import BBox, Location + + +class MapboxResult(OneResult): + + def __init__(self, json_content): + self._geometry = json_content.get('geometry', {}) + + for item in json_content.get('context', []): + if '.' in item['id']: + # attribute=country & text=Canada + attribute = item['id'].split('.')[0] + json_content[attribute] = item['text'] + + super(MapboxResult, self).__init__(json_content) + + @property + def lat(self): + coord = self._geometry['coordinates'] + if coord: + return coord[1] + + @property + def lng(self): + coord = self._geometry['coordinates'] + if coord: + return coord[0] + + @property + def address(self): + return self.raw.get('place_name') + + @property + def housenumber(self): + return self.raw.get('address') + + @property + def street(self): + return '' + + @property + def city(self): + return self.raw.get('place') + + @property + def state(self): + return self.raw.get('region') + + @property + def country(self): + return self.raw.get('country') + + @property + def postal(self): + return self.raw.get('postcode') + + @property + def accuracy(self): + if self.interpolated: + return "interpolated" + + @property + def quality(self): + return self.raw.get('relevance') + + @property + def interpolated(self): + return self._geometry.get('interpolated') + + @property + def bbox(self): + _bbox = self.raw.get('bbox') + if _bbox: + west = _bbox[0] + south = _bbox[1] + east = _bbox[2] + north = _bbox[3] + return self._get_bbox(south, west, north, east) + + +class MapboxQuery(MultipleResultsQuery): + """ + Mapbox Geocoding + ================ + The Mapbox Geocoding API lets you convert location text into + geographic coordinates (1600 Pennsylvania Ave NW → -77.0366,38.8971). + + API Reference + ------------- + https://www.mapbox.com/developers/api/geocoding/ + + Get Mapbox Access Token + ----------------------- + https://www.mapbox.com/account + """ + provider = 'mapbox' + method = 'geocode' + + _URL = u'https://api.mapbox.com/geocoding/v5/mapbox.places/{0}.json' + _RESULT_CLASS = MapboxResult + _KEY = mapbox_access_token + + def _build_params(self, location, provider_key, **kwargs): + base_params = { + 'access_token': provider_key, + 'country': kwargs.get('country'), + 'types': kwargs.get('types'), + } + # handle proximity + proximity = kwargs.get('proximity', None) + if proximity is not None: + proximity = Location(proximity) + # do not forget to convert bbox to mapbox expectations... + base_params['proximity'] = u'{longitude},{latitude}'.format( + longitude=proximity.longitude, + latitude=proximity.latitude + ) + + bbox = kwargs.get('bbox') + if bbox: + bbox = BBox(bbox=bbox) + # do not forget to convert bbox to mapbox expectations... + base_params['bbox'] = u'{west},{south},{east},{north}'.format( + west=bbox.west, + east=bbox.east, + south=bbox.south, + north=bbox.north + ) + + return base_params + + def _before_initialize(self, location, **kwargs): + self.url = self.url.format(location) + + def _adapt_results(self, json_response): + # extract the array of JSON objects + return json_response.get('features', []) + + +if __name__ == '__main__': + g = MapboxQuery("200 Queen Street", proximity=[45.3, -66.1]) + print(g.address) + g = MapboxQuery("200 Queen Street") + print(g.address) + g.debug() diff --git a/geocoder/mapbox.py:Zone.Identifier b/geocoder/mapbox.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/mapbox.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/mapbox_reverse.py b/geocoder/mapbox_reverse.py new file mode 100644 index 0000000..71ea9e0 --- /dev/null +++ b/geocoder/mapbox_reverse.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.mapbox import MapboxResult, MapboxQuery +from geocoder.location import Location + + +class MapboxReverseResult(MapboxResult): + + @property + def ok(self): + return bool(self.address) + + +class MapboxReverse(MapboxQuery): + """ + Mapbox Reverse Geocoding + ======================== + Reverse geocoding lets you reverse this process, turning a + pair of lat/lon coordinates into a meaningful place name + (-77.036,38.897 → 1600 Pennsylvania Ave NW). + + API Reference + ------------- + https://www.mapbox.com/developers/api/geocoding/ + + Get Mapbox Access Token + ----------------------- + https://www.mapbox.com/account + """ + provider = 'mapbox' + method = 'reverse' + + _URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/{lng},{lat}.json' + + def _build_params(self, location, provider_key, **kwargs): + return { + 'access_token': provider_key, + 'country': kwargs.get('country'), + 'types': kwargs.get('types'), + } + + def _before_initialize(self, location, **kwargs): + self.location = str(Location(location)) + lat, lng = Location(location).latlng + self.url = self.url.format(lng=lng, lat=lat) + + +if __name__ == '__main__': + g = MapboxReverse([45.4049053, -75.7077965]) + g.debug() diff --git a/geocoder/mapbox_reverse.py:Zone.Identifier b/geocoder/mapbox_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/mapbox_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/mapquest.py b/geocoder/mapquest.py new file mode 100644 index 0000000..f0e029c --- /dev/null +++ b/geocoder/mapquest.py @@ -0,0 +1,125 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import mapquest_key + +from geocoder.location import BBox + + +class MapquestResult(OneResult): + + @property + def lat(self): + return self.raw.get('latLng', {}).get('lat') + + @property + def lng(self): + return self.raw.get('latLng', {}).get('lng') + + @property + def street(self): + return self.raw.get('street') + + @property + def address(self): + if self.street: + return self.street + elif self.city: + return self.city + elif self.country: + return self.country + + @property + def quality(self): + return self.raw.get('geocodeQuality') + + @property + def postal(self): + return self.raw.get('postalCode') + + @property + def neighborhood(self): + return self.raw.get('adminArea6') + + @property + def city(self): + return self.raw.get('adminArea5') + + @property + def county(self): + return self.raw.get('adminArea4') + + @property + def state(self): + return self.raw.get('adminArea3') + + @property + def country(self): + return self.raw.get('adminArea1') + + +class MapquestQuery(MultipleResultsQuery): + """ + MapQuest + ======== + The geocoding service enables you to take an address and get the + associated latitude and longitude. You can also use any latitude + and longitude pair and get the associated address. Three types of + geocoding are offered: address, reverse, and batch. + + API Reference + ------------- + http://www.mapquestapi.com/geocoding/ + """ + provider = 'mapquest' + method = 'geocode' + + _URL = 'http://www.mapquestapi.com/geocoding/v1/address' + _RESULT_CLASS = MapquestResult + _KEY = mapquest_key + + def _build_headers(self, provider_key, **kwargs): + return { + 'referer': 'http://www.mapquestapi.com/geocoding/', + 'host': 'www.mapquestapi.com', + } + + def _build_params(self, location, provider_key, **kwargs): + params = { + 'key': provider_key, + 'location': location, + 'maxResults': kwargs.get("maxRows", 1), + 'outFormat': 'json', + } + + bbox = kwargs.get('bbox') + if bbox: + bbox = BBox(bbox=bbox) + params['boundingBox'] = u'{north},{west},{south},{east}'.format( + west=bbox.west, + east=bbox.east, + south=bbox.south, + north=bbox.north + ) + + return params + + def _catch_errors(self, json_response): + if b'The AppKey submitted with this request is invalid' in json_response: + self.error = 'MapQuest API Key invalid' + + return self.error + + def _adapt_results(self, json_response): + results = json_response.get('results', []) + if results: + return results[0]['locations'] + + return [] + + +if __name__ == '__main__': + g = MapquestQuery('Ottawa', maxRows=3) + g.debug() diff --git a/geocoder/mapquest.py:Zone.Identifier b/geocoder/mapquest.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/mapquest.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/mapquest_batch.py b/geocoder/mapquest_batch.py new file mode 100644 index 0000000..7ebbe0f --- /dev/null +++ b/geocoder/mapquest_batch.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.base import MultipleResultsQuery +from geocoder.mapquest import MapquestResult +from geocoder.keys import mapquest_key + + +class MapQuestBatchResult(MapquestResult): + + @property + def ok(self): + return bool(self.quality) + + +class MapquestBatch(MultipleResultsQuery): + """ + MapQuest + ======== + The geocoding service enables you to take an address and get the + associated latitude and longitude. You can also use any latitude + and longitude pair and get the associated address. Three types of + geocoding are offered: address, reverse, and batch. + + API Reference + ------------- + http://www.mapquestapi.com/geocoding/ + + """ + provider = 'mapquest' + method = 'batch' + + _RESULT_CLASS = MapQuestBatchResult + _URL = 'http://www.mapquestapi.com/geocoding/v1/batch' + _TIMEOUT = 30 + _KEY = mapquest_key + + def _build_params(self, location, provider_key, **kwargs): + self._TIMEOUT = kwargs.get('timeout', 30) + + return { + 'key': provider_key, + 'location': location, + 'maxResults': 1, + 'outFormat': 'json', + } + + def _adapt_results(self, json_response): + results = json_response.get('results', []) + if results: + return [result['locations'][0] for result in results] + + return [] + + +if __name__ == '__main__': + g = MapquestBatch(['Denver,CO', 'Boulder,CO']) + g.debug() diff --git a/geocoder/mapquest_batch.py:Zone.Identifier b/geocoder/mapquest_batch.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/mapquest_batch.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/mapquest_reverse.py b/geocoder/mapquest_reverse.py new file mode 100644 index 0000000..fd0b495 --- /dev/null +++ b/geocoder/mapquest_reverse.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.mapquest import MapquestResult, MapquestQuery +from geocoder.location import Location + + +class MapQuestReverseResult(MapquestResult): + + @property + def ok(self): + return bool(self.quality) + + +class MapquestReverse(MapquestQuery): + """ + MapQuest + ======== + The geocoding service enables you to take an address and get the + associated latitude and longitude. You can also use any latitude + and longitude pair and get the associated address. Three types of + geocoding are offered: address, reverse, and batch. + + API Reference + ------------- + http://www.mapquestapi.com/geocoding/ + + """ + provider = 'mapquest' + method = 'reverse' + + _URL = 'http://www.mapquestapi.com/geocoding/v1/reverse' + + def _build_params(self, location, provider_key, **kwargs): + return { + 'key': provider_key, + 'location': str(Location(location)), + 'maxResults': 1, + 'outFormat': 'json', + } + + +if __name__ == '__main__': + g = MapquestReverse([45.50, -76.05]) + g.debug() diff --git a/geocoder/mapquest_reverse.py:Zone.Identifier b/geocoder/mapquest_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/mapquest_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/mapzen.py b/geocoder/mapzen.py new file mode 100644 index 0000000..d0b7064 --- /dev/null +++ b/geocoder/mapzen.py @@ -0,0 +1,109 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.location import BBox +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import mapzen_key + + +class MapzenResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._geometry = json_content.get('geometry', {}) + self._properties = json_content.get('properties', {}) + + # proceed with super.__init__ + super(MapzenResult, self).__init__(json_content) + + @property + def lat(self): + return self._geometry['coordinates'][1] + + @property + def lng(self): + return self._geometry['coordinates'][0] + + @property + def bbox(self): + return BBox.factory(self.latlng).as_dict + + @property + def address(self): + return self._properties.get('label') + + @property + def housenumber(self): + return self._properties.get('housenumber') + + @property + def street(self): + return self._properties.get('street') + + @property + def neighbourhood(self): + return self._properties.get('neighbourhood') + + @property + def city(self): + return self._properties.get('locality') + + @property + def state(self): + return self._properties.get('region') + + @property + def country(self): + return self._properties.get('country') + + @property + def postal(self): + return self._properties.get('postalcode') + + @property + def gid(self): + return self._properties.get('gid') + + @property + def id(self): + return self._properties.get('id') + + +class MapzenQuery(MultipleResultsQuery): + """ + Mapzen REST API + ======================= + + API Reference + ------------- + https://mapzen.com/documentation/search/search/ + """ + provider = 'mapzen' + method = 'geocode' + + _URL = 'https://search.mapzen.com/v1/search' + _RESULT_CLASS = MapzenResult + _KEY = mapzen_key + + def __init__(self, *args, **kwargs): + raise DeprecationWarning('MapZen shut down as of January 2018: https://mapzen.com/blog/shutdown') + + def _build_params(self, location, provider_key, **kwargs): + return { + 'text': location, + 'api_key': provider_key, + 'size': kwargs.get('maxRows', 1) + } + + def _adapt_results(self, json_response): + return json_response['features'] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = MapzenQuery('201 Spear Street, San Francisco') + g.debug() diff --git a/geocoder/mapzen.py:Zone.Identifier b/geocoder/mapzen.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/mapzen.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/mapzen_reverse.py b/geocoder/mapzen_reverse.py new file mode 100644 index 0000000..41a694b --- /dev/null +++ b/geocoder/mapzen_reverse.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.mapzen import MapzenResult, MapzenQuery +from geocoder.location import Location + + +class MapzenReverseResult(MapzenResult): + + @property + def ok(self): + return bool(self.address) + + +class MapzenReverse(MapzenQuery): + """ + Mapzen REST API + ======================= + + API Reference + ------------- + https://mapzen.com/documentation/search/reverse/ + """ + provider = 'mapzen' + method = 'reverse' + + _URL = 'https://search.mapzen.com/v1/reverse' + _RESULT_CLASS = MapzenReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'point.lat': location.lat, + 'point.lon': location.lng, + 'size': kwargs.get('size', 1), + 'layers': kwargs.get('layers'), + 'source': kwargs.get('sources'), + 'boundary.country': kwargs.get('country'), + 'api_key': provider_key + } + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = MapzenReverse("45.4049053 -75.7077965", key='search-un1M9Hk') + g.debug() diff --git a/geocoder/mapzen_reverse.py:Zone.Identifier b/geocoder/mapzen_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/mapzen_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/maxmind.py b/geocoder/maxmind.py new file mode 100644 index 0000000..f11c740 --- /dev/null +++ b/geocoder/maxmind.py @@ -0,0 +1,140 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery + + +class MaxmindResults(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._location = json_content.get('location', {}) + self._traits = json_content.get('traits', {}) + + # proceed with super.__init__ + super(MaxmindResults, self).__init__(json_content) + + @property + def lat(self): + return self._location.get('latitude') + + @property + def lng(self): + return self._location.get('longitude') + + @property + def timezone(self): + return self._location.get('time_zone') + + @property + def metro_code(self): + return self._location.get('metro_code') + + @property + def domain(self): + return self._traits.get('domain') + + @property + def isp(self): + return self._traits.get('isp') + + @property + def organization(self): + return self._traits.get('organization') + + @property + def ip(self): + return self._traits.get('ip_address') + + @property + def postal(self): + return self.raw.get('postal', {}).get('code') + + @property + def city(self): + return self.raw.get('city', {}).get('names', {}).get('en') + + @property + def state(self): + return self.raw.get('subdivision', {}).get('names', {}).get('en') + + @property + def country(self): + return self.raw.get('country', {}).get('names', {}).get('en') + + @property + def country_code(self): + return self.raw.get('country', {}).get('iso_code') + + @property + def continent(self): + return self.raw.get('continent', {}).get('names', {}).get('en') + + @property + def continent_code(self): + return self.raw.get('continent', {}).get('code') + + @property + def address(self): + if self.city: + return u'{0}, {1}, {2}'.format(self.city, self.state, self.country) + elif self.state: + return u'{0}, {1}'.format(self.state, self.country) + elif self.country: + return u'{0}'.format(self.country) + else: + return u'' + + +class MaxmindQuery(MultipleResultsQuery): + """ + MaxMind's GeoIP2 + ======================= + MaxMind's GeoIP2 products enable you to identify the location, + organization, connection speed, and user type of your Internet + visitors. The GeoIP2 databases are among the most popular and + accurate IP geolocation databases available. + + API Reference + ------------- + https://www.maxmind.com/en/geolocation_landing + """ + provider = 'maxmind' + method = 'geocode' + + _URL = 'https://www.maxmind.com/geoip/v2.0/city_isp_org/{0}' + _RESULT_CLASS = MaxmindResults + _KEY_MANDATORY = False + + def _build_headers(self, provider_key, **kwargs): + return { + 'Referer': 'https://www.maxmind.com/en/geoip_demo', + 'Host': 'www.maxmind.com', + } + + def _build_params(self, location, provider_key, **kwargs): + return {'demo': 1} + + def _before_initialize(self, location, **kwargs): + location = location or 'me' + self.url = self._URL.format(location) + + def _catch_errors(self, json_response): + error = json_response.get('error') + if error: + self.error = json_response.get('code') + + return self.error + + def _adapt_results(self, json_response): + return [json_response] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = MaxmindQuery('8.8.8.8') + g.debug() diff --git a/geocoder/maxmind.py:Zone.Identifier b/geocoder/maxmind.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/maxmind.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/opencage.py b/geocoder/opencage.py new file mode 100644 index 0000000..98d14e5 --- /dev/null +++ b/geocoder/opencage.py @@ -0,0 +1,436 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.location import BBox +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import opencage_key + + +class OpenCageResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._geometry = json_content.get('geometry', {}) + self._components = json_content.get('components', {}) + self._annotations = json_content.get('annotations', {}) + self._bounds = json_content.get('bounds', {}) + + # proceed with super.__init__ + super(OpenCageResult, self).__init__(json_content) + + @property + def lat(self): + return self._geometry.get('lat') + + @property + def lng(self): + return self._geometry.get('lng') + + @property + def address(self): + return self.raw.get('formatted') + + @property + def housenumber(self): + return self._components.get('house_number') + + @property + def house_aliases(self): + house = self._components.get('house') + building = self._components.get('building') + public_building = self._components.get('public_building') + if house: # Priority can be rearranged + return house + elif building: + return building + elif public_building: + return public_building + + @property + def house(self): + house = self._components.get('house') + if house: + return house + else: + return self.house_aliases + + @property + def building(self): + building = self._components.get('building') + if building: + return building + else: + return self.house_aliases + + @property + def public_building(self): + public_building = self._components.get('public_building') + if public_building: + return public_building + else: + return self.house_aliases + + @property + def street_aliases(self): + street = self._components.get('street') + road = self._components.get('road') + footway = self._components.get('footway') + street_name = self._components.get('street_name') + residential = self._components.get('residential') + path = self._components.get('path') + pedestrian = self._components.get('pedestrian') + if street: + return street + elif road: + return road + elif footway: + return footway + elif street_name: + return street_name + elif residential: + return residential + elif path: + return path + elif pedestrian: + return pedestrian + + @property + def street(self): + street = self._components.get('street') + if street: + return street + else: + return self.street_aliases + + @property + def footway(self): + footway = self._components.get('footway') + if footway: + return footway + else: + return self.street_aliases + + @property + def road(self): + road = self._components.get('road') + if road: + return road + else: + return self.street_aliases + + @property + def street_name(self): + street_name = self._components.get('street_name') + if street_name: + return street_name + else: + return self.street_aliases + + @property + def residential(self): + residential = self._components.get('residential') + if residential: + return residential + else: + return self.street_aliases + + @property + def path(self): + path = self._components.get('path') + if path: + return path + else: + return self.street_aliases + + @property + def pedestrian(self): + pedestrian = self._components.get('pedestrian') + if pedestrian: + return pedestrian + else: + return self.street_aliases + + @property + def neighbourhood_aliases(self): + neighbourhood = self._components.get('neighbourhood') + suburb = self._components.get('suburb') + city_district = self._components.get('city_district') + if neighbourhood: # Priority can be rearranged + return neighbourhood + elif suburb: + return suburb + elif city_district: + return city_district + + @property + def neighbourhood(self): + neighbourhood = self._components.get('neighbourhood') + if neighbourhood: + return neighbourhood + else: + return self.neighbourhood_aliases + + @property + def suburb(self): + suburb = self._components.get('suburb') + if suburb: + return suburb + else: + return self.neighbourhood_aliases + + @property + def city_district(self): + city_district = self._components.get('city_district') + if city_district: + return city_district + else: + return self.neighbourhood_aliases + + @property + def city_aliases(self): + city = self._components.get('city') + town = self._components.get('town') + if city: # Priority can be rearranged + return city + elif town: + return town + else: # if nothing in city_aliases, then return village aliases + return self.village_aliases + + @property + def city(self): + city = self._components.get('city') + if city: + return city + else: + return self.city_aliases + + @property + def town(self): + town = self._components.get('town') + if town: + return town + else: + return self.city_aliases + + @property + def county(self): + return self._components.get('county') + + @property + def village_aliases(self): + village = self._components.get('village') + hamlet = self._components.get('hamlet') + locality = self._components.get('locality') + + if village: # Priority can be rearranged + return village + elif hamlet: + return hamlet + elif locality: + return locality + + @property + def village(self): + village = self._components.get('village') + if village: + return village + else: + return self.village_aliases + + @property + def hamlet(self): + hamlet = self._components.get('hamlet') + if hamlet: + return hamlet + else: + return self.village_aliases + + @property + def locality(self): + locality = self._components.get('locality') + if locality: + return locality + else: + return self.village_aliases + + @property + def state_aliases(self): + state = self._components.get('state') + province = self._components.get('province') + state_code = self._components.get('state_code') + + if state: # Priority can be rearranged + return state + elif province: + return province + elif state_code: + return state_code + + @property + def state(self): + state = self._components.get('state') + if state: + return state + else: + return self.state_aliases + + @property + def province(self): + province = self._components.get('province') + if province: + return province + else: + return self.state_aliases + + @property + def state_code(self): + state_code = self._components.get('state_code') + if state_code: + return state_code + else: + return self.state_aliases + + @property + def state_district(self): + return self._components.get('state_district') + + @property + def country(self): + country = self._components.get('country') + if country: + return country + else: + return self._components.get('country_name') + + @property + def country_code(self): + return self._components.get('country_code') + + @property + def postal(self): + return self._components.get('postcode') + + @property + def postcode(self): + return self._components.get('postcode') + + @property + def continent(self): + return self._components.get('continent') + + @property + def island(self): + return self._components.get('island') + + @property + def region(self): + return self._components.get('region') + + @property + def confidence(self): + return self.raw.get('confidence') + + @property + def w3w(self): + return self._annotations.get('what3words', {}).get('words') + + @property + def mgrs(self): + return self._annotations.get('MGRS') + + @property + def geohash(self): + return self._annotations.get('geohash') + + @property + def callingcode(self): + return self._annotations.get('callingcode') + + @property + def Maidenhead(self): + return self._annotations.get('Maidenhead') + + @property + def DMS(self): + return self._annotations.get('DMS') + + @property + def Mercator(self): + return self._annotations.get('Mercator') + + @property + def bbox(self): + south = self._bounds.get('southwest', {}).get('lat') + north = self._bounds.get('northeast', {}).get('lat') + west = self._bounds.get('southwest', {}).get('lng') + east = self._bounds.get('northeast', {}).get('lng') + if all([south, west, north, east]): + return BBox.factory([south, west, north, east]).as_dict + + +class OpenCageQuery(MultipleResultsQuery): + """ + OpenCage Geocoding Services + =========================== + OpenCage Geocoder simple, easy, and open geocoding for the entire world + Our API combines multiple geocoding systems in the background. + Each is optimized for different parts of the world and types of requests. + We aggregate the best results from open data sources and algorithms so you don't have to. + Each is optimized for different parts of the world and types of requests. + + API Reference + ------------- + https://geocoder.opencagedata.com/api + """ + provider = 'opencage' + method = 'geocode' + + _URL = 'http://api.opencagedata.com/geocode/v1/json' + _RESULT_CLASS = OpenCageResult + _KEY = opencage_key + + def _build_params(self, location, provider_key, **kwargs): + base_params = { + 'query': location, + 'key': provider_key, + 'limit': kwargs.get('maxRows', 1) + } + language = kwargs.get('language', None) + if language: + base_params['language'] = language + + return base_params + + def _catch_errors(self, json_response): + status = json_response.get('status') + if status and status.get('code') != 200: + self.status_code = status.get('code') + self.error = status.get('message') + + return self.error + + def _adapt_results(self, json_response): + # special license attribute + self.license = json_response['licenses'] + # Shows the limit and how many remaining calls you have on your + # API Key. Optional for paid OpenCage accounts + if json_response.get('rate'): + self.remaining_api_calls = json_response['rate']['remaining'] + self.limit_api_calls = json_response['rate']['limit'] + else: + self.remaining_api_calls = 999999 + self.limit_api_calls = 999999 + + # return geo results + return json_response['results'] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = OpenCageQuery('1552 Payette dr., Ottawa') + g.debug() diff --git a/geocoder/opencage.py:Zone.Identifier b/geocoder/opencage.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/opencage.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/opencage_reverse.py b/geocoder/opencage_reverse.py new file mode 100644 index 0000000..0cf3c83 --- /dev/null +++ b/geocoder/opencage_reverse.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.opencage import OpenCageResult, OpenCageQuery +from geocoder.location import Location + + +class OpenCageReverseResult(OpenCageResult): + + @property + def ok(self): + return bool(self.address) + + +class OpenCageReverse(OpenCageQuery): + """ + OpenCage Geocoding Services + =========================== + OpenCage Geocoder simple, easy, and open geocoding for the entire world + Our API combines multiple geocoding systems in the background. + Each is optimized for different parts of the world and types of requests. + We aggregate the best results from open data sources and algorithms so you don't have to. + Each is optimized for different parts of the world and types of requests. + + API Reference + ------------- + https://geocoder.opencagedata.com/api + """ + provider = 'opencage' + method = 'reverse' + + _URL = 'http://api.opencagedata.com/geocode/v1/json' + _RESULT_CLASS = OpenCageReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'query': location, + 'key': provider_key, + } + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = OpenCageReverse([45.4049053, -75.7077965]) + g.debug() diff --git a/geocoder/opencage_reverse.py:Zone.Identifier b/geocoder/opencage_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/opencage_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/osm.py b/geocoder/osm.py new file mode 100644 index 0000000..ddb750c --- /dev/null +++ b/geocoder/osm.py @@ -0,0 +1,396 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging +import json + +from geocoder.base import OneResult, MultipleResultsQuery + + +class OsmResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._address = json_content.get('address', {}) + + # proceed with super.__init__ + super(OsmResult, self).__init__(json_content) + + # ============================ # + # Geometry - Points & Polygons # + # ============================ # + + @property + def lat(self): + lat = self.raw.get('lat') + if lat: + return float(lat) + + @property + def lng(self): + lng = self.raw.get('lon') + if lng: + return float(lng) + + @property + def bbox(self): + _boundingbox = self.raw.get('boundingbox') + if _boundingbox: + south = float(_boundingbox[0]) + west = float(_boundingbox[2]) + north = float(_boundingbox[1]) + east = float(_boundingbox[3]) + return self._get_bbox(south, west, north, east) + + # ========================== # + # Tags for individual houses # + # ========================== # + + @property + def address(self): + return self.raw.get('display_name') + + @property + def housenumber(self): + return self._address.get('house_number') + + @property + def street(self): + return self._address.get('road') + + @property + def postal(self): + return self._address.get('postcode') + + # ============================ # + # Populated settlements, urban # + # ============================ # + + @property + def neighborhood(self): + """place=neighborhood + + A named part of a place=village, a place=town or a place=city. Smaller + than place=suburb and place=quarter. + + The tag can be used for any kind of landuse or mix of landuse (such as + residential, commercial, industrial etc). Usage of this term depends + greatly on local history, culture, politics, economy and organization + of settlements. More specific rules are intentionally avoided. + + Note: the British English spelling is used rather than the + American English spelling of neighborhood. + """ + return self._address.get('neighbourhood') + + @property + def suburb(self): + """place=suburb + + A distinct section of an urban settlement (city, town, etc.) with its + own name and identity. e.g. + + - annexed towns or villages which were formerly independent, + - independent (or dependent) municipalities within a city or next to a + much bigger town + - historical districts of settlements + - industrial districts or recreation areas within a settlements with + specific names. + """ + return self._address.get('suburb') + + @property + def quarter(self): + """place=quarter + + A named part of a bigger settlement where this part is smaller than + a suburb and bigger than a neighbourhood. This does not have to be + an administrative entity. + + The term quarter is sometimes used synonymously for neighbourhood. + """ + return self._address.get('quarter') + + # ====================================== # + # Populated settlements, urban and rural # + # ====================================== # + + @property + def allotments(self): + """place=allotments + + Dacha or cottage settlement, which is located outside other + inhabited locality. This value is used mainly in Russia and other + countries of the former Soviet Union, where a lot of such unofficial + settlements exist + """ + return self._address.get('hamlet') + + @property + def farm(self): + """place=farm + + A farm that has its own name. If the farm is not a part of bigger + settlement use place=isolated_dwelling. See also landuse=farmyard + """ + return self._address.get('hamlet') + + @property + def locality(self): + """place=isolated_dwelling + + For an unpopulated named place. + """ + return self._address.get('locality') + + @property + def isolated_dwelling(self): + """place=isolated_dwelling + + Smallest kind of human settlement. No more than 2 households. + """ + return self._address.get('hamlet') + + @property + def hamlet(self): + """place=hamlet + + A smaller rural community typically with less than 100-200 inhabitants, + few infrastructure. + """ + return self._address.get('hamlet') + + @property + def village(self): + """place=village + + A smaller distinct settlement, smaller than a town with few facilities + available with people traveling to nearby towns to access these. + Populations of villages vary widely in different territories but will + nearly always be less than 10,000 people, often a lot less. + + See place=neighbourhood on how to tag divisions within a larger village + """ + return self._address.get('village') + + @property + def town(self): + """place=town + + A second tier urban settlement of local importance, often with a + population of 10,000 people and good range of local facilities + including schools, medical facilities etc and traditionally a market. + In areas of low population, towns may have significantly + lower populations. + + See place=neighbourhood and possibly also place=suburb on how to tag + divisions within a town. + """ + return self._address.get('town') + + @property + def island(self): + """place=island + + Identifies the coastline of an island (> 1 km2), also consider + place=islet for very small islandsIdentifies the coastline of an + island (> 1 km2), also consider place=islet for very small islands + """ + return self._address.get('island') + + @property + def city(self): + """place=city + + The largest urban settlements in the territory, normally including the + national, state and provincial capitals. These are defined by charter + or other governmental designation in some territories and are a matter + of judgement in others. Should normally have a population of at + least 100,000 people and be larger than nearby towns. + + See place=suburb and place=neighbourhood on how to tag divisions + within a city. The outskirts of urban settlements may or may not match + the administratively declared boundary of the city. + """ + return self._address.get('city') + + # ================================ # + # Administratively declared places # + # ================================ # + + @property + def municipality(self): + """admin_level=8""" + return self._address.get('municipality') + + @property + def county(self): + """admin_level=6""" + return self._address.get('county') + + @property + def district(self): + """admin_level=5/6""" + return self._address.get('city_district') + + @property + def state(self): + """admin_level=4""" + return self._address.get('state') + + @property + def region(self): + """admin_level=3""" + return self._address.get('state') + + @property + def country(self): + """admin_level=2""" + return self._address.get('country') + + @property + def country_code(self): + """admin_level=2""" + return self._address.get('country_code') + + # ======================== # + # Quality Control & Others # + # ======================== # + + @property + def accuracy(self): + return self.importance + + @property + def quality(self): + return self.type + + @property + def population(self): + return self.raw.get('population') + + @property + def license(self): + return self.raw.get('license') + + @property + def type(self): + return self.raw.get('type') + + @property + def importance(self): + return self.raw.get('importance') + + @property + def icon(self): + return self.raw.get('icon') + + @property + def osm_type(self): + return self.raw.get('osm_type') + + @property + def osm_id(self): + return self.raw.get('osm_id') + + @property + def place_id(self): + return self.raw.get('place_id') + + @property + def place_rank(self): + return self.raw.get('place_rank') + + +class OsmQuery(MultipleResultsQuery): + """ + Nominatim + ========= + Nominatim (from the Latin, 'by name') is a tool to search OSM data by name + and address and to generate synthetic addresses of OSM points (reverse geocoding). + + API Reference + ------------- + http://wiki.openstreetmap.org/wiki/Nominatim + """ + provider = 'osm' + method = 'geocode' + + _URL = 'https://nominatim.openstreetmap.org/search' + _RESULT_CLASS = OsmResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + # backward compatitibility for 'limit' (now maxRows) + if 'limit' in kwargs: + logging.warning( + "argument 'limit' in OSM is deprecated and should be replaced with maxRows") + kwargs['maxRows'] = kwargs['limit'] + # build params + return { + 'q': location, + 'format': 'jsonv2', + 'addressdetails': 1, + 'limit': kwargs.get('maxRows', 1), + } + + def _before_initialize(self, location, **kwargs): + """ Check if specific URL has not been provided, otherwise, use cls._URL""" + url = kwargs.get('url', '') + if url.lower() == 'localhost': + self.url = 'http://localhost/nominatim/search' + elif url: + self.url = url + # else: do not change self.url, which is cls._URL + + +class OsmQueryDetail(MultipleResultsQuery): + """ + Nominatim + ========= + Nominatim (from the Latin, 'by name') is a tool to search OSM data by name + and address and to generate synthetic addresses of OSM points (reverse geocoding). + + API Reference + ------------- + http://wiki.openstreetmap.org/wiki/Nominatim + """ + provider = 'osm' + method = 'details' + + _URL = 'https://nominatim.openstreetmap.org/search' + _RESULT_CLASS = OsmResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + # backward compatitibility for 'limit' (now maxRows) + if 'limit' in kwargs: + logging.warning( + "argument 'limit' in OSM is deprecated and should be replaced with maxRows") + kwargs['maxRows'] = kwargs['limit'] + # build params + query = { + 'format': 'jsonv2', + 'addressdetails': 1, + 'limit': kwargs.get('maxRows', 1), + } + query.update(kwargs) + return query + + def _before_initialize(self, location, **kwargs): + """ Check if specific URL has not been provided, otherwise, use cls._URL""" + url = kwargs.get('url', '') + if url.lower() == 'localhost': + self.url = 'http://localhost/nominatim/search' + elif url: + self.url = url + # else: do not change self.url, which is cls._URL + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = OsmQuery('Ottawa, Ontario') + g.debug() + g = OsmQuery('Ottawa, Ontario', maxRows=5) + print(json.dumps(g.geojson, indent=4)) diff --git a/geocoder/osm.py:Zone.Identifier b/geocoder/osm.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/osm.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/osm_reverse.py b/geocoder/osm_reverse.py new file mode 100644 index 0000000..b0ef4bc --- /dev/null +++ b/geocoder/osm_reverse.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import +from geocoder.osm import OsmQuery +from geocoder.location import Location + + +class OsmReverse(OsmQuery): + """ + Nominatim + ========= + Nominatim (from the Latin, 'by name') is a tool to search OSM data by name + and address and to generate synthetic addresses of OSM points (reverse geocoding). + + API Reference + ------------- + http://wiki.openstreetmap.org/wiki/Nominatim + """ + provider = 'osm' + method = 'reverse' + + def _build_params(self, location, provider_key, **kwargs): + params = { + 'q': str(Location(location)), + 'format': 'jsonv2', + 'addressdetails': 1, + 'limit': kwargs.get('limit', 1) + } + if('lang_code' in kwargs): + params['accept-language'] = kwargs.get('lang_code') + return params + + +if __name__ == '__main__': + g = OsmReverse("45.3, -75.4") + g.debug() diff --git a/geocoder/osm_reverse.py:Zone.Identifier b/geocoder/osm_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/osm_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/ottawa.py b/geocoder/ottawa.py new file mode 100644 index 0000000..a699cd3 --- /dev/null +++ b/geocoder/ottawa.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging +import re + +from geocoder.base import OneResult, MultipleResultsQuery + + +class OttawaResult(OneResult): + + @property + def lat(self): + return self.raw.get('location', {}).get('y') + + @property + def lng(self): + return self.raw.get('location', {}).get('x') + + @property + def postal(self): + if self.address: + expression = r'([ABCEGHJKLMNPRSTVXY]{1}\d{1}[A-Z]{1}( *\d{1}[A-Z]{1}\d{1})?)' + pattern = re.compile(expression) + match = pattern.search(self.address.upper()) + if match: + return match.group(0) + + @property + def housenumber(self): + if self.address: + expression = r'\d+' + pattern = re.compile(expression) + match = pattern.search(self.address) + if match: + return int(match.group(0)) + + @property + def city(self): + return 'Ottawa' + + @property + def state(self): + return 'Ontario' + + @property + def country(self): + return 'Canada' + + @property + def address(self): + return self.raw.get('address') + + @property + def accuracy(self): + return self.raw.get('score') + + +class OttawaQuery(MultipleResultsQuery): + """ + Ottawa ArcGIS REST Services + =========================== + Geocoding is the process of assigning a location, usually in the form of + coordinate values (points), to an address by comparing the descriptive + location elements in the address to those present in the reference + material. Addresses come in many forms, ranging from the common address + format of a house number followed by the street name and succeeding + information to other location descriptions such as postal zone or census + tract. An address includes any type of information that distinguishes + a place. + + API Reference + ------------- + http://maps.ottawa.ca/ArcGIS/rest/services/compositeLocator/GeocodeServer/findAddressCandidates + """ + provider = 'ottawa' + method = 'geocode' + + _URL = 'http://maps.ottawa.ca/ArcGIS/rest/services/compositeLocator/GeocodeServer/findAddressCandidates' + _RESULT_CLASS = OttawaResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + return { + 'SingleLine': location.replace(', Ottawa, ON', ''), + 'f': 'json', + 'outSR': 4326, + 'maxLocations': kwargs.get('maxRows', 1) + } + + def _adapt_results(self, json_response): + return json_response.get('candidates', []) + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = OttawaQuery('1552 Payette dr.') + g.debug() diff --git a/geocoder/ottawa.py:Zone.Identifier b/geocoder/ottawa.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/ottawa.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/ottawa_parcel.py b/geocoder/ottawa_parcel.py new file mode 100644 index 0000000..8fc0c29 --- /dev/null +++ b/geocoder/ottawa_parcel.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery + + +class OttawaParcelIdResult(OneResult): + + @property + def ok(self): + return bool(self.address_id) + + @property + def address_id(self): + return self.raw.get('attributes', {}).get('PI Municipal Address ID') + + +class OttawaParcelIdQuery(MultipleResultsQuery): + # XXX 8 sept 2017: Service still available ? not documented and returning 403 + + _URL = 'http://maps.ottawa.ca/arcgis/rest/services/Property_Parcels/MapServer/find' + _RESULT_CLASS = OttawaParcelIdResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + return { + 'searchText': location, + 'layers': 0, + 'f': 'json', + 'sr': 4326, + } + + def _adapt_results(self, json_response): + return json_response.get('results', []) + + +class OttawaParcelResult(OneResult): + + @property + def ok(self): + return bool(self.geometry) + + @property + def length(self): + """Length in Feet (f)""" + length = self.parse['attributes'].get('Shape_Length') + if length: + return round(float(length)) + + @property + def area(self): + """Square Foot Area (sqft)""" + area = self.parse['attributes'].get('Shape_Area') + if area: + return round(float(area) * 10.76391) + + @property + def frontage(self): + """Length in Feet (f)""" + if self.length and self.area: + return round(self.area / self.length) + + @property + def municipality(self): + return self._clean(self.parse['attributes'].get('MUNICIPALITY_NAME')) + + @property + def housenumber(self): + return self._clean(self.parse['attributes'].get('ADDRESS_NUMBER')) + + @property + def suffix(self): + return self._clean(self.parse['attributes'].get('SUFFIX')) + + @property + def public_land(self): + return self._clean(self.parse['attributes'].get('PUBLICLAND')) + + @property + def street(self): + return self._clean(self.parse['attributes'].get('ROAD_NAME')) + + @property + def legal_unit(self): + return self._clean(self.parse['attributes'].get('LEGAL_UNIT')) + + @property + def pin(self): + return self._clean(self.parse['attributes'].get('PIN_NUMBER')) + + @property + def geometry(self): + return self.parse['geometry'] + + @property + def postal(self): + return self._clean(self.parse['attributes'].get('POSTAL_CODE')) + + def _clean(self, item): + if item: + return item.strip() + + +class OttawaParcelQuery(MultipleResultsQuery): + """ + Ottawa ArcGIS REST Services + =========================== + Geocoding is the process of assigning a location, usually in the form of + coordinate values (points), to an address by comparing the descriptive + location elements in the address to those present in the reference + material. Addresses come in many forms, ranging from the common address + format of a house number followed by the street name and succeeding + information to other location descriptions such as postal zone or census + tract. An address includes any type of information that distinguishes + a place. + + API Reference + ------------- + http://maps.ottawa.ca/ArcGIS/rest/services/ + compositeLocator/GeocodeServer/findAddressCandidates + """ + provider = 'ottawa' + method = 'parcel' + + _URL = 'http://maps.ottawa.ca/arcgis/rest/services/Property_Parcels/MapServer/find' + _RESULT_CLASS = OttawaParcelResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + ids = OttawaParcelIdQuery(location) + if not ids.address_id: + raise ValueError("Could not get any Id for given location") + + return { + 'searchText': ids.address_id, + 'layers': 2, + 'f': 'json', + 'sr': 4326, + } + + def _adapt_results(self, json_response): + return json_response.get('results', []) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + area = 2123 + length = 100 + frontage = 21 + location = '169 Carillon' + g = OttawaParcelQuery(location, timeout=10.0) + print('%s: %i x %i = %i' % (location, g.frontage, g.length, g.area)) + print('453 Booth: %i x %i = %i' % (frontage, length, area)) + print('%i x %i = %i' % (g.frontage - frontage, g.length - length, g.area - area)) diff --git a/geocoder/ottawa_parcel.py:Zone.Identifier b/geocoder/ottawa_parcel.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/ottawa_parcel.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/tamu.py b/geocoder/tamu.py new file mode 100644 index 0000000..bc20a15 --- /dev/null +++ b/geocoder/tamu.py @@ -0,0 +1,188 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import tamu_key + + +class TamuResult(OneResult): + + def __init__(self, json_content): + self.output_geocode = json_content.get('OutputGeocode', {}) + self.parsed_address = json_content.get('ParsedAddress', {}) + self.reference_feature = json_content.get('ReferenceFeature', {}) + self.census_value = json_content.get('CensusValues', [{}])[0].get('CensusValue1', {}) + super(TamuResult, self).__init__(json_content) + + @property + def lat(self): + lat = self.output_geocode.get('Latitude') + if lat: + return float(lat) + + @property + def lng(self): + lng = self.output_geocode.get('Longitude') + if lng: + return float(lng) + + @property + def quality(self): + return self.output_geocode.get('MatchedLocationType') + + @property + def accuracy(self): + return self.output_geocode.get('FeatureMatchingGeographyType') + + @property + def confidence(self): + return self.output_geocode.get('MatchScore') + + @property + def housenumber(self): + return self.parsed_address.get('Number') + + @property + def street(self): + name = self.parsed_address.get('Name', '') + suffix = self.parsed_address.get('Suffix', '') + return ' '.join([name, suffix]).strip() + + @property + def address(self): + return ' '.join([ + self.parsed_address.get('Number', ''), + self.parsed_address.get('Name', ''), + self.parsed_address.get('Suffix', ''), + self.parsed_address.get('City', ''), + self.parsed_address.get('State', ''), + self.parsed_address.get('Zip', '')]) + + @property + def city(self): + return self.parsed_address.get('City') + + @property + def state(self): + return self.parsed_address.get('State') + + @property + def postal(self): + return self.parsed_address.get('Zip') + + @property + def census_tract(self): + return self.census_value.get('CensusTract') + + @property + def census_block(self): + return self.census_value.get('CensusBlock') + + @property + def census_msa_fips(self): + return self.census_value.get('CensusMsaFips') + + @property + def census_mcd_fips(self): + return self.census_value.get('CensusMcdFips') + + @property + def census_metdiv_fips(self): + return self.census_value.get('CensusMetDivFips') + + @property + def census_place_fips(self): + return self.census_value.get('CensusPlaceFips') + + @property + def census_cbsa_fips(self): + return self.census_value.get('CensusCbsaFips') + + @property + def census_state_fips(self): + return self.census_value.get('CensusStateFips') + + @property + def census_county_fips(self): + return self.census_value.get('CensusCountyFips') + + @property + def census_year(self): + return self.census_value.get('CensusYear') + + +class TamuQuery(MultipleResultsQuery): + """ + TAMU Geocoding Services + ======================= + + Params + ------ + :param location: The street address of the location you want geocoded. + :param city: The city of the location to geocode. + :param state: The state of the location to geocode. + :param zipcode: The zipcode of the location to geocode. + :param key: The API key (use API key "demo" for testing). + + API Reference + ------------- + https://geoservices.tamu.edu/Services/Geocode/WebService + """ + provider = 'tamu' + method = 'geocode' + CENSUSYEARS = ['1990', '2000', '2010'] + + _URL = 'https://geoservices.tamu.edu/Services/Geocode/WebService' \ + '/GeocoderWebServiceHttpNonParsed_V04_01.aspx' + _RESULT_CLASS = TamuResult + _KEY = tamu_key + + def _build_params(self, location, provider_key, **kwargs): + # city, state, zip + city = kwargs.get('city', '') + state = kwargs.get('state', '') + zipcode = kwargs.get('zipcode', '') + return { + 'streetAddress': location, + 'city': city, + 'state': state, + 'zip': zipcode, + 'apikey': provider_key, + 'format': 'json', + 'census': 'true', + 'censusYear': '|'.join(self.CENSUSYEARS), + 'notStore': 'false', + 'verbose': 'true', + 'version': '4.01' + } + + def _catch_errors(self, json_response): + exception_occured = json_response.get('ExceptionOccured') + status_code = json_response.get('QueryStatusCodeValue') + exception = json_response.get('Exception') + + if exception_occured == 'True' or status_code != '200' or exception: + self.error = exception + + if status_code == '401' or status_code == '470': + self.error = u'Tamu returned status_code {0}. Is API key {1} valid?'.format(status_code, self.key) + + return self.error + + def _adapt_results(self, json_response): + return json_response['OutputGeocodes'] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = TamuQuery( + '595 Market Street', + city='San Francisco', + state='CA', + zipcode='94105') + + g.debug() diff --git a/geocoder/tamu.py:Zone.Identifier b/geocoder/tamu.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/tamu.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/tgos.py b/geocoder/tgos.py new file mode 100644 index 0000000..c427045 --- /dev/null +++ b/geocoder/tgos.py @@ -0,0 +1,226 @@ +#!/usr/bin/python +# coding: utf8 + +import re +import requests +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import tgos_key + + +class TgosResult(OneResult): + + def __init__(self, json_content, language): + super(TgosResult, self).__init__(json_content) + self.language = language + + @property + def quality(self): + return self.type + + @property + def lat(self): + return self.raw('geometry', {}).get('y') + + @property + def lng(self): + return self.raw('geometry', {}).get('x') + + @property + def address(self): + return self.raw.get('FULL_ADDR') + + @property + def housenumber(self): + number = self.number + if number: + match = re.match(r'\d+', number) + if match: + return int(match.group()) + return number + + @property + def street(self): + if bool(self.road and self.section): + return u'{road}{section}{segment}'.format( + road=self.road, + section=self.section, + segment={'zh-tw': u'段', 'en': 'Segement'}[self.language]) + return self.road + + @property + def state(self): + return self.county + + @property + def city(self): + return self.town + + @property + def country(self): + return {'en': u'Taiwan', 'zh-tw': u'中華民國'}[self.language] + + # TGOS specific attributes + # ======================== + @property + def alley(self): + return self.raw.get('ALLEY') + + @property + def lane(self): + return self.raw.get('LANE') + + @property + def neighborhood(self): + return self.raw.get('NEIGHBORHOOD') + + @property + def number(self): + return self.raw.get('NUMBER') + + @property + def road(self): + return self.raw.get('ROAD') + + @property + def section(self): + section = self.raw.get('SECTION') + if section: + if self.language == 'zh-tw': + return { + 0: u'零', + 1: u'一', + 2: u'二', + 3: u'三', + 4: u'四', + 5: u'五', + 6: u'六', + 7: u'七', + 8: u'八', + 9: u'九' + }[int(section)] + return int(section) + + @property + def sub_alley(self): + return self.raw.get('sub_alley') + + @property + def tong(self): + return self.raw.get('TONG') + + @property + def village(self): + return self.raw.get('VILLAGE') + + @property + def county(self): + return self.raw.get('county') + + @property + def name(self): + return self.raw.get('name') + + @property + def town(self): + return self.raw.get('town') + + @property + def type(self): + return self.raw.get('type') + + +class TgosQuery(MultipleResultsQuery): + ''' + TGOS Geocoding Service + + TGOS Map is official map service of Taiwan. + + API Reference + ------------- + http://api.tgos.nat.gov.tw/TGOS_MAP_API/Web/Default.aspx + ''' + provider = 'tgos' + method = 'geocode' + + _URL = 'http://gis.tgos.nat.gov.tw/TGLocator/TGLocator.ashx' + _RESULT_CLASS = TgosResult + _KEY = tgos_key + + @classmethod + def _get_api_key(cls, key=None): + # Retrieves API Key from method argument first, then from Environment variables + key = key or cls._KEY + + if not key: + key = cls._get_tgos_key() + + # raise exception if not valid key found + if not key and cls._KEY_MANDATORY: + raise ValueError('Provide API Key') + + return key + + @classmethod + def _get_tgos_key(cls): + url = 'http://api.tgos.nat.gov.tw/TGOS_API/tgos' + r = requests.get(url, headers={'Referer': url}) + + # TGOS Hash pattern used for TGOS API key + pattern = re.compile(r'TGOS.tgHash="([a-zA-Z\d/\-_+=]*)"') + match = pattern.search(r.text) + if match: + return match.group(1) + else: + raise ValueError('Cannot find TGOS.tgHash') + + def _build_params(self, location, provider_key, **kwargs): + return { + 'format': 'json', + 'input': location, + 'center': kwargs.get('method', 'center'), + 'srs': 'EPSG:4326', + 'ignoreGeometry': False, + 'keystr': provider_key, + 'pnum': kwargs.get('maxRows', 5) + } + + def _before_initialize(self, location, **kwargs): + # Custom language output + language = kwargs.get('language', 'taiwan').lower() + if language in ['english', 'en', 'eng']: + self.language = 'en' + elif language in ['chinese', 'zh']: + self.language = 'zh-tw' + else: + self.language = 'zh-tw' + + def _catch_errors(self, json_response): + status = json_response['status'] + if status != 'OK': + if status == 'REQUEST_DENIED': + self.error = json_response['error_message'] + self.status_code = 401 + else: + self.error = 'Unknown' + self.status_code = 500 + + return self.error + + def _adapt_results(self, json_response): + return json_response['results'] + + def _parse_results(self, json_response): + # overriding method to pass language to every result + for json_dict in self._adapt_results(json_response): + self.add(self.one_result(json_dict, self.language)) + + # set default result to use for delegation + self.current_result = len(self) > 0 and self[0] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = TgosQuery('台北市內湖區內湖路一段735號', language='en') + g.debug() diff --git a/geocoder/tgos.py:Zone.Identifier b/geocoder/tgos.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/tgos.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/tomtom.py b/geocoder/tomtom.py new file mode 100644 index 0000000..36ce38f --- /dev/null +++ b/geocoder/tomtom.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.location import BBox +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import tomtom_key + + +class TomtomResult(OneResult): + + def __init__(self, json_content): + self._address = json_content['address'] + super(TomtomResult, self).__init__(json_content) + + @property + def lat(self): + return self.raw.get('position', {}).get('lat') + + @property + def lng(self): + return self.raw.get('position', {}).get('lon') + + @property + def geohash(self): + return self.raw.get('id') + + @property + def quality(self): + return self.raw.get('type') + + @property + def bbox(self): + viewport = self.raw.get('viewport', {}) + if viewport: + bbox = { + 'south': viewport.get('btmRightPoint')['lon'], + 'west': viewport.get('btmRightPoint')['lat'], + 'north': viewport.get('topLeftPoint')['lon'], + 'east': viewport.get('topLeftPoint')['lat'], + } + return BBox.factory(bbox).as_dict + + @property + def address(self): + return self._address.get('freeformAddress') + + @property + def housenumber(self): + return self._address.get('streetNumber') + + @property + def street(self): + return self._address.get('streetName') + + @property + def road(self): + return self.street + + @property + def city(self): + return self._address.get('municipality') + + @property + def state(self): + return self._address.get('countrySubdivisionName', self._address.get('countrySubdivision')) + + @property + def country(self): + return self._address.get('countryCode') + + @property + def postal(self): + return self._address.get('postalCode') + + +class TomtomQuery(MultipleResultsQuery): + """ + Geocoding API + ============= + The Geocoding API gives developers access to TomTom’s first class geocoding service. + Developers may call this service through either a single or batch geocoding request. + This service supports global coverage, with house number level matching in over 50 countries, + and address point matching where available. + + API Reference + ------------- + https://developer.tomtom.com/tomtom-maps-apis-developers + """ + provider = 'tomtom' + method = 'geocode' + + _URL = 'https://api.tomtom.com/search/2/geocode/{0}.json' + _RESULT_CLASS = TomtomResult + _KEY = tomtom_key + + def _build_params(self, location, provider_key, **kwargs): + return { + 'key': provider_key, + 'limit': kwargs.get('maxRows', 1), + 'countrySet': kwargs.get('countrySet'), + 'lon': kwargs.get('lon'), + 'lat': kwargs.get('lat'), + 'radius': kwargs.get('radius') + } + + def _before_initialize(self, location, **kwargs): + self.url = self.url.format(location) + + def _adapt_results(self, json_response): + return json_response['results'] + + def _catch_errors(self, json_response): + if 'Developer Inactive' in str(json_response): + self.error = 'API Key not valid' + self.status_code = 401 + + return self.error + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = TomtomQuery('1552 Payette dr., Ottawa') + g.debug() diff --git a/geocoder/tomtom.py:Zone.Identifier b/geocoder/tomtom.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/tomtom.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/uscensus.py b/geocoder/uscensus.py new file mode 100644 index 0000000..2f2817e --- /dev/null +++ b/geocoder/uscensus.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import re +import logging + +from geocoder.base import OneResult, MultipleResultsQuery + + +class USCensusResult(OneResult): + + def __init__(self, json_content): + # create safe shortcuts + self._coordinates = json_content.get('coordinates', {}) + self._address_components = json_content.get('addressComponents', {}) + + # proceed with super.__init__ + super(USCensusResult, self).__init__(json_content) + + @property + def lat(self): + return self._coordinates.get('y') + + @property + def lng(self): + return self._coordinates.get('x') + + @property + def address(self): + return self.raw.get('matchedAddress') + + @property + def housenumber(self): + if self.address: + match = re.search('^\d+', self.address, re.UNICODE) + if match: + return match.group(0) + + @property + def fromhousenumber(self): + return self._address_components.get('fromAddress') + + @property + def tohousenumber(self): + return self._address_components.get('toAddress') + + @property + def streetname(self): + return self._address_components.get('streetName') + + @property + def prequalifier(self): + return self._address_components.get('preQualifier') + + @property + def predirection(self): + return self._address_components.get('preDirection') + + @property + def pretype(self): + return self._address_components.get('preType') + + @property + def suffixtype(self): + return self._address_components.get('suffixType') + + @property + def suffixdirection(self): + return self._address_components.get('suffixDirection') + + @property + def suffixqualifier(self): + return self._address_components.get('suffixQualifier') + + @property + def city(self): + return self._address_components.get('city') + + @property + def state(self): + return self._address_components.get('state') + + @property + def postal(self): + return self._address_components.get('zip') + + +class USCensusQuery(MultipleResultsQuery): + """ + US Census Geocoder REST Services + ======================= + The Census Geocoder is an address look-up tool that converts your address to an approximate coordinate (latitude/longitude) and returns information about the address range that includes the address and the census geography the address is within. The geocoder is available as a web interface and as an API (Representational State Transfer - REST - web-based service). + + API Reference + ------------- + https://geocoding.geo.census.gov/geocoder/Geocoding_Services_API.html + + """ + provider = 'uscensus' + method = 'geocode' + + _URL = 'https://geocoding.geo.census.gov/geocoder/locations/onelineaddress' + _RESULT_CLASS = USCensusResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + return { + 'address': location, + 'benchmark': kwargs.get('benchmark', '4'), + 'format': 'json' + } + + def _adapt_results(self, json_response): + return json_response['result']['addressMatches'] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = USCensusQuery('4600 Silver Hill Road, Suitland, MD 20746', benchmark=9) + g.debug() diff --git a/geocoder/uscensus.py:Zone.Identifier b/geocoder/uscensus.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/uscensus.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/uscensus_batch.py b/geocoder/uscensus_batch.py new file mode 100644 index 0000000..fc2bd72 --- /dev/null +++ b/geocoder/uscensus_batch.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import +from geocoder.base import OneResult, MultipleResultsQuery + +import logging +import io +import csv +import sys +import requests + + +PY2 = sys.version_info < (3, 0) +csv_io = io.BytesIO if PY2 else io.StringIO +csv_encode = (lambda input: input) if PY2 else (lambda input: input.encode('utf-8')) +csv_decode = (lambda input: input) if PY2 else (lambda input: input.decode('utf-8')) + +LOGGER = logging.getLogger(__name__) + + +class USCensusBatchResult(OneResult): + + def __init__(self, content): + self._content = content + + if self._content: + self._coordinates = tuple(float(pos) for pos in content[1].split(',')) + + # proceed with super.__init__ + super(USCensusBatchResult, self).__init__(content) + + @property + def lat(self): + if self._content: + return self._coordinates[1] + + @property + def lng(self): + if self._content: + return self._coordinates[0] + + @property + def address(self): + if self._content: + return self._content[0] + + +class USCensusBatch(MultipleResultsQuery): + """ + US Census Geocoder REST Services + ======================= + The Census Geocoder is an address look-up tool that converts your address to an approximate coordinate (latitude/longitude) and returns information about the address range that includes the address and the census geography the address is within. The geocoder is available as a web interface and as an API (Representational State Transfer - REST - web-based service). + + API Reference + ------------- + https://geocoding.geo.census.gov/geocoder/Geocoding_Services_API.html + + """ + provider = 'uscensus' + method = 'geocode' + + _URL = 'https://geocoding.geo.census.gov/geocoder/locations/addressbatch' + _RESULT_CLASS = USCensusBatchResult + _KEY_MANDATORY = False + + def generate_batch(self, locations): + out = csv_io() + writer = csv.writer(out) + + for idx, address in enumerate(locations): + writer.writerow([idx, address, None, None, None]) + + return csv_encode(out.getvalue()) + + def _build_params(self, locations, provider_key, **kwargs): + self.batch = self.generate_batch(locations) + self.locations_length = len(locations) + self.timeout = int(kwargs.get('timeout', '1800')) # 30mn timeout, us census can be really slow with big batches + self.benchmark = str(kwargs.get('benchmark', 4)) + + return { + 'benchmark': (None, self.benchmark), + 'addressFile': ('addresses.csv', self.batch) + } + + def _connect(self): + self.status_code = 'Unknown' + + try: + self.response = response = self.session.post( + self.url, + files=self.params, + headers=self.headers, + timeout=self.timeout, + proxies=self.proxies + ) + + # check that response is ok + self.status_code = response.status_code + response.raise_for_status() + + return response.content + + except (requests.exceptions.RequestException, LookupError) as err: + # store real status code and error + self.error = u'ERROR - {}'.format(str(err)) + LOGGER.error("Status code %s from %s: %s", + self.status_code, self.url, self.error) + + return False + + def _adapt_results(self, response): + result = csv_io(csv_decode(response)) + + rows = {} + for row in csv.reader(result): + if row[2] == 'Match': + rows[row[0]] = [row[4], row[5]] + + return rows + + def _parse_results(self, response): + rows = self._adapt_results(response) + + # re looping through the results to give them back in their original order + for idx in range(0, self.locations_length): + self.add(self.one_result(rows.get(str(idx), None))) + + self.current_result = len(self) > 0 and self[0] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = USCensusBatch(['4650 Silver Hill Road, Suitland, MD 20746', '42 Chapel Street, New Haven'], benchmark=9) + g.debug() diff --git a/geocoder/uscensus_batch.py:Zone.Identifier b/geocoder/uscensus_batch.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/uscensus_batch.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/uscensus_reverse.py b/geocoder/uscensus_reverse.py new file mode 100644 index 0000000..01656a8 --- /dev/null +++ b/geocoder/uscensus_reverse.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.location import Location +from geocoder.base import OneResult +from geocoder.uscensus import USCensusQuery + + +class USCensusReverseResult(OneResult): + + @property + def ok(self): + return bool(self.raw['States']) + + @property + def state(self): + if self.raw['States']: + return self.raw['States'][0].get('NAME') + + @property + def statenumber(self): + if self.raw['States']: + return self.raw['States'][0].get('STATE') + + @property + def county(self): + if self.raw['Counties']: + return self.raw['Counties'][0].get('NAME') + + @property + def countynumber(self): + if self.raw['Counties']: + return self.raw['Counties'][0].get('COUNTY') + + @property + def tract(self): + if self.raw['Census Tracts']: + return self.raw['Census Tracts'][0].get('NAME') + + @property + def tractnumber(self): + if self.raw['Census Tracts']: + return self.raw['Census Tracts'][0].get('TRACT') + + @property + def block(self): + if self.raw['2010 Census Blocks']: + return self.raw['2010 Census Blocks'][0].get('NAME') + elif self.raw['Census Blocks']: + return self.raw['Census Blocks'][0].get('NAME') + + @property + def blocknumber(self): + if self.raw['2010 Census Blocks']: + return self.raw['2010 Census Blocks'][0].get('BLOCK') + elif self.raw['Census Blocks']: + return self.raw['Census Blocks'][0].get('BLOCK') + + @property + def geoid(self): + if self.raw['2010 Census Blocks']: + return self.raw['2010 Census Blocks'][0].get('GEOID') + elif self.raw['Census Blocks']: + return self.raw['Census Blocks'][0].get('GEOID') + + +class USCensusReverse(USCensusQuery): + """ + US Census Geocoder REST Services + ======================= + The Census Geocoder is an address look-up tool that converts your address to an approximate coordinate (latitude/longitude) and returns information about the address range that includes the address and the census geography the address is within. The geocoder is available as a web interface and as an API (Representational State Transfer - REST - web-based service). + + API Reference + ------------- + https://geocoding.geo.census.gov/geocoder/Geocoding_Services_API.pdf + + """ + provider = 'uscensus' + method = 'reverse' + + _URL = 'https://geocoding.geo.census.gov/geocoder/geographies/coordinates' + _RESULT_CLASS = USCensusReverseResult + + def _build_params(self, location, provider_key, **kwargs): + location = Location(location) + return { + 'x': location.longitude, + 'y': location.latitude, + 'benchmark': kwargs.get('benchmark', '4'), + 'vintage': kwargs.get('vintage', '4'), + 'format': 'json' + } + + def _adapt_results(self, json_response): + return [json_response['result']['geographies']] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = USCensusReverse([38.846542, -76.92691]) + g.debug() diff --git a/geocoder/uscensus_reverse.py:Zone.Identifier b/geocoder/uscensus_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/uscensus_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/w3w.py b/geocoder/w3w.py new file mode 100644 index 0000000..3571462 --- /dev/null +++ b/geocoder/w3w.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery +from geocoder.keys import w3w_key + + +class W3WResult(OneResult): + + @property + def lat(self): + position = self.raw.get('geometry') + if position: + return position['lat'] + + @property + def lng(self): + position = self.raw.get('geometry') + if position: + return position['lng'] + + @property + def language(self): + return self.raw.get('language') + + @property + def words(self): + return self.raw.get('words') + + +class W3WQuery(MultipleResultsQuery): + """ + What3Words + ========== + What3Words is a global grid of 57 trillion 3mx3m squares. + Each square has a 3 word address that can be communicated quickly, + easily and with no ambiguity. + + Addressing the world + + Everyone and everywhere now has an address + + Params + ------ + :param location: Your search location you want geocoded. + :param key: W3W API key. + :param method: Chose a method (geocode, method) + + References + ---------- + API Reference: https://docs.what3words.com/api/v2/ + Get W3W key: https://map.what3words.com/register?dev=true + """ + provider = 'w3w' + method = 'geocode' + + _URL = 'https://api.what3words.com/v2/forward' + _RESULT_CLASS = W3WResult + _KEY = w3w_key + + def _build_params(self, location, provider_key, **kwargs): + return { + 'addr': location, + 'key': provider_key, + } + + def _adapt_results(self, json_response): + return [json_response] + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = W3WQuery('embedded.fizzled.trial') + g.debug() diff --git a/geocoder/w3w.py:Zone.Identifier b/geocoder/w3w.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/w3w.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/w3w_reverse.py b/geocoder/w3w_reverse.py new file mode 100644 index 0000000..e3cd2ab --- /dev/null +++ b/geocoder/w3w_reverse.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +from geocoder.w3w import W3WResult, W3WQuery + + +class W3WReverseResult(W3WResult): + + @property + def ok(self): + return bool(self.words) + + +class W3WReverse(W3WQuery): + """ + what3words + ========== + what3words is a global grid of 57 trillion 3mx3m squares. + Each square has a 3 word address that can be communicated quickly, + easily and with no ambiguity. + + Addressing the world + + Everyone and everywhere now has an address + + Params + ------ + :param location: Your search location you want geocoded. + :param key: W3W API key. + :param method: Chose a method (geocode, method) + + References + ---------- + API Reference: http://developer.what3words.com/ + Get W3W key: http://developer.what3words.com/api-register/ + """ + provider = 'w3w' + method = 'reverse' + + _URL = 'https://api.what3words.com/v2/reverse' + _RESULT_CLASS = W3WReverseResult + + def _build_params(self, location, provider_key, **kwargs): + return { + 'coords': location, + 'key': provider_key, + } + + +if __name__ == '__main__': + g = W3WReverse([45.15, -75.14]) + g.debug() diff --git a/geocoder/w3w_reverse.py:Zone.Identifier b/geocoder/w3w_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/w3w_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/yahoo.py b/geocoder/yahoo.py new file mode 100644 index 0000000..8fd9ec9 --- /dev/null +++ b/geocoder/yahoo.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery + + +class YahooResult(OneResult): + + @property + def lat(self): + return self.raw.get('latitude') + + @property + def lng(self): + return self.raw.get('longitude') + + @property + def address(self): + line1 = self.raw.get('line1') + line2 = self.raw.get('line2') + if line1: + return ', '.join([line1, line2]) + else: + return line2 + + @property + def housenumber(self): + return self.raw.get('house') + + @property + def street(self): + return self.raw.get('street') + + @property + def neighborhood(self): + return self.raw.get('neighborhood') + + @property + def city(self): + return self.raw.get('city') + + @property + def county(self): + return self.raw.get('county') + + @property + def state(self): + return self.raw.get('state') + + @property + def country(self): + return self.raw.get('country') + + @property + def hash(self): + return self.raw.get('hash') + + @property + def quality(self): + return self.raw.get('addressMatchType') + + @property + def postal(self): + postal = self.raw.get('postal') + if postal: + return postal + else: + return self.raw.get('uzip') + + +class YahooQuery(MultipleResultsQuery): + """ + Yahoo BOSS Geo Services + ======================= + Yahoo PlaceFinder is a geocoding Web service that helps developers make + their applications location-aware by converting street addresses or + place names into geographic coordinates (and vice versa). + + API Reference + ------------- + https://developer.yahoo.com/boss/geo/ + """ + provider = 'yahoo' + method = 'geocode' + + _URL = 'https://sgws2.maps.yahoo.com/FindLocation' + _RESULT_CLASS = YahooResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + return{ + 'q': location, + 'flags': 'J', + 'locale': kwargs.get('locale', 'en-CA'), + } + + def _catch_errors(self, json_response): + status = json_response['statusDescription'] + if status: + if not status == 'OK': + self.error = status + + return self.error + + def _adapt_results(self, json_response): + return [json_response['Result']] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = YahooQuery('1552 Payette dr., Ottawa, ON') + g.debug() diff --git a/geocoder/yahoo.py:Zone.Identifier b/geocoder/yahoo.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/yahoo.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/yandex.py b/geocoder/yandex.py new file mode 100644 index 0000000..c55e15f --- /dev/null +++ b/geocoder/yandex.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# coding: utf8 +from __future__ import absolute_import + +import logging + +from geocoder.base import OneResult, MultipleResultsQuery + + +class YandexResult(OneResult): + + def __init__(self, json_content): + self._meta_data = json_content['metaDataProperty']['GeocoderMetaData'] + super(YandexResult, self).__init__(json_content) + + @property + def lat(self): + pos = self.raw.get('Point', {}).get('pos') + if pos: + return pos.split(' ')[1] + + @property + def lng(self): + pos = self.raw.get('Point', {}).get('pos') + if pos: + return pos.split(' ')[0] + + @property + def bbox(self): + envelope = self._meta_data.get('boundedBy', {}).get('Envelope', {}) + if envelope: + east, north = envelope.get('upperCorner', '').split(' ') + west, south = envelope.get('lowerCorner', '').split(' ') + try: + return self._get_bbox(float(south), + float(west), + float(north), + float(east)) + except: + pass + + @property + def description(self): + return self.raw.get('description') + + @property + def address(self): + return self._meta_data.get('text') + + @property + def quality(self): + return self._meta_data.get('kind') + + @property + def accuracy(self): + return self._meta_data.get('precision') + + @property + def _country(self): + return self._meta_data.get('AddressDetails', {}).get('Country', {}) + + @property + def country(self): + return self._country.get('CountryName') + + @property + def country_code(self): + return self._country.get('CountryNameCode') + + @property + def _administrativeArea(self): + return self._country.get('AdministrativeArea', {}) + + @property + def state(self): + return self._administrativeArea.get('AdministrativeAreaName') + + @property + def _subAdministrativeArea(self): + return self._administrativeArea.get('SubAdministrativeArea', {}) + + @property + def county(self): + return self._subAdministrativeArea.get('SubAdministrativeAreaName') + + @property + def _locality(self): + return self._subAdministrativeArea.get('Locality', {}) + + @property + def city(self): + return self._locality.get('LocalityName') + + @property + def _thoroughfare(self): + return self._locality.get('Thoroughfare', {}) + + @property + def street(self): + return self._thoroughfare.get('ThoroughfareName') + + @property + def _premise(self): + return self._thoroughfare.get('Premise', {}) + + @property + def housenumber(self): + return self._premise.get('PremiseNumber') + + +class YandexQuery(MultipleResultsQuery): + """ + Yandex + ====== + Yandex (Russian: Яндекс) is a Russian Internet company + which operates the largest search engine in Russia with + about 60% market share in that country. + + The Yandex home page has been rated as the most popular website in Russia. + + Params + ------ + :param location: Your search location you want geocoded. + :param lang: Chose the following language: + > ru-RU — Russian (by default) + > uk-UA — Ukrainian + > be-BY — Belarusian + > en-US — American English + > en-BR — British English + > tr-TR — Turkish (only for maps of Turkey) + :param kind: Type of toponym (only for reverse geocoding): + > house - house or building + > street - street + > metro - subway station + > district - city district + > locality - locality (city, town, village, etc.) + + References + ---------- + API Reference: http://api.yandex.com/maps/doc/geocoder/desc/concepts/input_params.xml + """ + provider = 'yandex' + method = 'geocode' + + _URL = 'https://geocode-maps.yandex.ru/1.x/' + _RESULT_CLASS = YandexResult + _KEY_MANDATORY = False + + def _build_params(self, location, provider_key, **kwargs): + return { + 'geocode': location, + 'lang': kwargs.get('lang', 'en-US'), + 'kind': kwargs.get('kind', ''), + 'format': 'json', + 'results': kwargs.get('maxRows', 1), + } + + def _adapt_results(self, json_response): + return [item['GeoObject'] for item + in json_response['response']['GeoObjectCollection']['featureMember']] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = YandexQuery('1552 Payette dr., Ottawa', maxRows=3) + g.debug() diff --git a/geocoder/yandex.py:Zone.Identifier b/geocoder/yandex.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/yandex.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoder/yandex_reverse.py b/geocoder/yandex_reverse.py new file mode 100644 index 0000000..ad7cd0a --- /dev/null +++ b/geocoder/yandex_reverse.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# coding: utf8 + +from __future__ import absolute_import + +import logging + +from geocoder.yandex import YandexResult, YandexQuery +from geocoder.location import Location + + +class YandexReverseResult(YandexResult): + + @property + def ok(self): + return bool(self.address) + + +class YandexReverse(YandexQuery): + """ + Yandex + ====== + Yandex (Russian: Яндекс) is a Russian Internet company + which operates the largest search engine in Russia with + about 60% market share in that country. + The Yandex home page has been rated as the most popular website in Russia. + Params + ------ + :param location: Your search location you want geocoded. + :param lang: Chose the following language: + > ru-RU — Russian (by default) + > uk-UA — Ukrainian + > be-BY — Belarusian + > en-US — American English + > en-BR — British English + > tr-TR — Turkish (only for maps of Turkey) + :param kind: Type of toponym (only for reverse geocoding): + > house - house or building + > street - street + > metro - subway station + > district - city district + > locality - locality (city, town, village, etc.) + References + ---------- + API Reference: http://api.yandex.com/maps/doc/geocoder/ + desc/concepts/input_params.xml + """ + provider = 'yandex' + method = 'reverse' + + _RESULT_CLASS = YandexReverseResult + + def _build_params(self, location, provider_key, **kwargs): + x, y = Location(location).xy + self.location = u'{}, {}'.format(x, y) + return { + 'geocode': self.location, + 'lang': kwargs.get('lang', 'en-US'), + 'kind': kwargs.get('kind', ''), + 'format': 'json', + 'results': kwargs.get('maxRows', 1), + } + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + g = YandexReverse({'lat': 41.005407, 'lng': 28.978349}) + g.debug() diff --git a/geocoder/yandex_reverse.py:Zone.Identifier b/geocoder/yandex_reverse.py:Zone.Identifier new file mode 100644 index 0000000..0aaaf93 --- /dev/null +++ b/geocoder/yandex_reverse.py:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=C:\Users\stacy\Downloads\geocoder-1.38.1.tar.gz diff --git a/geocoderdupe.py b/geocoderdupe.py new file mode 100644 index 0000000..e69de29 diff --git a/weather/weather.py b/weather/weather.py index 75fd2f6..b2fe645 100644 --- a/weather/weather.py +++ b/weather/weather.py @@ -8,7 +8,7 @@ # # It's your job to implement this function. -from weather.weather_apis import ( +from weather.weather_apis import( geocode_location, estimate_location, get_weather_office, @@ -21,4 +21,16 @@ def print_weather(location=None, metric=False, verbose=False): When metric is True, prints out the weather in metric units. When verbose is True, prints out a more detailed report. """ - print("Not finished...") # YOUR CODE HERE! +#ask for location -> geocode location + if location: + coordinates = geocode_location(location) +#if none -> estimate location + else: + coordinates = estimate_location() +#coordinate -> get weather office -> get forecast + office = get_weather_office(coordinates["lat"], coordinates["lng"]) + forecast = get_forecast(office["office"], office["x"], office["y"], metric=False) + for entry in forecast: + print(entry["name"], 'it will be', entry["temperature"], 'degrees and', entry["description"], 'with', entry["wind_speed"], entry["wind_direction"], 'winds.') + +#'name': 'This Afternoon', 'temperature': 53, 'wind_speed': '6 mph', 'wind_direction': 'NW', 'description': 'Mostly Sunny' \ No newline at end of file