python-package/lets_plot/geo_data/gis/request.py (380 lines of code) (raw):
import enum
from numbers import Number
from typing import Optional, List, Tuple, Union
from .geometry import GeoRect, GeoPoint
from ..type_assertion import assert_type, assert_list_type, assert_optional_type
MISSING_SCOPE_OR_REQUEST_EXCEPTION_TEXT = 'Missing required argument: scope or request.'
MISSING_LEVEL_OR_REQUEST_EXCEPTION_TEXT = 'Missing required argument: level or request.'
MISSING_LEVEL_AND_SCOPE_OR_REQUEST_EXCEPTION_TEXT = 'Missing required argument. You must enter level and scope or request.'
GeoId = str
class PayloadKind(enum.Enum):
highlights = 'highlights'
centroids = 'centroid'
boundaries = 'boundary'
limits = 'limit'
poisitions = 'position'
class RequestKind(enum.Enum):
explicit = 'by_id'
geocoding = 'by_geocoding'
reverse = 'reverse'
class IgnoringStrategyKind(enum.Enum):
skip_all = 'skip_all'
skip_missing = 'skip_missing'
skip_ambiguous = 'skip_ambiguous'
take_namesakes = 'take_namesakes'
class LevelKind(enum.Enum):
country = 'country'
state = 'state'
county = 'county'
city = 'city'
MODE_BY_GEOCODING = 'by_geocoding'
MODE_BY_ID = 'by_id'
class MapRegionKind(enum.Enum):
id = True
name = False
place = 'place'
class MapRegion:
'''
Represents three different entities:
scope - ids of already geocoded objects. The only kind of MapRegion allowed to store multiply objects
place - already geocoded single place. In addition to id it holds administrative level and requeted name.
Used mostly as parent object for geocoding other objects.
with_name - single name, not yet geocoded.
'''
@staticmethod
def name_or_none(place: Optional['MapRegion']):
if place is None:
return None
if place.kind == MapRegionKind.place:
return place.request()
if place.kind == MapRegionKind.name:
return place.name()
raise ValueError('MapRegion with kind \'{}\' doesn\'t have a name'.format(place.kind))
@staticmethod
def place(id: str, request: Optional[str], level_kind: LevelKind):
assert_type(id, str)
assert_optional_type(request, str)
assert_type(level_kind, LevelKind)
return MapRegion(MapRegionKind.place, [id], request, level_kind)
@staticmethod
def scope(parent_ids: List[str]):
assert_list_type(parent_ids, str)
return MapRegion(MapRegionKind.id, parent_ids)
@staticmethod
def with_name(name: str):
assert_type(name, str)
return MapRegion(MapRegionKind.name, [name])
def __init__(self, kind: MapRegionKind, values: List[str], request: Optional[str] = None, level_kind: Optional[LevelKind] = None):
assert_type(kind, MapRegionKind)
assert_list_type(values, str)
assert_optional_type(request, str)
assert_optional_type(level_kind, LevelKind)
self.kind: MapRegionKind = kind
self.values: Tuple[str] = tuple(values, )
self._request:Optional[str] = request
self._level_kind: Optional[LevelKind] = level_kind
self._hash = hash((self.values, self.kind))
def request(self) -> Optional[str]:
assert self.kind == MapRegionKind.place, 'Invalid MapRegion kind. Expected \'place\', but was ' + str(self.kind)
assert_optional_type(self._request, str)
return self._request
def name(self) -> str:
assert self.kind == MapRegionKind.name, 'Invalid MapRegion kind. Expected \'name\', but was ' + str(self.kind)
assert_type(self.values[0], str)
return self.values[0]
def level_kind(self) -> Optional[LevelKind]:
assert self.kind == MapRegionKind.place, 'Invalid MapRegion kind: only place contains level_kind'
return self._level_kind
def __eq__(self, other: 'MapRegion'):
return isinstance(other, MapRegion) \
and self.kind == other.kind \
and self.values == other.values \
and self._request == other._request \
and self._level_kind == other._level_kind
def __ne__(self, o: object) -> bool:
return not self == o
def __str__(self):
if self.kind == MapRegionKind.place:
return '{} {} {}'.format(str(self.values), self._request, self._level_kind)
if self.kind == MapRegionKind.name:
return self.values[0]
if self.kind == MapRegionKind.id:
return ",".join(self.values)
return str(self.values)
def __hash__(self):
return self._hash
class AmbiguityResolver:
@staticmethod
def empty() -> 'AmbiguityResolver':
return AmbiguityResolver()
def __init__(self,
ignoring_strategy: Optional[IgnoringStrategyKind] = None,
closest_coord: Optional[GeoPoint] = None,
box: Optional[GeoRect] = None):
assert_optional_type(ignoring_strategy, IgnoringStrategyKind)
assert_optional_type(closest_coord, GeoPoint)
assert_optional_type(box, GeoRect)
self.ignoring_strategy: IgnoringStrategyKind = ignoring_strategy
self.closest_coord: Optional[GeoPoint] = closest_coord
self.box: Optional[GeoRect] = box
def __eq__(self, o):
return isinstance(o, AmbiguityResolver) \
and self.ignoring_strategy == o.ignoring_strategy \
and self.closest_coord == o.closest_coord \
and self.box == o.box
def __ne__(self, o):
return not self == o
class RegionQuery:
def __init__(self,
request: Optional[str],
scope: Optional[MapRegion] = None,
ambiguity_resolver: AmbiguityResolver = AmbiguityResolver.empty(),
country: Optional[MapRegion] = None,
state: Optional[MapRegion] = None,
county: Optional[MapRegion] = None
):
assert_optional_type(request, str)
assert_optional_type(scope, MapRegion)
assert_type(ambiguity_resolver, AmbiguityResolver)
assert_optional_type(county, MapRegion)
assert_optional_type(state, MapRegion)
assert_optional_type(country, MapRegion)
self.request: Optional[str] = request
self.scope: Optional[MapRegion] = scope
self.ambiguity_resolver: AmbiguityResolver = ambiguity_resolver
self.country: Optional[MapRegion] = country
self.state: Optional[MapRegion] = state
self.county: Optional[MapRegion] = county
def __eq__(self, o: object) -> bool:
return isinstance(o, RegionQuery) \
and self.request == o.request \
and self.scope == o.scope \
and self.ambiguity_resolver == o.ambiguity_resolver \
and self.country == o.country \
and self.state == o.state \
and self.county == o.county
def __ne__(self, o: object) -> bool:
return not self == o
def __str__(self):
return str(self.request) + ' in ' + str(self.scope)
class Request:
def __init__(self, requested_payload: List[PayloadKind], resolution: Optional[Number]):
assert_list_type(requested_payload, PayloadKind)
assert_optional_type(resolution, Number)
assert requested_payload is not None
self.requested_payload: List[PayloadKind] = requested_payload
self.resolution: Optional[int] = resolution
def __eq__(self, o: object) -> bool:
return isinstance(o, Request) \
and self.requested_payload == o.requested_payload \
and self.resolution == o.resolution
def __ne__(self, o: object) -> bool:
return not self == o
class GeocodingRequest(Request):
@staticmethod
def _check_required_parameters(region_queries: List[RegionQuery],
level: Optional[LevelKind]) -> None:
if len(region_queries) == 0 and not level:
raise ValueError(MISSING_LEVEL_AND_SCOPE_OR_REQUEST_EXCEPTION_TEXT)
for query in region_queries:
if not query.request and not level and not query.scope:
raise ValueError(MISSING_LEVEL_AND_SCOPE_OR_REQUEST_EXCEPTION_TEXT)
if not query.request and not level and query.scope:
raise ValueError(MISSING_LEVEL_OR_REQUEST_EXCEPTION_TEXT)
if not query.request and not level and not query.scope:
raise ValueError(MISSING_SCOPE_OR_REQUEST_EXCEPTION_TEXT)
def __init__(self,
requested_payload: List[PayloadKind],
resolution: Optional[int],
region_queries: List[RegionQuery],
scope: List[MapRegion],
level: Optional[LevelKind],
namesake_example_limit: int,
allow_ambiguous: bool
):
super().__init__(requested_payload, resolution)
assert_list_type(requested_payload, PayloadKind)
assert_optional_type(resolution, int)
assert_list_type(region_queries, RegionQuery)
assert_optional_type(level, LevelKind)
assert_type(namesake_example_limit, int)
assert_type(allow_ambiguous, bool)
self._check_required_parameters(region_queries, level)
assert region_queries is not None
assert namesake_example_limit is not None
self.region_queries: List[RegionQuery] = region_queries
self.scope: List[MapRegion] = scope
self.level: Optional[LevelKind] = level
self.namesake_example_limit: int = namesake_example_limit
self.allow_ambiguous: bool = allow_ambiguous
def __eq__(self, o: object) -> bool:
return isinstance(o, GeocodingRequest) \
and super().__eq__(o) \
and self.region_queries == o.region_queries \
and self.level == o.level \
and self.namesake_example_limit == o.namesake_example_limit \
and self.allow_ambiguous == o.allow_ambiguous
def __ne__(self, o: object) -> bool:
return not self == o
class ExplicitRequest(Request):
def __init__(self,
requested_payload: List[PayloadKind],
ids: List[GeoId],
resolution: Optional[int] = None
):
super().__init__(requested_payload, resolution)
assert_list_type(requested_payload, PayloadKind)
assert_list_type(ids, GeoId)
assert_optional_type(resolution, int)
assert ids is not None
self.geo_object_list: List[GeoId] = ids
def __eq__(self, o: object) -> bool:
return isinstance(o, ExplicitRequest) \
and super().__eq__(o) \
and self.geo_object_list == o.geo_object_list
def __ne__(self, o: object) -> bool:
return not self == o
class ReverseGeocodingRequest(Request):
def __init__(self,
coordinates: List[GeoPoint],
level: LevelKind,
scope: Optional[MapRegion],
requested_payload: List[PayloadKind],
resolution: Optional[int] = None
):
super().__init__(requested_payload, resolution)
assert_list_type(coordinates, GeoPoint)
assert_type(level, LevelKind)
assert_optional_type(scope, MapRegion)
self.coordinates: List[GeoPoint] = coordinates
self.level: LevelKind = level
self.scope: Optional[MapRegion] = scope
def __eq__(self, o: object) -> bool:
return isinstance(o, ReverseGeocodingRequest) \
and super().__eq__(o) \
and self.coordinates == o.coordinates \
and self.level == o.level \
and self.scope == o.scope
def __ne__(self, o: object) -> bool:
return not self == o
class RequestBuilder:
def __init__(self):
self.request_kind: Optional[RequestKind] = None
self.requested_payload: List[PayloadKind] = []
self.resolution: Optional[int] = None
self.ids: List[str] = []
self.region_queries: List[RegionQuery] = []
self.scope: List[MapRegion] = []
self.level: Optional[LevelKind] = None
self.namesake_limit: int = 10
self.allow_ambiguous: bool = False
# reverse
self.reverse_coordinates: Optional[List[GeoPoint]] = None
self.reverse_scope: Optional[MapRegion] = None
def set_reverse_coordinates(self, coordinates: List[GeoPoint]) -> 'RequestBuilder':
assert_list_type(coordinates, GeoPoint)
self.reverse_coordinates = coordinates
return self
def set_reverse_scope(self, region: Optional[MapRegion]) -> 'RequestBuilder':
assert_optional_type(region, MapRegion)
self.reverse_scope = region
return self
def set_request_kind(self, v: RequestKind) -> 'RequestBuilder':
assert_type(v, RequestKind)
self.request_kind = v
return self
def set_requested_payload(self, v: List[PayloadKind]) -> 'RequestBuilder':
assert_list_type(v, PayloadKind)
self.requested_payload = v
return self
def set_resolution(self, v: Optional[int]) -> 'RequestBuilder':
assert_optional_type(v, int)
self.resolution = v
return self
def set_ids(self, v: List[str]) -> 'RequestBuilder':
assert_list_type(v, str)
self.ids = v
return self
def set_queries(self, v: List[RegionQuery]) -> 'RequestBuilder':
assert_list_type(v, RegionQuery)
self.region_queries = v
return self
def set_scope(self, v: List[MapRegion]) -> 'RequestBuilder':
assert_list_type(v, MapRegion)
self.scope = v
return self
def set_level(self, v: LevelKind) -> 'RequestBuilder':
assert_optional_type(v, LevelKind)
self.level = v
return self
def set_namesake_limit(self, v: int) -> 'RequestBuilder':
assert_optional_type(v, int)
self.namesake_limit = v
return self
def set_allow_ambiguous(self, v: bool) -> 'RequestBuilder':
assert_optional_type(v, bool)
self.allow_ambiguous = v
return self
def build(self) -> Union[ExplicitRequest, GeocodingRequest, ReverseGeocodingRequest]:
if self.request_kind == RequestKind.explicit:
return ExplicitRequest(self.requested_payload, self.ids, self.resolution)
elif self.request_kind == RequestKind.geocoding:
return GeocodingRequest(self.requested_payload, self.resolution, self.region_queries, self.scope,
self.level, self.namesake_limit, self.allow_ambiguous)
elif self.request_kind == RequestKind.reverse:
assert self.reverse_coordinates is not None
assert self.level is not None
return ReverseGeocodingRequest(self.reverse_coordinates, self.level, self.reverse_scope,
self.requested_payload, self.resolution)
else:
raise ValueError('Unknown mode: ' + str(self.request_kind))
class MapRegionBuilder:
def __init__(self):
self.parent_kind: Optional[bool] = None
self.parent_values: List[str] = []
def set_parent_kind(self, kind: bool) -> 'MapRegionBuilder':
self.parent_kind = kind
return self
def set_parent_values(self, values: List[str]) -> 'MapRegionBuilder':
self.parent_values = values
return self
def build(self) -> Optional[MapRegion]:
if self.parent_kind is not None:
return MapRegion(MapRegionKind(self.parent_kind), self.parent_values)
else:
return None
class RegionQueryBuilder:
def __init__(self):
self.request: Optional[str] = None
self.scope: Optional[MapRegion] = None
self.ignoring_strategy: Optional[IgnoringStrategyKind] = None
self.closest_coord: Optional[GeoPoint] = None
self.box: Optional[GeoRect] = None
def set_request(self, request: Optional[str]) -> 'RegionQueryBuilder':
assert_optional_type(request, str)
self.request = request
return self
def set_scope(self, parent: Optional[MapRegion]) -> 'RegionQueryBuilder':
assert_optional_type(parent, MapRegion)
self.scope = parent
return self
def set_ignoring_strategy(self, ignoring_strategy: IgnoringStrategyKind):
assert_type(ignoring_strategy, IgnoringStrategyKind)
self.ignoring_strategy = ignoring_strategy
return self
def set_closest_coord(self, closest_coord: Optional[GeoPoint]):
assert_optional_type(closest_coord, GeoPoint)
self.closest_coord = closest_coord
return self
def set_box(self, box: Optional[GeoRect]):
assert_optional_type(box, GeoRect)
self.box = box
return self
def build(self) -> RegionQuery:
return RegionQuery(self.request, self.scope, self._build_ambiguity_resolver())
def _build_ambiguity_resolver(self) -> AmbiguityResolver:
if self.ignoring_strategy is not None \
or self.closest_coord is not None \
or self.box is not None:
return AmbiguityResolver(self.ignoring_strategy, self.closest_coord, self.box)
else:
return AmbiguityResolver.empty()