Exemplo n.º 1
0
 def test_pro_filter_AttrFilter_inexact(self):
     """Test AttrFilter postprocessor with ``exact_match=False``."""
     good_values = ['roof', 'parcel']
     candidates_in = [self.best, self.good, self.better]
     candidates_exp = [self.best, self.better]  # roof is a substr of rooftop
     candidates_out = AttrFilter(good_values, 'locator', exact_match=False).process(candidates_in)
     self.assertEqual_(candidates_out, candidates_exp)
Exemplo n.º 2
0
 def test_pro_filter_AttrFilter_exact(self):
     """Test AttrFilter postprocessor."""
     good_values = ['roof', 'parcel']
     candidates_in = [self.best, self.good, self.better]
     candidates_exp = [self.better]  # just the one with the parcel locator
     candidates_out = AttrFilter(good_values, 'locator', exact_match=True).process(candidates_in)
     self.assertEqual_(candidates_out, candidates_exp)
Exemplo n.º 3
0
class _EsriEUGeocodeService():
    """
    Base class including for Esri EU REST and SOAP Geocoders

    As of 29 Dec 2011, the ESRI website claims to support Andorra, Austria,
    Belgium, Denmark, Finland, France, Germany, Gibraltar, Ireland, Italy,
    Liechtenstein, Luxembourg, Monaco, The Netherlands, Norway, Portugal,
    San Marino, Spain, Sweden, Switzerland, United Kingdom, and Vatican City.
    """
    _wkid = 4326

    #: FIPS codes of supported countries
    SUPPORTED_COUNTRIES_FIPS = [
        'AN', 'AU', 'BE', 'DA', 'FI', 'FR', 'GM', 'GI', 'EI', 'IT', 'LS', 'LU',
        'MN', 'NL', 'NO', 'PO', 'SM', 'SP', 'SW', 'SZ', 'UK', 'VT'
    ]

    #: ISO-2 codes of supported countries
    SUPPORTED_COUNTRIES_ISO2 = [
        'AD', 'AT', 'BE', 'DK', 'FI', 'FR', 'DE', 'GI', 'IE', 'IT', 'LI', 'LU',
        'MC', 'NL', 'NO', 'PT', 'SM', 'ES', 'SE', 'CH', 'GB', 'VC'
    ]

    #: Map of FIPS to ISO-2 codes, if they are different.
    MAP_FIPS_TO_ISO2 = {
        'AN': 'AD',
        'AU': 'AT',
        'DA': 'DK',
        'GM': 'DE',
        'EI': 'IE',
        'LS': 'LI',
        'MN': 'MC',
        'PO': 'PT',
        'SP': 'ES',
        'SW': 'SE',
        'SZ': 'CH',
        'UK': 'GB',
        'VT': 'VC',
    }

    #: Map to standardize locator
    LOCATOR_MAP = {
        'EU_Street_Addr': 'interpolation',
    }

    DEFAULT_PREPROCESSORS = [
        CountryPreProcessor(SUPPORTED_COUNTRIES_ISO2, MAP_FIPS_TO_ISO2),
        ParseSingleLine(),
    ]

    DEFAULT_POSTPROCESSORS = [
        AttrFilter(['EU_Street_Addr'], 'locator', False),
        AttrRename('locator', LOCATOR_MAP),
        UseHighScoreIfAtLeast(100),
        GroupBy('match_addr'),
        ScoreSorter(),
    ]
Exemplo n.º 4
0
    def test_esri_geocoder_eu_default_override(self):
        """
        Test for default argument bug in 3.1 --
        EsriNA and EsriEU append processors rather than replace them
        """
        geocoder = Geocoder([['omgeo.services.EsriEU',
                            {'postprocessors': [AttrFilter([
                                'rooftop',
                                'interpolation',
                                'postal_specific'],
                                'locator')]}]])

        self.assertEqual(1, len(geocoder._sources[0]._postprocessors),
            'EsriEU geocoder incorrectly processed defaults')
        self.assertEqual('AttrFilter',
            geocoder._sources[0]._postprocessors[0].__class__.__name__,
            'EsriEU geocoder incorrectly processed defaults')
Exemplo n.º 5
0
class _EsriNAGeocodeService():
    """
    Defaults for the _EsriNAGeocodeService
    """

    LOCATOR_MAP = {
        'RoofTop': 'rooftop',
        'Streets': 'interpolation',
    }

    DEFAULT_PREPROCESSORS = [CountryPreProcessor(['US', 'CA'])]

    DEFAULT_POSTPROCESSORS = [
        AttrRename('locator', LOCATOR_MAP),
        AttrFilter(['rooftop', 'interpolation'], 'locator'),
        AttrSorter(['rooftop', 'interpolation'], 'locator'),
        UseHighScoreIfAtLeast(99.8),
        GroupBy('match_addr'),
        ScoreSorter(),
    ]
Exemplo n.º 6
0
class Nominatim(GeocodeService):
    """
    Class to geocode using `Nominatim services hosted
    by MapQuest <http://open.mapquestapi.com/nominatim/>`_.
    """
    _wkid = 4326
    _endpoint = 'http://open.mapquestapi.com/nominatim/v1/search'

    DEFAULT_ACCEPTED_ENTITIES = [
        'building.', 'historic.castle', 'leisure.ice_rink',
        'leisure.miniature_golf', 'leisure.sports_centre', 'lesiure.stadium',
        'leisure.track', 'lesiure.water_park', 'man_made.lighthouse',
        'man_made.works', 'military.barracks', 'military.bunker', 'office.',
        'place.house', 'amenity.', 'power.generator', 'railway.station',
        'shop.', 'tourism.'
    ]

    DEFAULT_REJECTED_ENTITIES = [
        'amenity.drinking_water', 'amentity.bicycle_parking',
        'amentity.ev_charging', 'amentity.grit_bin', 'amentity.atm',
        'amentity.hunting_stand', 'amentity.post_box'
    ]

    DEFAULT_PREPROCESSORS = [ReplaceRangeWithNumber()
                             ]  # 766-68 Any St. -> 766 Any St.
    """Preprocessors to use with this geocoder service, in order of desired execution."""

    DEFAULT_POSTPROCESSORS = [
        AttrFilter(DEFAULT_ACCEPTED_ENTITIES, 'entity', exact_match=False),
        AttrExclude(DEFAULT_REJECTED_ENTITIES, 'entity')
    ]
    """Postprocessors to use with this geocoder service, in order of desired execution."""
    def __init__(self, preprocessors=None, postprocessors=None, settings=None):
        preprocessors = Nominatim.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors
        postprocessors = Nominatim.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors
        GeocodeService.__init__(self, preprocessors, postprocessors, settings)

    def _geocode(self, pq):
        query = {
            'q': pq.query,
            'countrycodes': pq.country,  # only takes ISO-2
            'format': 'json'
        }

        if pq.viewbox is not None:
            query = dict(
                query, **{
                    'viewbox': pq.viewbox.to_mapquest_str(),
                    'bounded': pq.bounded
                })

        response_obj = self._get_json_obj(self._endpoint, query)

        returned_candidates = []  # this will be the list returned
        for r in response_obj:
            c = Candidate()
            c.locator = 'parcel'  # we don't have one but this is the closest match
            c.entity = '%s.%s' % (r['class'], r['type'])  # ex.: "place.house"
            c.match_addr = r[
                'display_name']  # ex. "Wolf Building, 340, N 12th St, Philadelphia, Philadelphia County, Pennsylvania, 19107, United States of America" #TODO: shorten w/ pieces
            c.x = float(r['lon'])  # long, ex. -122.13 # cast to float in 1.3.4
            c.y = float(r['lat'])  # lat, ex. 47.64 # cast to float in 1.3.4
            c.wkid = self._wkid
            c.geoservice = self.__class__.__name__
            returned_candidates.append(c)
        return returned_candidates
Exemplo n.º 7
0
class EsriWGS(GeocodeService):
    """
    Class to geocode using the `ESRI World Geocoding service
    <http://geocode.arcgis.com/arcgis/geocoding.html>`_.

    This uses two endpoints -- one for single-line addresses,
    and one for multi-part addresses.

    An optional (key) parameter can be passed to the PlaceQuery
    which will be passed as a magicKey to the find endpoint if
    using a single line address/text search. This allows omgeo
    to be used with the `Esri suggest endpoint
    <https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm>`_.

    Note: Based on tests using the magicKey parameter, it is
    recommended that a viewbox not be used with in conjuction
    with the magicKey. Additionally, address/search text passed
    via the query may be ignored when using a magicKey.
    """

    LOCATOR_MAP = {
        'PointAddress': 'rooftop',
        'StreetAddress': 'interpolation',
        'PostalExt': 'postal_specific',  # accept ZIP+4
        'Postal': 'postal'
    }

    DEFAULT_PREPROCESSORS = [CancelIfPOBox()]

    DEFAULT_POSTPROCESSORS = [
        AttrFilter(
            [
                'PointAddress',
                'StreetAddress',
                #'PostalExt',
                #'Postal'
            ],
            'locator_type'),
        #AttrExclude(['USA_Postal'], 'locator'), #accept postal from everywhere but US (need PostalExt)
        AttrSorter(
            [
                'PointAddress',
                'StreetAddress',
                #'PostalExt',
                #'Postal'
            ],
            'locator_type'),
        AttrRename(
            'locator',
            LOCATOR_MAP),  # after filter to avoid searching things we toss out
        UseHighScoreIfAtLeast(99.8),
        ScoreSorter(),
        GroupBy('match_addr'),
        GroupBy(('x', 'y')),
    ]

    _endpoint = 'http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer'

    def _geocode(self, pq):
        """
        :arg PlaceQuery pq: PlaceQuery object to use for geocoding
        :returns: list of location Candidates
        """
        #: List of desired output fields
        #: See `ESRI docs <http://geocode.arcgis.com/arcgis/geocoding.html#output>_` for details
        outFields = (
            'Loc_name',
            #'Shape',
            'Score',
            'Match_Addr',  #based on address standards for the country
            #'Address', # returned by default
            #'Country' # 3-digit ISO 3166-1 code for a country. Example: Canada = "CAN"
            #'Admin',
            #'DepAdmin',
            #'SubAdmin',
            #'Locality',
            #'Postal',
            #'PostalExt',
            'Addr_Type',
            #'Type',
            #'Rank',
            'AddNum',
            'StPreDir',
            'StPreType',
            'StName',
            'StType',
            'StDir',
            #'Side',
            #'AddNumFrom',
            #'AddNumTo',
            #'AddBldg',
            'City',
            'Subregion',
            'Region',
            'Postal',
            'Country',
            #'Ymax',
            #'Ymin',
            #'Xmin',
            #'Xmax',
            #'X',
            #'Y',
            'DisplayX',
            'DisplayY',
            #'LangCode',
            #'Status',
        )
        outFields = ','.join(outFields)
        query = dict(
            f='json',  # default HTML. Other options are JSON and KMZ.
            outFields=outFields,
            #outSR=WKID, defaults to 4326
            maxLocations=20,  # default 1; max is 20
        )

        # Postal-code only searches work in the single-line but not multipart geocoder
        # Remember that with the default postprocessors, postcode-level results will be eliminated
        if pq.query == pq.address == '' and pq.postal != '':
            pq.query = pq.postal

        if pq.query == '':  # multipart
            method = 'findAddressCandidates'
            query = dict(
                query,
                Address=pq.
                address,  # commonly represents the house number and street name of a complete address
                Neighborhood=pq.neighborhood,
                City=pq.city,
                Subregion=pq.subregion,
                Region=pq.state,
                Postal=pq.postal,
                #PostalExt=
                CountryCode=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
            if pq.bounded and pq.viewbox is not None:
                query = dict(query, searchExtent=pq.viewbox.to_esri_wgs_json())
        else:  # single-line
            method = 'find'
            magic_key = pq.key if hasattr(pq, 'key') else ''
            query = dict(
                query,
                text=pq.
                query,  # This can be a street address, place name, postal code, or POI.
                sourceCountry=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
            if magic_key:
                query[
                    'magicKey'] = magic_key  # This is a lookup key returned from the suggest endpoint.
            if pq.bounded and pq.viewbox is not None:
                query = dict(query, bbox=pq.viewbox.to_esri_wgs_json())

        endpoint = self._endpoint + '/' + method
        response_obj = self._get_json_obj(endpoint, query)
        returned_candidates = []  # this will be the list returned
        try:
            if method == 'find':
                locations = response_obj['locations']
            else:
                locations = response_obj['candidates']

            for location in locations:
                c = Candidate()
                if method == 'find':  # singlepart
                    attributes = location['feature']['attributes']
                else:  # findAddressCandidates / multipart
                    attributes = location['attributes']
                c.match_addr = attributes['Match_Addr']
                c.locator = attributes['Loc_name']
                c.locator_type = attributes['Addr_Type']
                c.score = attributes['Score']
                c.x = attributes[
                    'DisplayX']  # represents the actual location of the address.
                c.y = attributes['DisplayY']
                c.wkid = response_obj['spatialReference']['wkid']
                c.geoservice = self.__class__.__name__

                # Optional address component fields.
                for in_key, out_key in [('City', 'match_city'),
                                        ('Subregion', 'match_subregion'),
                                        ('Region', 'match_region'),
                                        ('Postal', 'match_postal'),
                                        ('Country', 'match_country')]:
                    setattr(c, out_key, attributes.get(in_key, ''))
                setattr(c, 'match_streetaddr',
                        self._street_addr_from_response(attributes))
                returned_candidates.append(c)
        except KeyError:
            pass
        return returned_candidates

    def _street_addr_from_response(self, attributes):
        """Construct a street address (no city, region, etc.) from a geocoder response.

        :param attributes: A dict of address attributes as returned by the Esri geocoder.
        """
        # The exact ordering of the address component fields that should be
        # used to reconstruct the full street address is not specified in the
        # Esri documentation, but the examples imply that it is this.
        ordered_fields = [
            'AddNum', 'StPreDir', 'StPreType', 'StName', 'StType', 'StDir'
        ]
        result = []
        for field in ordered_fields:
            result.append(attributes.get(field, ''))
        if any(result):
            return ' '.join([s for s in result
                             if s])  # Filter out empty strings.
        else:
            return ''

    def __init__(self, preprocessors=None, postprocessors=None, settings=None):
        preprocessors = EsriWGS.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors
        postprocessors = EsriWGS.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors
        GeocodeService.__init__(self, preprocessors, postprocessors, settings)
Exemplo n.º 8
0
class Bing(GeocodeService):
    """
    Class to geocode using Bing services:
     * `Find a Location by Query <http://msdn.microsoft.com/en-us/library/ff701711.aspx>`_
     * `Find a Location by Address <http://msdn.microsoft.com/en-us/library/ff701714.aspx>`_

    Settings used by the Bing GeocodeService object may include:
     * api_key --  The API key used to access Bing services.

    """
    _endpoint = 'http://dev.virtualearth.net/REST/v1/Locations'

    DEFAULT_PREPROCESSORS = [ReplaceRangeWithNumber()]

    DEFAULT_POSTPROCESSORS = [
        AttrMigrator('confidence', 'score', {
            'High': 100,
            'Medium': 85,
            'Low': 50
        }),
        UseHighScoreIfAtLeast(100),
        AttrFilter([
            'Address', 'AdministrativeBuilding', 'AgriculturalStructure',
            'BusinessName', 'BusinessStructure', 'BusStation', 'Camp',
            'Church', 'CityHall', 'CommunityCenter', 'ConventionCenter',
            'Courthouse', 'Factory', 'FerryTerminal', 'FishHatchery', 'Fort',
            'Garden', 'Geyser', 'Heliport', 'IndustrialStructure',
            'InformationCenter', 'Junction', 'LandmarkBuilding', 'Library',
            'Lighthouse', 'Marina', 'MedicalStructure', 'MetroStation', 'Mine',
            'Mission', 'Monument', 'Mosque', 'Museum', 'NauticalStructure',
            'NavigationalStructure', 'OfficeBuilding', 'ParkAndRide',
            'PlayingField', 'PoliceStation', 'PostOffice', 'PowerStation',
            'Prison', 'RaceTrack', 'ReligiousStructure', 'RestArea', 'Ruin',
            'ShoppingCenter', 'Site', 'SkiArea', 'Spring', 'Stadium', 'Temple',
            'TouristStructure'
        ], 'entity'),
        AttrRename(
            'locator',
            dict(Rooftop='rooftop',
                 Parcel='parcel',
                 ParcelCentroid='parcel',
                 Interpolation='interpolation',
                 InterpolationOffset='interpolation_offset')),
        AttrSorter(
            ['rooftop', 'parcel', 'interpolation_offset', 'interpolation'],
            'locator'),
        AttrSorter(['Address'], 'entity'),
        ScoreSorter(),
        GroupBy(('x', 'y')),
        GroupBy('match_addr')
    ]
    DEFAULT_POSTPROCESSORS = []

    def __init__(self, preprocessors=None, postprocessors=None, settings=None):
        preprocessors = Bing.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors
        postprocessors = Bing.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors
        GeocodeService.__init__(self, preprocessors, postprocessors, settings)

    def _geocode(self, pq):
        if pq.query.strip() == '':
            # No single line query string; use address elements:
            query = {
                'addressLine': pq.address,
                'locality': pq.city,
                'adminDistrict': pq.state,
                'postalCode': pq.postal,
                'countryRegion': pq.country
            }
        else:
            query = {'query': pq.query}

        if pq.viewbox is not None:
            query = dict(query, **{'umv': pq.viewbox.to_bing_str()})
        if hasattr(pq, 'culture'):
            query = dict(query, c=pq.culture)
        if hasattr(pq, 'user_ip'):
            query = dict(query, uip=pq.user_ip)
        if hasattr(pq, 'user_lat') and hasattr(pq, 'user_lon'):
            query = dict(query, **{'ul': '%f,%f' % (pq.user_lat, pq.user_lon)})

        addl_settings = {'key': self._settings['api_key']}
        query = dict(query, **addl_settings)
        response_obj = self._get_json_obj(self._endpoint, query)
        returned_candidates = []  # this will be the list returned
        for r in response_obj['resourceSets'][0]['resources']:
            c = Candidate()
            c.entity = r['entityType']
            c.locator = r['geocodePoints'][0][
                'calculationMethod']  # ex. "Parcel"
            c.confidence = r['confidence']  # High|Medium|Low
            c.match_addr = r[
                'name']  # ex. "1 Microsoft Way, Redmond, WA 98052"
            c.x = r['geocodePoints'][0]['coordinates'][1]  # long, ex. -122.13
            c.y = r['geocodePoints'][0]['coordinates'][0]  # lat, ex. 47.64
            c.wkid = 4326
            c.geoservice = self.__class__.__name__
            returned_candidates.append(c)
        return returned_candidates
Exemplo n.º 9
0
    def setUp(self):
        # Viewbox objects - callowhill is from BSS Spring Garden station to Wash. Sq.
        vb = {
            'callowhill': Viewbox(-75.162628, 39.962769, -75.150963, 39.956322)
        }
        # PlaceQuery objects
        self.pq = {  # North American Addresses:
            'azavea': PlaceQuery('340 N 12th St Ste 402 Philadelphia PA'),
            'ambiguous_azavea': PlaceQuery('340 12th St Ste 402 Philadelphia PA'),
            'zip_plus_4_in_postal_plus_country': PlaceQuery(postal='19127-1115', country='US'),
            'wolf': PlaceQuery('Wolf Building'),
            'wolf_philly': PlaceQuery('Wolf Building, Philadelphia PA'),
            'wolf_bounded': PlaceQuery('Wolf Building', bounded=True, viewbox=vb['callowhill']),
            'bounded_340_12th': PlaceQuery('340 12th St, Philadelphia PA',
                                           bounded=True, viewbox=vb['callowhill']),
            'alpha_774R_W_Central_Ave': PlaceQuery('774R W Central Ave Alpha NJ'),
            'alpha_774_W_Central_Ave_Rear': PlaceQuery('774 W Central Ave Rear, Alpha NJ'),
            '8_kirkbride': PlaceQuery('8 Kirkbride Rd 08822'),
            'george_washington': PlaceQuery('201 W Montmorency Blvd, George, Washington'),
            'pine_needles_dr': PlaceQuery('11761 pine needles providence forge'),
            'pine_needles_ct': PlaceQuery('5328 pine needles providence forge'),
            'pine_needles_terr': PlaceQuery('5359 pine needles providence forge'),
            'moorestown_hyphenated': PlaceQuery('111-113 W Main St Moorestown NJ'),
            'willow_street': PlaceQuery('2819F Willow Street Pike Willow Street PA'),
            'willow_street_parts': PlaceQuery(address='2819F Willow Street Pike',
                                              city='Willow Street', state='PA', country='US'),
            'quebec': PlaceQuery('756 Rue Berri Montreal QC', country='CA'),
            'quebec_accent': PlaceQuery('527 Ch. Beauséjour, Saint-Elzéar-de-Témiscouata QC'),
            'quebec_hyphenated': PlaceQuery('227-227A Rue Commerciale, Saint-Louis-du-Ha! Ha! QC'),
            'senado_mx': PlaceQuery('Paseo de la Reforma 135, Tabacalera, Cuauhtémoc, Distrito Federal, 06030'),
            'senado_mx_struct': PlaceQuery(address='Paseo de la Reforma 135', neighborhood='Tabacalera, Cuauhtémoc', subregion='', state='Distrito Federal', postal='06030', country='MX'),
            'robert_cheetham': PlaceQuery('Robert Cheetham, Philadelphia'),
            # European Addresses:
            'london_pieces': PlaceQuery(address='31 Maiden Lane', city='London', country='UK'),
            'london_one_line': PlaceQuery('31 Maiden Lane, London WC2E', country='UK'),
            'london_pieces_hyphenated': PlaceQuery(address='31-32 Maiden Lane', city='London',
                                                   country='UK'),
            'london_one_line_hyphenated': PlaceQuery('31-32 Maiden Lane London WC2E', country='UK'),
            # Oceanian Addresses:
            'karori': PlaceQuery('102 Karori Road Karori Wellington', country='NZ'),
        }

        if BING_MAPS_API_KEY is not None:
            bing_settings = dict(api_key=BING_MAPS_API_KEY)
            self.g_bing = Geocoder(
                [['omgeo.services.Bing', {
                    'settings': bing_settings
                }]])

        if MAPQUEST_API_KEY is not None:
            mapquest_settings = dict(api_key=MAPQUEST_API_KEY)
            self.g_mapquest = Geocoder(
                [['omgeo.services.MapQuest', {
                    'settings': mapquest_settings
                }]])
            self.g_mapquest_ssl = Geocoder([[
                'omgeo.services.MapQuestSSL', {
                    'settings': mapquest_settings
                }
            ]])

        if PELIAS_API_KEY is not None:
            pelias_settings = dict(api_key=PELIAS_API_KEY)
            self.g_pelias = Geocoder(
                [['omgeo.services.Pelias', {
                    'settings': pelias_settings
                }]])

        if GOOGLE_API_KEY is not None:
            self.g_google = Geocoder([[
                'omgeo.services.Google', {
                    'settings': {
                        'api_key': GOOGLE_API_KEY
                    }
                }
            ]])
            self.g_google_wo_postprocess = Geocoder([[
                'omgeo.services.Google', {
                    'settings': {
                        'api_key': GOOGLE_API_KEY
                    },
                    'postprocessors': []
                }
            ]])

        #: main geocoder used for tests, using default APIs
        self.g = Geocoder()

        # geocoders using individual services
        self.g_esri_wgs = Geocoder([['omgeo.services.EsriWGS', {}]])

        if ESRI_CLIENT_ID is not None and ESRI_CLIENT_SECRET is not None:
            self.g_esri_wgs_auth = Geocoder([[
                'omgeo.services.EsriWGS', {
                    'settings': {
                        'client_id': ESRI_CLIENT_ID,
                        'client_secret': ESRI_CLIENT_SECRET
                    }
                }
            ]])

        if MAPQUEST_API_KEY is not None:  # MapQuest's open Nominatime API now also requires a key
            self.g_nom = Geocoder([['omgeo.services.Nominatim', {}]])

        self.g_census = Geocoder([['omgeo.services.USCensus', {}]])

        ESRI_WGS_LOCATOR_MAP = {
            'PointAddress': 'rooftop',
            'StreetAddress': 'interpolation',
            'PostalExt': 'postal_specific',  # accept ZIP+4
            'Postal': 'postal'
        }
        ESRI_WGS_POSTPROCESSORS_POSTAL_OK = [
            AttrExclude(
                ['USA.Postal'], 'locator'
            ),  # accept postal from everywhere but US (need PostalExt)
            AttrFilter(
                ['PointAddress', 'StreetAddress', 'PostalExt', 'Postal'],
                'locator_type'),
            AttrSorter(
                ['PointAddress', 'StreetAddress', 'PostalExt', 'Postal'],
                'locator_type'),
            AttrRename('locator', ESRI_WGS_LOCATOR_MAP
                       ),  # after filter to avoid searching things we toss out
            UseHighScoreIfAtLeast(99.8),
            ScoreSorter(),
            GroupBy('match_addr'),
            GroupBy(('x', 'y')),
        ]
        GEOCODERS_POSTAL_OK = [[
            'omgeo.services.EsriWGS', {
                'postprocessors': ESRI_WGS_POSTPROCESSORS_POSTAL_OK
            }
        ]]
        self.g_esri_wgs_postal_ok = Geocoder(GEOCODERS_POSTAL_OK)

        #: geocoder with fast timeout
        self.impatient_geocoder = Geocoder(
            [['omgeo.services.EsriWGS', {
                'settings': {
                    'timeout': 0.001
                }
            }]])
Exemplo n.º 10
0
class EsriWGS(GeocodeService):
    """
    Class to geocode using the `ESRI World Geocoding service
    <https://developers.arcgis.com/features/geocoding/>`_.

    This uses two endpoints -- one for single-line addresses,
    and one for multi-part addresses.

    An optional ``key`` parameter can be passed to the :class:`~omgeo.places.PlaceQuery`
    which will be passed as a ``magicKey`` to the find endpoint if
    using a single line address/text search. This allows omgeo
    to be used with the `Esri suggest endpoint
    <https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm>`_.

    .. warning::

        Based on tests using the magicKey parameter, it is
        recommended that a viewbox not be used with in conjuction
        with the magicKey. Additionally, address/search text passed
        via the query may be ignored when using a magicKey.

    An optional ``for_storage`` flag can be passed to the :class:`~omgeo.places.PlaceQuery` which
    will cause ``findStorage=true`` to be passed to the find endpoint.
    The Esri terms of service requires this if the returned point will be stored
    in a database.
    If you pass this flag, you `must` set the client_id and client_secret settings.

    Settings used by the EsriWGS GeocodeService object may include:
     * client_id --  The Client ID used to access Esri services.
     * client_secret --  The Client Secret used to access Esri services.

    """

    LOCATOR_MAP = {
        'PointAddress': 'rooftop',
        'StreetAddress': 'interpolation',
        'PostalExt': 'postal_specific',  # accept ZIP+4
        'Postal': 'postal'
    }

    DEFAULT_PREPROCESSORS = [CancelIfPOBox()]

    DEFAULT_POSTPROCESSORS = [
        AttrFilter(
            [
                'PointAddress',
                'StreetAddress',
                # 'PostalExt',
                # 'Postal'
            ],
            'locator_type'),
        # AttrExclude(['USA_Postal'], 'locator'), #accept postal from everywhere but US (need PostalExt)
        AttrSorter(
            [
                'PointAddress',
                'StreetAddress',
                # 'PostalExt',
                # 'Postal'
            ],
            'locator_type'),
        AttrRename(
            'locator',
            LOCATOR_MAP),  # after filter to avoid searching things we toss out
        UseHighScoreIfAtLeast(99.8),
        ScoreSorter(),
        GroupBy('match_addr'),
        GroupBy(('x', 'y')),
    ]

    _endpoint = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer'

    def __init__(self, preprocessors=None, postprocessors=None, settings=None):
        preprocessors = EsriWGS.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors
        postprocessors = EsriWGS.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors
        settings = {} if settings is None else settings

        if 'client_id' in settings and 'client_secret' in settings:
            self._authenticated = True
            self._client_id = settings['client_id']
            self._client_secret = settings['client_secret']
            self._token = None
        elif 'client_id' not in settings and 'client_secret' not in settings:
            self._authenticated = False
        else:
            raise Exception(
                'Must specify both client_id and client_secret to use authentication'
            )

        GeocodeService.__init__(self, preprocessors, postprocessors, settings)

    def _geocode(self, pq):
        """
        :arg PlaceQuery pq: PlaceQuery object to use for geocoding
        :returns: list of location Candidates
        """
        #: List of desired output fields
        #: See `ESRI docs <https://developers.arcgis.com/rest/geocode/api-reference/geocoding-geocode-addresses.htm>_` for details
        outFields = (
            'Loc_name',
            # 'Shape',
            'Score',
            'Match_addr',  # based on address standards for the country
            # 'Address', # returned by default
            # 'Country' # 3-digit ISO 3166-1 code for a country. Example: Canada = "CAN"
            # 'Admin',
            # 'DepAdmin',
            # 'SubAdmin',
            # 'Locality',
            # 'Postal',
            # 'PostalExt',
            'Addr_type',
            # 'Type',
            # 'Rank',
            'AddNum',
            'StPreDir',
            'StPreType',
            'StName',
            'StType',
            'StDir',
            # 'Side',
            # 'AddNumFrom',
            # 'AddNumTo',
            # 'AddBldg',
            'City',
            'Subregion',
            'Region',
            'Postal',
            'Country',
            # 'Ymax',
            # 'Ymin',
            # 'Xmin',
            # 'Xmax',
            # 'X',
            # 'Y',
            'DisplayX',
            'DisplayY',
            # 'LangCode',
            # 'Status',
        )
        outFields = ','.join(outFields)
        query = dict(
            f='json',  # default HTML. Other options are JSON and KMZ.
            outFields=outFields,
            # outSR=WKID, defaults to 4326
            maxLocations=20,  # default 1; max is 20
        )

        # Postal-code only searches work in the single-line but not multipart geocoder
        # Remember that with the default postprocessors, postcode-level results will be eliminated
        if pq.query == pq.address == '' and pq.postal != '':
            pq.query = pq.postal

        if pq.query == '':  # multipart
            query = dict(
                query,
                Address=pq.
                address,  # commonly represents the house number and street name of a complete address
                Neighborhood=pq.neighborhood,
                City=pq.city,
                Subregion=pq.subregion,
                Region=pq.state,
                Postal=pq.postal,
                # PostalExt=
                CountryCode=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
        else:  # single-line
            magic_key = pq.key if hasattr(pq, 'key') else ''
            query = dict(
                query,
                singleLine=pq.
                query,  # This can be a street address, place name, postal code, or POI.
                sourceCountry=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
            if magic_key:
                query[
                    'magicKey'] = magic_key  # This is a lookup key returned from the suggest endpoint.

        if pq.bounded and pq.viewbox is not None:
            query = dict(query, searchExtent=pq.viewbox.to_esri_wgs_json())

        if self._authenticated:
            if self._token is None or self._token_expiration < datetime.utcnow(
            ):
                expiration = timedelta(hours=2)
                self._token = self.get_token(expiration)
                self._token_expiration = datetime.utcnow() + expiration

            query['token'] = self._token

        if getattr(pq, 'for_storage', False):
            query['forStorage'] = 'true'

        endpoint = self._endpoint + '/findAddressCandidates'
        response_obj = self._get_json_obj(endpoint, query)
        returned_candidates = []  # this will be the list returned
        try:
            locations = response_obj['candidates']
            for location in locations:
                c = Candidate()
                attributes = location['attributes']
                c.match_addr = attributes['Match_addr']
                c.locator = attributes['Loc_name']
                c.locator_type = attributes['Addr_type']
                c.score = attributes['Score']
                c.x = attributes[
                    'DisplayX']  # represents the actual location of the address.
                c.y = attributes['DisplayY']
                c.wkid = response_obj['spatialReference']['wkid']
                c.geoservice = self.__class__.__name__

                # Optional address component fields.
                for in_key, out_key in [('City', 'match_city'),
                                        ('Subregion', 'match_subregion'),
                                        ('Region', 'match_region'),
                                        ('Postal', 'match_postal'),
                                        ('Country', 'match_country')]:
                    setattr(c, out_key, attributes.get(in_key, ''))
                setattr(c, 'match_streetaddr',
                        self._street_addr_from_response(attributes))
                returned_candidates.append(c)
        except KeyError:
            pass
        return returned_candidates

    def _street_addr_from_response(self, attributes):
        """Construct a street address (no city, region, etc.) from a geocoder response.

        :param attributes: A dict of address attributes as returned by the Esri geocoder.
        """
        # The exact ordering of the address component fields that should be
        # used to reconstruct the full street address is not specified in the
        # Esri documentation, but the examples imply that it is this.
        ordered_fields = [
            'AddNum', 'StPreDir', 'StPreType', 'StName', 'StType', 'StDir'
        ]
        result = []
        for field in ordered_fields:
            result.append(attributes.get(field, ''))
        if any(result):
            return ' '.join([s for s in result
                             if s])  # Filter out empty strings.
        else:
            return ''

    def get_token(self, expires=None):
        """
        :param expires: The time until the returned token expires.
            Must be an instance of :class:`datetime.timedelta`.
            If not specified, the token will expire in 2 hours.
        :returns: A token suitable for use with the Esri geocoding API
        """
        endpoint = 'https://www.arcgis.com/sharing/rest/oauth2/token/'
        query = {
            'client_id': self._client_id,
            'client_secret': self._client_secret,
            'grant_type': 'client_credentials'
        }

        if expires is not None:
            if not isinstance(expires, timedelta):
                raise Exception(
                    'If expires is provided it must be a timedelta instance')
            query['expiration'] = int(expires.total_seconds() / 60)

        response_obj = self._get_json_obj(endpoint, query, is_post=True)

        return response_obj['access_token']