]> git.openstreetmap.org Git - nominatim.git/blobdiff - nominatim/api/v1/server_glue.py
rename lookup() API to details and add lookup call
[nominatim.git] / nominatim / api / v1 / server_glue.py
index 550b1e3a68c717501d61e054d055d49efa0e0476..b1bd672bda6705bea59d2927490f40f30a52687a 100644 (file)
@@ -8,8 +8,10 @@
 Generic part of the server implementation of the v1 API.
 Combine with the scaffolding provided for the various Python ASGI frameworks.
 """
 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, cast
+from functools import reduce
 import abc
 import abc
+import math
 
 from nominatim.config import Configuration
 import nominatim.api as napi
 
 from nominatim.config import Configuration
 import nominatim.api as napi
@@ -22,7 +24,6 @@ CONTENT_TYPE = {
   'debug': 'text/html; charset=utf-8'
 }
 
   'debug': 'text/html; charset=utf-8'
 }
 
-
 class ASGIAdaptor(abc.ABC):
     """ Adapter class for the different ASGI frameworks.
         Wraps functionality over concrete requests and responses.
 class ASGIAdaptor(abc.ABC):
     """ Adapter class for the different ASGI frameworks.
         Wraps functionality over concrete requests and responses.
@@ -129,6 +130,34 @@ class ASGIAdaptor(abc.ABC):
 
         return intval
 
 
         return intval
 
+
+    def get_float(self, name: str, default: Optional[float] = None) -> float:
+        """ 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.
+        """
+        value = self.get(name)
+
+        if value is None:
+            if default is not None:
+                return default
+
+            self.raise_error(f"Parameter '{name}' missing.")
+
+        try:
+            fval = float(value)
+        except ValueError:
+            self.raise_error(f"Parameter '{name}' must be a number.")
+
+        if math.isnan(fval) or math.isinf(fval):
+            self.raise_error(f"Parameter '{name}' must be a number.")
+
+        return fval
+
+
     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'.
     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 +198,18 @@ class ASGIAdaptor(abc.ABC):
         return False
 
 
         return False
 
 
+    def get_layers(self) -> Optional[napi.DataLayer]:
+        """ Return a parsed version of the layer parameter.
+        """
+        param = self.get('layer', None)
+        if param is None:
+            return None
+
+        return cast(napi.DataLayer,
+                    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
     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
@@ -227,7 +268,7 @@ async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
 
     locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
 
 
     locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
 
-    result = await api.lookup(place, details)
+    result = await api.details(place, details)
 
     if debug:
         return params.build_response(loglib.get_and_disable())
 
     if debug:
         return params.build_response(loglib.get_and_disable())
@@ -243,9 +284,78 @@ async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
     return params.build_response(output)
 
 
     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)))
+
+    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('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)}
+    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]
 
 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),
 ROUTES = [
     ('status', status_endpoint),
-    ('details', details_endpoint)
+    ('details', details_endpoint),
+    ('reverse', reverse_endpoint)
 ]
 ]