python-package/lets_plot/geo_data/gis/json_request.py (195 lines of code) (raw):

import enum from typing import Dict, List, Optional, Tuple from .fluent_dict import FluentDict, FluentList from .geometry import GeoPoint from .request import RegionQuery, MapRegion, MapRegionKind, PayloadKind, RegionQueryBuilder, IgnoringStrategyKind, \ MapRegionBuilder from .request import Request, GeocodingRequest, ExplicitRequest, RequestBuilder, RequestKind, ReverseGeocodingRequest from .response import LevelKind, GeoRect PROTOCOL_VERSION = 3 class Field(enum.Enum): version = 'version' mode = 'mode' requested_payload = 'feature_options' resolution = 'resolution' view_box = 'view_box' fetched_ids = 'fetched_ids' option_kind = 'kind' geo_object_list = 'ids' region_queries = 'region_queries' region_query_names = 'region_query_names' region_query_countries = 'region_query_countries' region_query_states = 'region_query_states' region_query_counties = 'region_query_counties' region_query_parent = 'region_query_parent' scope = 'scope' level = 'level' map_region_kind = 'kind' map_region_values = 'values' match = 'match' namesake_example_limit = 'namesake_example_limit' allow_ambiguous = 'allow_ambiguous' ambiguity_resolver = 'ambiguity_resolver' ambiguity_ignoring_strategy = 'ambiguity_resolver_ignoring_strategy' ambiguity_closest_coord = 'ambiguity_resolver_closest_coord' ambiguity_box = 'ambiguity_resolver_box' reverse_level = "level" reverse_coordinates = "reverse_coordinates" reverse_parent = "reverse_parent" coord_lon = 'lon' coord_lat = 'lat' min_lon = 'min_lon' min_lat = 'min_lat' max_lon = 'max_lon' max_lat = 'max_lat' class RequestFormatter: @staticmethod def format(request: Request) -> FluentDict: if isinstance(request, GeocodingRequest): return RequestFormatter._format_geocoding_request(request) elif isinstance(request, ExplicitRequest): return RequestFormatter._format_explicit_request(request) elif isinstance(request, ReverseGeocodingRequest): return RequestFormatter._format_reverse_geocoding_request(request) else: raise ValueError('Unknown request kind: ' + str(request)) @staticmethod def _format_geocoding_request(request: 'GeocodingRequest') -> FluentDict: return RequestFormatter \ ._common(RequestKind.geocoding, request) \ .put(Field.region_queries, RequestFormatter._format_region_queries(request.region_queries)) \ .put(Field.scope, RequestFormatter._format_scope(request.scope)) \ .put(Field.level, request.level) \ .put(Field.namesake_example_limit, request.namesake_example_limit) \ .put(Field.allow_ambiguous, request.allow_ambiguous) @staticmethod def _format_explicit_request(request: 'ExplicitRequest') -> FluentDict: return RequestFormatter \ ._common(RequestKind.explicit, request) \ .put(Field.geo_object_list, request.geo_object_list) @staticmethod def _format_reverse_geocoding_request(request: 'ReverseGeocodingRequest'): return RequestFormatter \ ._common(RequestKind.reverse, request) \ .put(Field.reverse_coordinates, [RequestFormatter._format_coord(coord) for coord in request.coordinates]) \ .put(Field.reverse_level, request.level) \ .put(Field.reverse_parent, RequestFormatter._format_map_region(request.scope)) @staticmethod def _common(request_kind: RequestKind, request: Request) -> FluentDict: return FluentDict() \ .put(Field.version, PROTOCOL_VERSION) \ .put(Field.mode, request_kind.value) \ .put(Field.requested_payload, request.requested_payload) \ .put(Field.resolution, request.resolution) \ .put(Field.view_box, None) \ .put(Field.fetched_ids, None) @staticmethod def _format_region_queries(region_queires: List[RegionQuery]) -> List[Dict]: result = [] for query in region_queires: result.append( FluentDict() .put(Field.region_query_names, [] if query.request is None else [query.request]) .put(Field.region_query_countries, RequestFormatter._format_map_region(query.country)) .put(Field.region_query_states, RequestFormatter._format_map_region(query.state)) .put(Field.region_query_counties, RequestFormatter._format_map_region(query.county)) .put(Field.ambiguity_resolver, None if query.ambiguity_resolver is None else FluentDict() .put(Field.ambiguity_ignoring_strategy, query.ambiguity_resolver.ignoring_strategy) .put(Field.ambiguity_box, RequestFormatter._format_box(query.ambiguity_resolver.box)) .put(Field.ambiguity_closest_coord, RequestFormatter._format_coord(query.ambiguity_resolver.closest_coord))) \ .put(Field.region_query_parent, RequestFormatter._format_map_region(query.scope)) .to_dict() ) return result @staticmethod def _format_scope(scope: List[MapRegion]) -> List[Dict]: return [RequestFormatter._format_map_region(s) for s in scope] @staticmethod def _format_map_region(parent: Optional[MapRegion]) -> Optional[Dict]: if parent is None: return None # special case - place is just a geocoded object with id and extra information, used by client # server doesn't need this extra information if parent.kind.value == 'place': return FluentDict() \ .put(Field.map_region_kind, MapRegionKind.id.value) \ .put(Field.map_region_values, parent.values) \ .to_dict() return FluentDict() \ .put(Field.map_region_kind, parent.kind.value) \ .put(Field.map_region_values, parent.values) \ .to_dict() @staticmethod def _format_coord(closest_coord: GeoPoint) -> Optional[Tuple[float, float]]: if closest_coord is None: return None return closest_coord.lon, closest_coord.lat @staticmethod def _format_box(rect: GeoRect) -> Optional[Dict]: if rect is None: return None return FluentDict() \ .put(Field.min_lon, rect.start_lon) \ .put(Field.min_lat, rect.min_lat) \ .put(Field.max_lon, rect.end_lon) \ .put(Field.max_lat, rect.max_lat) \ .to_dict() class RequestParser: @staticmethod def parse(request_json: Dict) -> Request: request = RequestBuilder() request_dict = FluentDict(request_json) \ .visit_enum(Field.mode, RequestKind, request.set_request_kind) \ .visit_enums(Field.requested_payload, PayloadKind, request.set_requested_payload) \ .visit_int_optional(Field.resolution, request.set_resolution) if request.request_kind == RequestKind.explicit: request_dict.visit_str_list(Field.geo_object_list, request.set_ids) elif request.request_kind == RequestKind.geocoding: request_dict \ .visit_enum_existing(Field.level, LevelKind, request.set_level) \ .visit_int(Field.namesake_example_limit, request.set_namesake_limit) \ .visit_bool(Field.allow_ambiguous, request.set_allow_ambiguous) \ .visit_list(Field.region_queries, lambda regions: request.set_queries(regions.map(RequestParser._parse_region_query).list())) elif request.request_kind == RequestKind.reverse: request_dict \ .visit_enum_existing(Field.reverse_level, LevelKind, request.set_level) \ .visit_list(Field.reverse_coordinates, lambda coords: request.set_reverse_coordinates(RequestParser._parse_coordinates(coords))) \ .visit_object_optional(Field.reverse_parent, lambda parent: request.set_reverse_scope(RequestParser._parse_map_region(parent))) else: raise ValueError('Unknown request kind: ' + str(request)) return request.build() @staticmethod def _parse_region_query(region_query: dict) -> RegionQuery: region_q = FluentDict(region_query) assert len(region_q.get_list(Field.region_query_names)) in [0, 1], 'Multirequests are not supported' builder = RegionQueryBuilder() FluentDict(region_query) \ .visit_str_list(Field.region_query_names, lambda names: builder.set_request(names[0] if len(names) == 1 else None)) \ .visit_object_optional(Field.ambiguity_resolver, lambda resolver: resolver .visit_list_optional(Field.ambiguity_closest_coord, lambda coord: builder.set_closest_coord(RequestParser._parse_coord(coord))) .visit_object_optional(Field.ambiguity_box, lambda jsonBox: builder.set_box(RequestParser._parse_geo_rect(jsonBox))) .visit_enum_existing(Field.ambiguity_ignoring_strategy, IgnoringStrategyKind, builder.set_ignoring_strategy)) \ .visit_object_optional(Field.region_query_parent, lambda parent: builder.set_scope(RequestParser._parse_map_region(parent))) return builder.build() @staticmethod def _parse_map_region(json: FluentDict) -> MapRegion: builder = MapRegionBuilder() json \ .visit_str_list(Field.map_region_values, builder.set_parent_values) \ .visit_bool(Field.map_region_kind, builder.set_parent_kind) return builder.build() @staticmethod def _parse_coord(jsonCoord: FluentList) -> GeoPoint: return GeoPoint(jsonCoord.list()[0], jsonCoord.list()[1]) @staticmethod def _parse_geo_rect(jsonBox: FluentDict) -> GeoRect: return GeoRect( start_lon=jsonBox.get_float(Field.min_lon), min_lat=jsonBox.get_float(Field.min_lat), end_lon=jsonBox.get_float(Field.max_lon), max_lat=jsonBox.get_float(Field.max_lat), ) @staticmethod def _parse_coordinates(coordinates: FluentList) -> List[GeoPoint]: return [GeoPoint(coord[0], coord[1]) for coord in coordinates.list()]