python-package/lets_plot/geo_data/geocoder.py (370 lines of code) (raw):

# Copyright (c) 2020. JetBrains s.r.o. # Use of this source code is governed by the MIT license that can be found in the LICENSE file. from collections import namedtuple from collections.abc import Iterable from typing import Union, List, Optional, Dict from pandas import Series from .geocodes import _to_level_kind, request_types, Geocodes, _raise_exception, _ensure_is_list from .gis.geocoding_service import GeocodingService from .gis.geometry import GeoRect, GeoPoint from .gis.request import RequestBuilder, GeocodingRequest, RequestKind, MapRegion, AmbiguityResolver, \ RegionQuery, LevelKind, IgnoringStrategyKind, PayloadKind, ReverseGeocodingRequest from .gis.response import Response, SuccessResponse from .type_assertion import assert_list_type NAMESAKE_MAX_COUNT = 10 ShapelyPointType = 'shapely.geometry.Point' ShapelyPolygonType = 'shapely.geometry.Polygon' QuerySpec = namedtuple('QuerySpec', 'name, county, state, country') WhereSpec = namedtuple('WhereSpec', 'scope, ambiguity_resolver') parent_types = Optional[Union[str, Geocodes, 'Geocoder', MapRegion, List]] # list of same types scope_types = Optional[Union[str, Geocodes, 'Geocoder', ShapelyPolygonType]] def _to_scope(location: scope_types) -> Optional[Union[List[MapRegion], MapRegion]]: if location is None: return None def _make_region(obj: Union[str, Geocodes]) -> Optional[MapRegion]: if isinstance(obj, Geocodes): return MapRegion.scope(obj.unique_ids()) if isinstance(obj, str): return MapRegion.with_name(obj) raise ValueError('Unsupported scope type. Expected Geocoder, str or list, but was `{}`'.format(type(obj))) if isinstance(location, list): return [_make_region(obj) for obj in location] return _make_region(location) class LazyShapely: @staticmethod def is_point(p) -> bool: if not LazyShapely._is_shapely_available(): return False from shapely.geometry import Point return isinstance(p, Point) @staticmethod def is_polygon(p): if not LazyShapely._is_shapely_available(): return False from shapely.geometry import Polygon return isinstance(p, Polygon) @staticmethod def _is_shapely_available(): try: import shapely return True except ImportError: return False def _make_ambiguity_resolver(ignoring_strategy: Optional[IgnoringStrategyKind] = None, scope: Optional[ShapelyPolygonType] = None, closest_object: Optional[Union[Geocodes, ShapelyPointType]] = None): if LazyShapely.is_polygon(scope): rect = GeoRect(start_lon=scope.bounds[0], min_lat=scope.bounds[1], end_lon=scope.bounds[2], max_lat=scope.bounds[3]) elif scope is None: rect = None else: assert scope is not None # else for empty scope - existing scope should be already handled raise ValueError('Wrong type of parameter `scope` - expected `shapely.geometry.Polygon`, but was `{}`'.format( type(scope).__name__)) return AmbiguityResolver( ignoring_strategy=ignoring_strategy, closest_coord=_to_geo_point(closest_object), box=rect ) def _to_geo_point(closest_place: Optional[Union[Geocodes, ShapelyPointType]]) -> Optional[GeoPoint]: if closest_place is None: return None if isinstance(closest_place, Geocoder): closest_place = closest_place._geocode() if isinstance(closest_place, Geocodes): closest_place_id = closest_place.as_list()[0].unique_ids() assert len(closest_place_id) == 1 request = RequestBuilder() \ .set_request_kind(RequestKind.explicit) \ .set_requested_payload([PayloadKind.centroids]) \ .set_ids(closest_place_id) \ .build() response: Response = GeocodingService().do_request(request) if isinstance(response, SuccessResponse): assert len(response.features) == 1 centroid = response.features[0].centroid return GeoPoint(lon=centroid.lon, lat=centroid.lat) else: raise ValueError("Unexpected geocoding response for id " + str(closest_place_id[0])) if LazyShapely.is_point(closest_place): return GeoPoint(lon=closest_place.x, lat=closest_place.y) raise ValueError('Not supported type: {}'.format(type(closest_place))) def _get_or_none(list, index): if index >= len(list): return None return list[index] def _ensure_is_parent_list(obj): if obj is None: return None if isinstance(obj, Geocoder): obj = obj._geocode() if isinstance(obj, Geocodes): return obj.as_list() if isinstance(obj, Iterable) and not isinstance(obj, str): return [v for v in obj] return [obj] def _make_parents(values: parent_types) -> List[Optional[MapRegion]]: values = _ensure_is_parent_list(values) if values is None: return [] return list(map(lambda v: _make_parent_region(v) if values is not None else None, values)) def _make_parent_region(place: parent_types) -> Optional[MapRegion]: if place is None: return None if isinstance(place, Geocoder): place = place._geocode() if isinstance(place, str): return MapRegion.with_name(place) if isinstance(place, Geocodes): assert len(place.to_map_regions()) == 1, 'Region object used as parent should contain only single record' return place.to_map_regions()[0] raise ValueError('Unsupported parent type: ' + str(type(place))) class Geocoder: """ Do not use this class explicitly. Instead you should construct its objects with special functions: `geocode() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode.html>`__, `geocode_cities() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_cities.html>`__, `geocode_counties() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_counties.html>`__, `geocode_states() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_states.html>`__, `geocode_countries() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_countries.html>`__, ``reverse_geocode()``. """ def __init__(self): """Initialize self.""" self._inc_res = 0 def get_limits(self) -> 'GeoDataFrame': """ Return bboxes (Polygon geometry) for given regions in form of ``GeoDataFrame``. For regions intersecting anti-meridian bbox will be divided into two parts and stored as two rows. Returns ------- ``GeoDataFrame`` Table of data. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 5 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() countries = geocode_countries(['Germany', 'Poland']).get_limits() display(countries) ggplot() + geom_rect(aes(fill='found name'), data=countries, color='white') """ return self._geocode().limits() def get_centroids(self) -> 'GeoDataFrame': """ Return centroids (Point geometry) for given regions in form of ``GeoDataFrame``. Returns ------- ``GeoDataFrame`` Table of data. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 5 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() countries = geocode_countries(['Germany', 'Poland']).get_centroids() display(countries) ggplot() + geom_point(aes(color='found name'), data=countries, size=10) """ return self._geocode().centroids() def get_boundaries(self, resolution=None) -> 'GeoDataFrame': """ Return boundaries for given regions in the form of ``GeoDataFrame``. Parameters ---------- resolution : int or str Boundaries resolution. Returns ------- ``GeoDataFrame`` Table of data. Notes ----- If ``resolution`` has int type, it may take one of the following values: - 1-3 for world scale view, - 4-6 for country scale view, - 7-9 for state scale view, - 10-12 for county scale view, - 13-15 for city scale view. Here value 1 corresponds to maximum performance and 15 - to maximum quality. If ``resolution`` is of str type, it may take one of the following values: - 'world' corresponds to int value 2, - 'country' corresponds to int value 5, - 'state' corresponds to int value 8, - 'county' corresponds to int value 11, - 'city' corresponds to int value 14. Here value 'world' corresponds to maximum performance and 'city' - to maximum quality. The resolution choice depends on the type of displayed area. The number of objects also matters: one state looks good on a 'state' scale while 50 states is a 'country' view. It is allowed to use any resolution for all regions. For example, 'city' scale can be used for a state to get a more detailed boundary when zooming in, or 'world' for a small preview. If ``resolution`` is not specified (or equal to None), it will be auto-detected. Auto-detection by level_kind is used for geocoding and the number of objects. In this case performance is preferred over quality. The pixelated geometries can be obtained. Use explicit resolution or ``inc_res()`` function for better quality. If the number of objects is equal to n, then ``resolution`` will be the following: - For countries: if n < 3 then ``resolution=3``, else ``resolution=1``. - For states: if n < 3 then ``resolution=7``, if n < 10 then ``resolution=4``, else ``resolution=2``. - For counties: if n < 5 then ``resolution=10``, if n < 20 then ``resolution=8``, else ``resolution=3``. - For cities: if n < 5 then ``resolution=13``, if n < 50 then ``resolution=4``, else ``resolution=3``. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 5 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() countries = geocode_countries(['Germany', 'Poland']).inc_res().get_boundaries() display(countries) ggplot() + geom_map(aes(fill='found name'), data=countries, color='white') """ return self._geocode().boundaries(resolution, self._inc_res) def get_geocodes(self) -> 'DataFrame': """ Return metadata for given regions. Returns ------- ``DataFrame`` Table of data. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 2 from lets_plot.geo_data import * geocode_countries(['Germany', 'Russia']).get_geocodes() """ return self._geocode().to_data_frame() def inc_res(self, delta=2): """ Increase auto-detected resolution for boundaries. Parameters ---------- delta : int, default=2 Value that will be added to auto-detected resolution. Returns ------- ``Geocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 5 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() countries = geocode_countries(['Germany', 'Poland']).inc_res().get_boundaries() display(countries) ggplot() + geom_map(aes(fill='found name'), data=countries, color='white') """ self._inc_res = delta return self def _geocode(self) -> Geocodes: raise ValueError('Abstract method') def _to_coords(lon: Optional[Union[float, Series, List[float]]], lat: Optional[Union[float, Series, List[float]]]) -> \ List[GeoPoint]: if type(lon) != type(lat): raise ValueError('lon and lat have different types') if isinstance(lon, float): return [GeoPoint(lon, lat)] if isinstance(lon, Series): lon = lon.tolist() lat = lat.tolist() if isinstance(lon, list): assert_list_type(lon, float) assert_list_type(lat, float) return [GeoPoint(lo, la) for lo, la in zip(lon, lat)] class ReverseGeocoder(Geocoder): """ Do not use this class explicitly. Instead you should construct its objects with special functions: `geocode() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode.html>`__, `geocode_cities() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_cities.html>`__, `geocode_counties() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_counties.html>`__, `geocode_states() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_states.html>`__, `geocode_countries() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_countries.html>`__, ``reverse_geocode()``. """ def __init__(self, lon, lat, level: Optional[Union[str, LevelKind]], scope=None): """Initialize self.""" Geocoder.__init__(self) self._geocodes: Optional[Geocodes] = None self._request: ReverseGeocodingRequest = RequestBuilder() \ .set_requested_payload([PayloadKind.centroids, PayloadKind.poisitions, PayloadKind.limits]) \ .set_request_kind(RequestKind.reverse) \ .set_reverse_coordinates(_to_coords(lon, lat)) \ .set_level(_to_level_kind(level)) \ .set_reverse_scope(_to_scope(scope)) \ .build() def _geocode(self) -> Geocodes: if self._geocodes is None: response: Response = GeocodingService().do_request(self._request) if not isinstance(response, SuccessResponse): _raise_exception(response) self._geocodes = Geocodes( response.level, response.answers, [RegionQuery(request='[{}, {}]'.format(pt.lon, pt.lat)) for pt in self._request.coordinates], highlights=False ) return self._geocodes class NamesGeocoder(Geocoder): """ Do not use this class explicitly. Instead you should construct its objects with special functions: `geocode() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode.html>`__, `geocode_cities() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_cities.html>`__, `geocode_counties() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_counties.html>`__, `geocode_states() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_states.html>`__, `geocode_countries() <https://lets-plot.org/python/pages/api/lets_plot.geo_data.geocode_countries.html>`__. """ def __init__( self, level: Optional[Union[str, LevelKind]] = None, request: request_types = None ): """Initialize self.""" Geocoder.__init__(self) self._geocodes: Optional[Geocodes] = None self._scope: List[Optional[MapRegion]] = [] self._level: Optional[LevelKind] = _to_level_kind(level) self._default_ambiguity_resolver: AmbiguityResolver = AmbiguityResolver.empty() # TODO rename to geohint self._highlights: bool = False self._allow_ambiguous = False self._countries: List[Optional[MapRegion]] = [] self._states: List[Optional[MapRegion]] = [] self._counties: List[Optional[MapRegion]] = [] self._overridings: Dict[QuerySpec, WhereSpec] = {} # query to scope requests: Optional[List[str]] = _ensure_is_list(request) if requests is not None: self._names: List[Optional[str]] = list(map(lambda name: name if requests is not None else None, requests)) else: self._names = [] def scope(self, scope) -> 'NamesGeocoder': """ Limit area of interest to resolve an ambiguity. Parameters ---------- scope : str or ``Geocoder`` Area of interest. If it is of str type then it should be the geo-object name. If it is of ``Geocoder`` type then it must contain only one object. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 6 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() scope = geocode_states('Kentucky') city = geocode_cities('Franklin').scope(scope).get_boundaries() display(city) ggplot() + geom_map(data=city) + ggtitle('Franklin, Kentucky') """ self._reset_geocodes() self._scope = _prepare_new_scope(scope) return self def highlights(self, v: bool): """ Add matched string to geocodes ``DataFrame``. Doesn't affect ``GeoDataFrame``. Parameters ---------- v : bool If True geocodes ``DataFrame`` will contain column 'highlights' with string that matched the name. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 2 from lets_plot.geo_data import * geocode(names='OH').allow_ambiguous().highlights(True).get_geocodes() """ self._highlights = v return self def countries(self, countries): """ Set parents for 'country' level to resolve an ambiguity or to join geometry with data via multi-key. Parameters ---------- countries : str or ``Geocoder`` or list Parents for 'country' level. If it is of str type then it should be the country name. If it is of ``Geocoder`` type then it must contain the same number of values as the number of names of ``Geocoder``. If it is of list type then it must be the same size as the number of names of ``Geocoder``. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 5 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() cities = geocode_cities(['Boston', 'Boston']).countries(['US', 'UK']).get_centroids() display(cities) ggplot() + geom_livemap() + geom_point(data=cities, color='red', size=5) """ self._reset_geocodes() self._countries = _make_parents(countries) return self def states(self, states) -> 'NamesGeocoder': """ Set parents for 'state' level to resolve an ambiguity or to join geometry with data via multi-key. Parameters ---------- states : str or ``Geocoder`` or list Parents for 'state' level. If it is of str type then it should be the state name. If it is of ``Geocoder`` type then it must contain the same number of values as the number of names of ``Geocoder``. If it is of list type then it must be the same size as the number of names of ``Geocoder``. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 6 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() states = geocode_states(['Massachusetts', 'New York']) cities = geocode_cities(['Boston', 'Boston']).states(states).get_centroids() display(cities) ggplot() + geom_livemap() + geom_point(data=cities, color='red', size=5) """ self._reset_geocodes() self._states = _make_parents(states) return self def counties(self, counties: parent_types) -> 'NamesGeocoder': """ Set parents for 'county' level to resolve an ambiguity or to join geometry with data via multi-key. Parameters ---------- counties : str or ``Geocoder`` or list Parents for 'county' level. If it is of str type then it should be the county name. If it is of ``Geocoder`` type then it must contain the same number of values as the number of names of ``Geocoder``. If it is of list type then it must be the same size as the number of names of ``Geocoder``. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 7 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() counties = geocode_counties(['Suffolk County', 'Erie County'])\\ .states(['Massachusetts', 'New York']) cities = geocode_cities(['Boston', 'Boston']).counties(counties).get_centroids() display(cities) ggplot() + geom_livemap() + geom_point(data=cities, color='red', size=5) """ self._reset_geocodes() self._counties = _make_parents(counties) return self def ignore_not_found(self) -> 'NamesGeocoder': """ Remove not found objects from the result. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 6 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() countries = geocode_countries(['Germany', 'Hungary', 'Czechoslovakia'])\\ .ignore_not_found().get_boundaries(6) display(countries) ggplot() + geom_map(aes(fill='found name'), data=countries, color='white') """ self._reset_geocodes() self._default_ambiguity_resolver = AmbiguityResolver(IgnoringStrategyKind.skip_missing) return self def ignore_all_errors(self) -> 'NamesGeocoder': """ Remove objects that have multiple matches from the result. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 6 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() cities = geocode_cities(['Boston', 'Worcester', 'Barnstable'])\\ .ignore_all_errors().get_centroids() display(cities) ggplot() + geom_livemap() + geom_point(data=cities, color='red', size=5) """ self._reset_geocodes() self._default_ambiguity_resolver = AmbiguityResolver(IgnoringStrategyKind.skip_all) return self def allow_ambiguous(self) -> 'NamesGeocoder': """ For objects that have multiple matches add all of them to the result. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 6 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() cities = geocode_cities('Worcester').scope('US')\\ .allow_ambiguous().get_centroids() display(cities) ggplot() + geom_livemap() + geom_point(data=cities, color='red', size=5) """ self._reset_geocodes() self._default_ambiguity_resolver = AmbiguityResolver(IgnoringStrategyKind.take_namesakes) self._allow_ambiguous = True return self def where(self, name: str, county: Optional[parent_types] = None, state: Optional[parent_types] = None, country: Optional[parent_types] = None, scope: scope_types = None, closest_to: Optional[Union[Geocodes, ShapelyPointType]] = None ) -> 'NamesGeocoder': """ Allows to resolve ambiguity by setting up extra parameters. Combination of name, county, state, country identifies a row with an ambiguity. If row with given names does not exist error will be generated. Parameters ---------- name : str Name in ``Geocoder`` that needs better qualification. county : str If ``Geocoder`` has parent counties this field must be present to identify a row for the name. state : str If ``Geocoder`` has parent states this field must be present to identify a row for the name. country : str If ``Geocoder`` has parent countries this field must be present to identify a row for the name. scope : str or ``Geocoder`` or ``shapely.geometry.Polygon`` Limits area of geocoding. If parent country is set then error will be generated. If type is a str - geoobject should have geocoded scope in parents. If type is a ``Geocoder`` - geoobject should have geocoded scope in parents. Scope should contain only one entry. If type is a ``shapely.geometry.Polygon`` - geoobject centroid should fall into bbox of the polygon. closest_to : ``Geocoder`` or ``shapely.geometry.Point`` Resolve ambiguity by taking closest geoobject. Returns ------- ``NamesGeocoder`` Geocoder object specification. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 6 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() city = geocode_cities('Warwick').countries('US')\\ .where(name='Warwick', country='US', scope='Massachusetts').get_centroids() display(city) ggplot() + geom_livemap() + geom_point(data=city, color='red', size=5) | .. jupyter-execute:: :linenos: :emphasize-lines: 7 from IPython.display import display from lets_plot import * from lets_plot.geo_data import * LetsPlot.setup_html() closest_city = geocode_cities('Birmingham').get_centroids().iloc[0].geometry city = geocode_cities('Warwick')\\ .where(name='Warwick', closest_to=closest_city).get_centroids() display(city) ggplot() + geom_livemap() + geom_point(data=city, color='red', size=5) """ self._reset_geocodes() query_spec = QuerySpec( name, _make_parent_region(county), _make_parent_region(state), _make_parent_region(country) ) def query_exist(query): for i in range(len(self._names)): if query.name == self._names[i] and \ query.country == _get_or_none(self._countries, i) and \ query.state == _get_or_none(self._states, i) and \ query.county == _get_or_none(self._counties, i): return True return False if not query_exist(query_spec): parents: List[str] = [] if query_spec.county is not None: parents.append('county={}'.format(str(query_spec.county))) if query_spec.state is not None: parents.append('state={}'.format(str(query_spec.state))) if query_spec.country is not None: parents.append('country={}'.format(str(query_spec.country))) parents_str = ", ".join(parents) if len(parents_str) == 0: raise ValueError("{} is not found in names".format(name)) else: raise ValueError("{}({}) is not found in names".format(name, parents_str)) if scope is None: new_scope = None ambiguity_resolver = _make_ambiguity_resolver(scope=None, closest_object=closest_to) else: if LazyShapely.is_polygon(scope): new_scope = None ambiguity_resolver = _make_ambiguity_resolver(scope=scope, closest_object=closest_to) else: new_scope = _prepare_new_scope(scope)[0] ambiguity_resolver = _make_ambiguity_resolver(scope=None, closest_object=closest_to) self._overridings[query_spec] = WhereSpec(new_scope, ambiguity_resolver) return self def _build_request(self) -> GeocodingRequest: if len(self._names) == 0: def to_scope(parents): if len(parents) == 0: return None elif len(parents) == 1: return parents[0] else: raise ValueError( 'Too many parent objects. Expcted single object instead of {}'.format(len(parents)) ) # all countries/states etc. We need one dummy query queries = [ RegionQuery( request=None, country=to_scope(self._countries), state=to_scope(self._states), county=to_scope(self._counties) ) ] else: def assert_parents_size(parents: List, parents_level: str): if len(parents) == 0: return if len(parents) != len(self._names): raise ValueError( 'Invalid request: {} count({}) != names count({})' .format(parents_level, len(parents), len(self._names)) ) if len(self._countries) > 0 and len(self._scope) > 0: raise ValueError("Invalid request: countries and scope can't be used simultaneously") assert_parents_size(self._countries, 'countries') assert_parents_size(self._states, 'states') assert_parents_size(self._counties, 'counties') queries = [] for i in range(len(self._names)): name = self._names[i] country = _get_or_none(self._countries, i) state = _get_or_none(self._states, i) county = _get_or_none(self._counties, i) scope, ambiguity_resolver = self._overridings.get( QuerySpec(name, county, state, country), WhereSpec(None, self._default_ambiguity_resolver) ) query = RegionQuery( request=name, country=country, state=state, county=county, scope=scope, ambiguity_resolver=ambiguity_resolver ) queries.append(query) request = RequestBuilder() \ .set_request_kind(RequestKind.geocoding) \ .set_queries(queries) \ .set_scope(self._scope) \ .set_level(self._level) \ .set_namesake_limit(NAMESAKE_MAX_COUNT) \ .set_allow_ambiguous(self._allow_ambiguous) payload = [PayloadKind.limits, PayloadKind.poisitions, PayloadKind.centroids] if self._highlights: payload.append(PayloadKind.highlights) request.set_requested_payload(payload) return request.build() def _geocode(self) -> Geocodes: if self._geocodes is None: request: GeocodingRequest = self._build_request() response: Response = GeocodingService().do_request(request) if not isinstance(response, SuccessResponse): _raise_exception(response) self._geocodes = Geocodes(response.level, response.answers, request.region_queries, self._highlights) return self._geocodes def _reset_geocodes(self): self._geocodes = None def __eq__(self, o): return isinstance(o, NamesGeocoder) \ and self._overridings == o._overridings def __ne__(self, o): return not self == o def _prepare_new_scope(scope: Optional[Union[str, Geocoder, Geocodes, MapRegion]]) -> List[MapRegion]: """ Return list of MapRegions. Every MapRegion object contains only one name or id. """ if scope is None: return [] def assert_scope_length_(l): if l != 1: raise ValueError("'scope' has {} entries, but expected to have exactly 1".format(l)) if isinstance(scope, MapRegion): assert_scope_length_(len(scope.values)) return [scope] if isinstance(scope, str): return [MapRegion.with_name(scope)] if isinstance(scope, Geocoder): scope = scope._geocode() if isinstance(scope, Geocodes): map_regions = scope.to_map_regions() assert_scope_length_(len(map_regions)) return map_regions raise ValueError("Unsupported 'scope' type. Expected 'str' or 'Geocoder' but was '{}'".format(type(scope).__name__))