X-Git-Url: https://git.openstreetmap.org./nominatim.git/blobdiff_plain/1facfd019b3e0c86b3cfb1a25ae0837cce4bdcae..300921a93e29d6bdc2a7da8ceaebfcce0c555c84:/nominatim/api/v1/server_glue.py diff --git a/nominatim/api/v1/server_glue.py b/nominatim/api/v1/server_glue.py index 550b1e3a..3c0c58b0 100644 --- a/nominatim/api/v1/server_glue.py +++ b/nominatim/api/v1/server_glue.py @@ -8,7 +8,7 @@ Generic part of the server implementation of the v1 API. Combine with the scaffolding provided for the various Python ASGI frameworks. """ -from typing import Optional, Any, Type, Callable, NoReturn +from typing import Optional, Any, Type, Callable, NoReturn, TypeVar import abc from nominatim.config import Configuration @@ -22,6 +22,7 @@ CONTENT_TYPE = { 'debug': 'text/html; charset=utf-8' } +ConvT = TypeVar('ConvT', int, float) class ASGIAdaptor(abc.ABC): """ Adapter class for the different ASGI frameworks. @@ -106,9 +107,10 @@ class ASGIAdaptor(abc.ABC): raise self.error(msg, status) - def get_int(self, name: str, default: Optional[int] = None) -> int: - """ Return an input parameter as an int. Raises an exception if - the parameter is given but not in an integer format. + def _get_typed(self, name: str, dest_type: Type[ConvT], type_name: str, + default: Optional[ConvT] = None) -> ConvT: + """ Return an input parameter as the type 'dest_type'. Raises an + exception if the parameter is given but not in the given format. If 'default' is given, then it will be returned when the parameter is missing completely. When 'default' is None, an error will be @@ -123,12 +125,35 @@ class ASGIAdaptor(abc.ABC): self.raise_error(f"Parameter '{name}' missing.") try: - intval = int(value) + intval = dest_type(value) except ValueError: - self.raise_error(f"Parameter '{name}' must be a number.") + self.raise_error(f"Parameter '{name}' must be a {type_name}.") return intval + + def get_int(self, name: str, default: Optional[int] = None) -> int: + """ Return an input parameter as an int. Raises an exception if + the parameter is given but not in an integer format. + + If 'default' is given, then it will be returned when the parameter + is missing completely. When 'default' is None, an error will be + raised on a missing parameter. + """ + return self._get_typed(name, int, 'number', default) + + + def get_float(self, name: str, default: Optional[float] = None) -> int: + """ Return an input parameter as a flaoting-point number. Raises an + exception if the parameter is given but not in an float format. + + If 'default' is given, then it will be returned when the parameter + is missing completely. When 'default' is None, an error will be + raised on a missing parameter. + """ + return self._get_typed(name, float, 'number', default) + + def get_bool(self, name: str, default: Optional[bool] = None) -> bool: """ Return an input parameter as bool. Only '0' is accepted as an input for 'false' all other inputs will be interpreted as 'true'. @@ -169,6 +194,17 @@ class ASGIAdaptor(abc.ABC): return False + def get_layers(self) -> napi.DataLayer: + """ Return a parsed version of the layer parameter. + """ + param = self.get('layer', None) + if param is None: + return None + + return reduce(napi.DataLayer.__or__, + (getattr(napi.DataLayer, s.upper()) for s in param.split(','))) + + def parse_format(self, result_type: Type[Any], default: str) -> str: """ Get and check the 'format' parameter and prepare the formatter. `result_type` is the type of result to be returned by the function @@ -243,9 +279,83 @@ async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> return params.build_response(output) +async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any: + """ Server glue for /reverse endpoint. See API docs for details. + """ + fmt = params.parse_format(napi.ReverseResults, 'xml') + debug = params.setup_debugging() + coord = napi.Point(params.get_float('lon'), params.get_float('lat')) + locales = napi.Locales.from_accept_languages(params.get_accepted_languages()) + + zoom = max(0, min(18, params.get_int('zoom', 18))) + + # Negation makes sure that NaN is handled. Don't change. + if not abs(coord[0]) <= 180 or not abs(coord[1]) <= 90: + params.raise_error('Invalid coordinates.') + + details = napi.LookupDetails(address_details=True, + geometry_simplification=params.get_float('polygon_threshold', 0.0)) + numgeoms = 0 + if params.get_bool('polygon_geojson', False): + details.geometry_output |= napi.GeometryFormat.GEOJSON + numgeoms += 1 + if fmt not in ('geojson', 'geocodejson'): + if params.get_bool('polygon_text', False): + details.geometry_output |= napi.GeometryFormat.TEXT + numgeoms += 1 + if params.get_bool('polygon_kml', False): + details.geometry_output |= napi.GeometryFormat.KML + numgeoms += 1 + if params.get_bool('polygon_svg', False): + details.geometry_output |= napi.GeometryFormat.SVG + numgeoms += 1 + + if numgeoms > params.config().get_int('POLYGON_OUTPUT_MAX_TYPES'): + params.raise_error(f'Too many polgyon output options selected.') + + result = await api.reverse(coord, REVERSE_MAX_RANKS[zoom], + params.get_layers() or + napi.DataLayer.ADDRESS | napi.DataLayer.POI, + details) + + if debug: + return params.build_response(loglib.get_and_disable()) + + fmt_options = {'locales': locales, + 'extratags': params.get_bool('extratags', False), + 'namedetails': params.get_bool('namedetails', False), + 'addressdetails': params.get_bool('addressdetails', True), + 'single_result': True} + if fmt == 'xml': + fmt_options['xml_roottag'] = 'reversegeocode' + fmt_options['xml_extra_info'] = {'querystring': 'TODO'} + + output = formatting.format_result(napi.ReverseResults([result] if result else []), + fmt, fmt_options) + + return params.build_response(output) + + EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any] +REVERSE_MAX_RANKS = [2, 2, 2, # 0-2 Continent/Sea + 4, 4, # 3-4 Country + 8, # 5 State + 10, 10, # 6-7 Region + 12, 12, # 8-9 County + 16, 17, # 10-11 City + 18, # 12 Town + 19, # 13 Village/Suburb + 22, # 14 Hamlet/Neighbourhood + 25, # 15 Localities + 26, # 16 Major Streets + 27, # 17 Minor Streets + 30 # 18 Building + ] + + ROUTES = [ ('status', status_endpoint), - ('details', details_endpoint) + ('details', details_endpoint), + ('reverse', reverse_endpoint) ]