mirror of
https://github.com/osm-search/Nominatim.git
synced 2024-12-25 14:02:12 +03:00
add output formatters for ReverseResults
These formatters are written in a way that they can be reused for search results later.
This commit is contained in:
parent
878302a622
commit
35b52c4656
@ -15,4 +15,4 @@ ignored-classes=NominatimArgs,closing
|
|||||||
# typed Python is enabled. See also https://github.com/PyCQA/pylint/issues/5273
|
# typed Python is enabled. See also https://github.com/PyCQA/pylint/issues/5273
|
||||||
disable=too-few-public-methods,duplicate-code,too-many-ancestors,bad-option-value,no-self-use,not-context-manager,use-dict-literal,chained-comparison
|
disable=too-few-public-methods,duplicate-code,too-many-ancestors,bad-option-value,no-self-use,not-context-manager,use-dict-literal,chained-comparison
|
||||||
|
|
||||||
good-names=i,x,y,m,t,fd,db,cc,x1,x2,y1,y2,pt
|
good-names=i,x,y,m,t,fd,db,cc,x1,x2,y1,y2,pt,k,v
|
||||||
|
@ -31,5 +31,6 @@ from .results import (SourceTable as SourceTable,
|
|||||||
WordInfo as WordInfo,
|
WordInfo as WordInfo,
|
||||||
WordInfos as WordInfos,
|
WordInfos as WordInfos,
|
||||||
DetailedResult as DetailedResult,
|
DetailedResult as DetailedResult,
|
||||||
ReverseResult as ReverseResult)
|
ReverseResult as ReverseResult,
|
||||||
|
ReverseResults as ReverseResults)
|
||||||
from .localization import (Locales as Locales)
|
from .localization import (Locales as Locales)
|
||||||
|
@ -11,7 +11,7 @@ Data classes are part of the public API while the functions are for
|
|||||||
internal use only. That's why they are implemented as free-standing functions
|
internal use only. That's why they are implemented as free-standing functions
|
||||||
instead of member functions.
|
instead of member functions.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Tuple, Dict, Sequence, TypeVar, Type
|
from typing import Optional, Tuple, Dict, Sequence, TypeVar, Type, List
|
||||||
import enum
|
import enum
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
@ -22,6 +22,7 @@ from nominatim.typing import SaSelect, SaRow
|
|||||||
from nominatim.api.types import Point, Bbox, LookupDetails
|
from nominatim.api.types import Point, Bbox, LookupDetails
|
||||||
from nominatim.api.connection import SearchConnection
|
from nominatim.api.connection import SearchConnection
|
||||||
from nominatim.api.logging import log
|
from nominatim.api.logging import log
|
||||||
|
from nominatim.api.localization import Locales
|
||||||
|
|
||||||
# This file defines complex result data classes.
|
# This file defines complex result data classes.
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
@ -52,8 +53,30 @@ class AddressLine:
|
|||||||
rank_address: int
|
rank_address: int
|
||||||
distance: float
|
distance: float
|
||||||
|
|
||||||
|
local_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AddressLines(List[AddressLine]):
|
||||||
|
""" Sequence of address lines order in descending order by their rank.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def localize(self, locales: Locales) -> List[str]:
|
||||||
|
""" Set the local name of address parts according to the chosen
|
||||||
|
locale. Return the list of local names without duplications.
|
||||||
|
|
||||||
|
Only address parts that are marked as isaddress are localized
|
||||||
|
and returned.
|
||||||
|
"""
|
||||||
|
label_parts: List[str] = []
|
||||||
|
|
||||||
|
for line in self:
|
||||||
|
if line.isaddress and line.names:
|
||||||
|
line.local_name = locales.display_name(line.names)
|
||||||
|
if not label_parts or label_parts[-1] != line.local_name:
|
||||||
|
label_parts.append(line.local_name)
|
||||||
|
|
||||||
|
return label_parts
|
||||||
|
|
||||||
AddressLines = Sequence[AddressLine]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
@ -144,6 +167,12 @@ class ReverseResult(BaseResult):
|
|||||||
bbox: Optional[Bbox] = None
|
bbox: Optional[Bbox] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReverseResults(List[ReverseResult]):
|
||||||
|
""" Sequence of reverse lookup results ordered by distance.
|
||||||
|
May be empty when no result was found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _filter_geometries(row: SaRow) -> Dict[str, str]:
|
def _filter_geometries(row: SaRow) -> Dict[str, str]:
|
||||||
return {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212
|
return {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212
|
||||||
if k.startswith('geometry_')}
|
if k.startswith('geometry_')}
|
||||||
@ -333,7 +362,7 @@ async def complete_address_details(conn: SearchConnection, result: BaseResult) -
|
|||||||
sql = sa.select(sfn).order_by(sa.column('rank_address').desc(),
|
sql = sa.select(sfn).order_by(sa.column('rank_address').desc(),
|
||||||
sa.column('isaddress').desc())
|
sa.column('isaddress').desc())
|
||||||
|
|
||||||
result.address_rows = []
|
result.address_rows = AddressLines()
|
||||||
for row in await conn.execute(sql):
|
for row in await conn.execute(sql):
|
||||||
result.address_rows.append(_result_row_to_address_row(row))
|
result.address_rows.append(_result_row_to_address_row(row))
|
||||||
|
|
||||||
@ -357,7 +386,7 @@ def _placex_select_address_row(conn: SearchConnection,
|
|||||||
async def complete_linked_places(conn: SearchConnection, result: BaseResult) -> None:
|
async def complete_linked_places(conn: SearchConnection, result: BaseResult) -> None:
|
||||||
""" Retrieve information about places that link to the result.
|
""" Retrieve information about places that link to the result.
|
||||||
"""
|
"""
|
||||||
result.linked_rows = []
|
result.linked_rows = AddressLines()
|
||||||
if result.source_table != SourceTable.PLACEX:
|
if result.source_table != SourceTable.PLACEX:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -392,7 +421,7 @@ async def complete_parented_places(conn: SearchConnection, result: BaseResult) -
|
|||||||
""" Retrieve information about places that the result provides the
|
""" Retrieve information about places that the result provides the
|
||||||
address for.
|
address for.
|
||||||
"""
|
"""
|
||||||
result.parented_rows = []
|
result.parented_rows = AddressLines()
|
||||||
if result.source_table != SourceTable.PLACEX:
|
if result.source_table != SourceTable.PLACEX:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -10,6 +10,52 @@ Hard-coded information about tag catagories.
|
|||||||
These tables have been copied verbatim from the old PHP code. For future
|
These tables have been copied verbatim from the old PHP code. For future
|
||||||
version a more flexible formatting is required.
|
version a more flexible formatting is required.
|
||||||
"""
|
"""
|
||||||
|
from typing import Tuple, Optional, Mapping
|
||||||
|
|
||||||
|
def get_label_tag(category: Tuple[str, str], extratags: Optional[Mapping[str, str]],
|
||||||
|
rank: int, country: Optional[str]) -> str:
|
||||||
|
""" Create a label tag for the given place that can be used as an XML name.
|
||||||
|
"""
|
||||||
|
if rank < 26 and extratags and 'place'in extratags:
|
||||||
|
label = extratags['place']
|
||||||
|
elif category == ('boundary', 'administrative'):
|
||||||
|
label = ADMIN_LABELS.get((country or '', int(rank/2)))\
|
||||||
|
or ADMIN_LABELS.get(('', int(rank/2)))\
|
||||||
|
or 'Administrative'
|
||||||
|
elif category[1] == 'postal_code':
|
||||||
|
label = 'postcode'
|
||||||
|
elif rank < 26:
|
||||||
|
label = category[1] if category[1] != 'yes' else category[0]
|
||||||
|
elif rank < 28:
|
||||||
|
label = 'road'
|
||||||
|
elif category[0] == 'place'\
|
||||||
|
and category[1] in ('house_number', 'house_name', 'country_code'):
|
||||||
|
label = category[1]
|
||||||
|
else:
|
||||||
|
label = category[0]
|
||||||
|
|
||||||
|
return label.lower().replace(' ', '_')
|
||||||
|
|
||||||
|
|
||||||
|
ADMIN_LABELS = {
|
||||||
|
('', 1): 'Continent',
|
||||||
|
('', 2): 'Country',
|
||||||
|
('', 3): 'Region',
|
||||||
|
('', 4): 'State',
|
||||||
|
('', 5): 'State District',
|
||||||
|
('', 6): 'County',
|
||||||
|
('', 7): 'Municipality',
|
||||||
|
('', 8): 'City',
|
||||||
|
('', 9): 'City District',
|
||||||
|
('', 10): 'Suburb',
|
||||||
|
('', 11): 'Neighbourhood',
|
||||||
|
('', 12): 'City Block',
|
||||||
|
('no', 3): 'State',
|
||||||
|
('no', 4): 'County',
|
||||||
|
('se', 3): 'State',
|
||||||
|
('se', 4): 'County'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ICONS = {
|
ICONS = {
|
||||||
('boundary', 'administrative'): 'poi_boundary_administrative',
|
('boundary', 'administrative'): 'poi_boundary_administrative',
|
||||||
|
43
nominatim/api/v1/constants.py
Normal file
43
nominatim/api/v1/constants.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 by the Nominatim developer community.
|
||||||
|
# For a full list of authors see the git log.
|
||||||
|
"""
|
||||||
|
Constants shared by all formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import nominatim.api as napi
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
OSM_ATTRIBUTION = 'Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright'
|
||||||
|
|
||||||
|
OSM_TYPE_NAME = {
|
||||||
|
'N': 'node',
|
||||||
|
'W': 'way',
|
||||||
|
'R': 'relation'
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_EXTENT = [25, 25, 25, 25,
|
||||||
|
7,
|
||||||
|
2.6, 2.6, 2.0, 1.0, 1.0,
|
||||||
|
0.7, 0.7, 0.7,
|
||||||
|
0.16, 0.16, 0.16, 0.16,
|
||||||
|
0.04, 0.04,
|
||||||
|
0.02, 0.02,
|
||||||
|
0.01, 0.01, 0.01, 0.01, 0.01,
|
||||||
|
0.015, 0.015, 0.015, 0.015,
|
||||||
|
0.005]
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_from_result(result: napi.ReverseResult) -> napi.Bbox:
|
||||||
|
""" Compute a bounding box for the result. For ways and relations
|
||||||
|
a given boundingbox is used. For all other object, a box is computed
|
||||||
|
around the centroid according to dimensions dereived from the
|
||||||
|
search rank.
|
||||||
|
"""
|
||||||
|
if (result.osm_object and result.osm_object[0] == 'N') or result.bbox is None:
|
||||||
|
return napi.Bbox.from_point(result.centroid, NODE_EXTENT[result.rank_search])
|
||||||
|
|
||||||
|
return result.bbox
|
@ -13,6 +13,7 @@ import collections
|
|||||||
import nominatim.api as napi
|
import nominatim.api as napi
|
||||||
from nominatim.api.result_formatting import FormatDispatcher
|
from nominatim.api.result_formatting import FormatDispatcher
|
||||||
from nominatim.api.v1.classtypes import ICONS
|
from nominatim.api.v1.classtypes import ICONS
|
||||||
|
from nominatim.api.v1 import format_json, format_xml
|
||||||
from nominatim.utils.json_writer import JsonWriter
|
from nominatim.utils.json_writer import JsonWriter
|
||||||
|
|
||||||
dispatch = FormatDispatcher()
|
dispatch = FormatDispatcher()
|
||||||
@ -93,7 +94,7 @@ def _add_parent_rows_grouped(writer: JsonWriter, rows: napi.AddressLines,
|
|||||||
|
|
||||||
|
|
||||||
@dispatch.format_func(napi.DetailedResult, 'json')
|
@dispatch.format_func(napi.DetailedResult, 'json')
|
||||||
def _format_search_json(result: napi.DetailedResult, options: Mapping[str, Any]) -> str:
|
def _format_details_json(result: napi.DetailedResult, options: Mapping[str, Any]) -> str:
|
||||||
locales = options.get('locales', napi.Locales())
|
locales = options.get('locales', napi.Locales())
|
||||||
geom = result.geometry.get('geojson')
|
geom = result.geometry.get('geojson')
|
||||||
centroid = result.centroid.to_geojson()
|
centroid = result.centroid.to_geojson()
|
||||||
@ -161,3 +162,36 @@ def _format_search_json(result: napi.DetailedResult, options: Mapping[str, Any])
|
|||||||
out.end_object()
|
out.end_object()
|
||||||
|
|
||||||
return out()
|
return out()
|
||||||
|
|
||||||
|
|
||||||
|
@dispatch.format_func(napi.ReverseResults, 'xml')
|
||||||
|
def _format_reverse_xml(results: napi.ReverseResults, options: Mapping[str, Any]) -> str:
|
||||||
|
return format_xml.format_base_xml(results,
|
||||||
|
options, True, 'reversegeocode',
|
||||||
|
{'querystring': 'TODO'})
|
||||||
|
|
||||||
|
|
||||||
|
@dispatch.format_func(napi.ReverseResults, 'geojson')
|
||||||
|
def _format_reverse_geojson(results: napi.ReverseResults,
|
||||||
|
options: Mapping[str, Any]) -> str:
|
||||||
|
return format_json.format_base_geojson(results, options, True)
|
||||||
|
|
||||||
|
|
||||||
|
@dispatch.format_func(napi.ReverseResults, 'geocodejson')
|
||||||
|
def _format_reverse_geocodejson(results: napi.ReverseResults,
|
||||||
|
options: Mapping[str, Any]) -> str:
|
||||||
|
return format_json.format_base_geocodejson(results, options, True)
|
||||||
|
|
||||||
|
|
||||||
|
@dispatch.format_func(napi.ReverseResults, 'json')
|
||||||
|
def _format_reverse_json(results: napi.ReverseResults,
|
||||||
|
options: Mapping[str, Any]) -> str:
|
||||||
|
return format_json.format_base_json(results, options, True,
|
||||||
|
class_label='class')
|
||||||
|
|
||||||
|
|
||||||
|
@dispatch.format_func(napi.ReverseResults, 'jsonv2')
|
||||||
|
def _format_reverse_jsonv2(results: napi.ReverseResults,
|
||||||
|
options: Mapping[str, Any]) -> str:
|
||||||
|
return format_json.format_base_json(results, options, True,
|
||||||
|
class_label='category')
|
||||||
|
283
nominatim/api/v1/format_json.py
Normal file
283
nominatim/api/v1/format_json.py
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 by the Nominatim developer community.
|
||||||
|
# For a full list of authors see the git log.
|
||||||
|
"""
|
||||||
|
Helper functions for output of results in json formats.
|
||||||
|
"""
|
||||||
|
from typing import Mapping, Any, Optional, Tuple
|
||||||
|
|
||||||
|
import nominatim.api as napi
|
||||||
|
from nominatim.api.v1.constants import OSM_ATTRIBUTION, OSM_TYPE_NAME, bbox_from_result
|
||||||
|
from nominatim.api.v1.classtypes import ICONS, get_label_tag
|
||||||
|
from nominatim.utils.json_writer import JsonWriter
|
||||||
|
|
||||||
|
def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
|
||||||
|
if osm_object is not None:
|
||||||
|
out.keyval_not_none('osm_type', OSM_TYPE_NAME.get(osm_object[0], None))\
|
||||||
|
.keyval('osm_id', osm_object[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _write_typed_address(out: JsonWriter, address: Optional[napi.AddressLines],
|
||||||
|
country_code: Optional[str]) -> None:
|
||||||
|
parts = {}
|
||||||
|
for line in (address or []):
|
||||||
|
if line.isaddress and line.local_name:
|
||||||
|
label = get_label_tag(line.category, line.extratags,
|
||||||
|
line.rank_address, country_code)
|
||||||
|
if label not in parts:
|
||||||
|
parts[label] = line.local_name
|
||||||
|
|
||||||
|
for k, v in parts.items():
|
||||||
|
out.keyval(k, v)
|
||||||
|
|
||||||
|
if country_code:
|
||||||
|
out.keyval('country_code', country_code)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_geocodejson_address(out: JsonWriter,
|
||||||
|
address: Optional[napi.AddressLines],
|
||||||
|
obj_place_id: Optional[int],
|
||||||
|
country_code: Optional[str]) -> None:
|
||||||
|
extra = {}
|
||||||
|
for line in (address or []):
|
||||||
|
if line.isaddress and line.local_name:
|
||||||
|
if line.category[1] in ('postcode', 'postal_code'):
|
||||||
|
out.keyval('postcode', line.local_name)
|
||||||
|
elif line.category[1] == 'house_number':
|
||||||
|
out.keyval('housenumber', line.local_name)
|
||||||
|
elif (obj_place_id is None or obj_place_id != line.place_id) \
|
||||||
|
and line.rank_address >= 4 and line.rank_address < 28:
|
||||||
|
extra[GEOCODEJSON_RANKS[line.rank_address]] = line.local_name
|
||||||
|
|
||||||
|
for k, v in extra.items():
|
||||||
|
out.keyval(k, v)
|
||||||
|
|
||||||
|
if country_code:
|
||||||
|
out.keyval('country_code', country_code)
|
||||||
|
|
||||||
|
|
||||||
|
def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-branches
|
||||||
|
options: Mapping[str, Any], simple: bool,
|
||||||
|
class_label: str) -> str:
|
||||||
|
""" Return the result list as a simple json string in custom Nominatim format.
|
||||||
|
"""
|
||||||
|
locales = options.get('locales', napi.Locales())
|
||||||
|
|
||||||
|
out = JsonWriter()
|
||||||
|
|
||||||
|
if simple:
|
||||||
|
if not results:
|
||||||
|
return '{"error":"Unable to geocode"}'
|
||||||
|
else:
|
||||||
|
out.start_array()
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
label_parts = result.address_rows.localize(locales) if result.address_rows else []
|
||||||
|
|
||||||
|
out.start_object()\
|
||||||
|
.keyval_not_none('place_id', result.place_id)\
|
||||||
|
.keyval('licence', OSM_ATTRIBUTION)\
|
||||||
|
|
||||||
|
_write_osm_id(out, result.osm_object)
|
||||||
|
|
||||||
|
out.keyval('lat', result.centroid.lat)\
|
||||||
|
.keyval('lon', result.centroid.lon)\
|
||||||
|
.keyval(class_label, result.category[0])\
|
||||||
|
.keyval('type', result.category[1])\
|
||||||
|
.keyval('place_rank', result.rank_search)\
|
||||||
|
.keyval('importance', result.calculated_importance())\
|
||||||
|
.keyval('addresstype', get_label_tag(result.category, result.extratags,
|
||||||
|
result.rank_address,
|
||||||
|
result.country_code))\
|
||||||
|
.keyval('name', locales.display_name(result.names))\
|
||||||
|
.keyval('display_name', ', '.join(label_parts))
|
||||||
|
|
||||||
|
|
||||||
|
if options.get('icon_base_url', None):
|
||||||
|
icon = ICONS.get(result.category)
|
||||||
|
if icon:
|
||||||
|
out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
|
||||||
|
|
||||||
|
if options.get('addressdetails', False):
|
||||||
|
out.key('address').start_object()
|
||||||
|
_write_typed_address(out, result.address_rows, result.country_code)
|
||||||
|
out.end_object().next()
|
||||||
|
|
||||||
|
if options.get('extratags', False):
|
||||||
|
out.keyval('extratags', result.extratags)
|
||||||
|
|
||||||
|
if options.get('namedetails', False):
|
||||||
|
out.keyval('namedetails', result.names)
|
||||||
|
|
||||||
|
bbox = bbox_from_result(result)
|
||||||
|
out.key('boundingbox').start_array()\
|
||||||
|
.value(bbox.minlat).next()\
|
||||||
|
.value(bbox.maxlat).next()\
|
||||||
|
.value(bbox.minlon).next()\
|
||||||
|
.value(bbox.maxlon).next()\
|
||||||
|
.end_array().next()
|
||||||
|
|
||||||
|
if result.geometry:
|
||||||
|
for key in ('text', 'kml'):
|
||||||
|
out.keyval_not_none('geo' + key, result.geometry.get(key))
|
||||||
|
if 'geojson' in result.geometry:
|
||||||
|
out.key('geojson').raw(result.geometry['geojson']).next()
|
||||||
|
out.keyval_not_none('svg', result.geometry.get('svg'))
|
||||||
|
|
||||||
|
out.end_object()
|
||||||
|
|
||||||
|
if simple:
|
||||||
|
return out()
|
||||||
|
|
||||||
|
out.next()
|
||||||
|
|
||||||
|
out.end_array()
|
||||||
|
|
||||||
|
return out()
|
||||||
|
|
||||||
|
|
||||||
|
def format_base_geojson(results: napi.ReverseResults,
|
||||||
|
options: Mapping[str, Any],
|
||||||
|
simple: bool) -> str:
|
||||||
|
""" Return the result list as a geojson string.
|
||||||
|
"""
|
||||||
|
if not results and simple:
|
||||||
|
return '{"error":"Unable to geocode"}'
|
||||||
|
|
||||||
|
locales = options.get('locales', napi.Locales())
|
||||||
|
|
||||||
|
out = JsonWriter()
|
||||||
|
|
||||||
|
out.start_object()\
|
||||||
|
.keyval('type', 'FeatureCollection')\
|
||||||
|
.keyval('licence', OSM_ATTRIBUTION)\
|
||||||
|
.key('features').start_array()
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
if result.address_rows:
|
||||||
|
label_parts = result.address_rows.localize(locales)
|
||||||
|
else:
|
||||||
|
label_parts = []
|
||||||
|
|
||||||
|
out.start_object()\
|
||||||
|
.keyval('type', 'Feature')\
|
||||||
|
.key('properties').start_object()
|
||||||
|
|
||||||
|
out.keyval_not_none('place_id', result.place_id)
|
||||||
|
|
||||||
|
_write_osm_id(out, result.osm_object)
|
||||||
|
|
||||||
|
out.keyval('place_rank', result.rank_search)\
|
||||||
|
.keyval('category', result.category[0])\
|
||||||
|
.keyval('type', result.category[1])\
|
||||||
|
.keyval('importance', result.calculated_importance())\
|
||||||
|
.keyval('addresstype', get_label_tag(result.category, result.extratags,
|
||||||
|
result.rank_address,
|
||||||
|
result.country_code))\
|
||||||
|
.keyval('name', locales.display_name(result.names))\
|
||||||
|
.keyval('display_name', ', '.join(label_parts))
|
||||||
|
|
||||||
|
if options.get('addressdetails', False):
|
||||||
|
out.key('address').start_object()
|
||||||
|
_write_typed_address(out, result.address_rows, result.country_code)
|
||||||
|
out.end_object().next()
|
||||||
|
|
||||||
|
if options.get('extratags', False):
|
||||||
|
out.keyval('extratags', result.extratags)
|
||||||
|
|
||||||
|
if options.get('namedetails', False):
|
||||||
|
out.keyval('namedetails', result.names)
|
||||||
|
|
||||||
|
out.end_object().next() # properties
|
||||||
|
|
||||||
|
bbox = bbox_from_result(result)
|
||||||
|
out.keyval('bbox', bbox.coords)
|
||||||
|
|
||||||
|
out.key('geometry').raw(result.geometry.get('geojson')
|
||||||
|
or result.centroid.to_geojson()).next()
|
||||||
|
|
||||||
|
out.end_object().next()
|
||||||
|
|
||||||
|
out.end_array().next().end_object()
|
||||||
|
|
||||||
|
return out()
|
||||||
|
|
||||||
|
|
||||||
|
def format_base_geocodejson(results: napi.ReverseResults,
|
||||||
|
options: Mapping[str, Any], simple: bool) -> str:
|
||||||
|
""" Return the result list as a geocodejson string.
|
||||||
|
"""
|
||||||
|
if not results and simple:
|
||||||
|
return '{"error":"Unable to geocode"}'
|
||||||
|
|
||||||
|
locales = options.get('locales', napi.Locales())
|
||||||
|
|
||||||
|
out = JsonWriter()
|
||||||
|
|
||||||
|
out.start_object()\
|
||||||
|
.keyval('type', 'FeatureCollection')\
|
||||||
|
.key('geocoding').start_object()\
|
||||||
|
.keyval('version', '0.1.0')\
|
||||||
|
.keyval('attribution', OSM_ATTRIBUTION)\
|
||||||
|
.keyval('licence', 'ODbL')\
|
||||||
|
.keyval_not_none('query', options.get('query'))\
|
||||||
|
.end_object().next()\
|
||||||
|
.key('features').start_array()
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
if result.address_rows:
|
||||||
|
label_parts = result.address_rows.localize(locales)
|
||||||
|
else:
|
||||||
|
label_parts = []
|
||||||
|
|
||||||
|
out.start_object()\
|
||||||
|
.keyval('type', 'Feature')\
|
||||||
|
.key('properties').start_object()\
|
||||||
|
.key('geocoding').start_object()
|
||||||
|
|
||||||
|
out.keyval_not_none('place_id', result.place_id)
|
||||||
|
|
||||||
|
_write_osm_id(out, result.osm_object)
|
||||||
|
|
||||||
|
out.keyval('osm_key', result.category[0])\
|
||||||
|
.keyval('osm_value', result.category[1])\
|
||||||
|
.keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
|
||||||
|
.keyval_not_none('accuracy', result.distance)\
|
||||||
|
.keyval('label', ', '.join(label_parts))\
|
||||||
|
.keyval_not_none('name', locales.display_name(result.names))\
|
||||||
|
|
||||||
|
if options.get('addressdetails', False):
|
||||||
|
_write_geocodejson_address(out, result.address_rows, result.place_id,
|
||||||
|
result.country_code)
|
||||||
|
|
||||||
|
out.key('admin').start_object()
|
||||||
|
if result.address_rows:
|
||||||
|
for line in result.address_rows:
|
||||||
|
if line.isaddress and (line.admin_level or 15) < 15 and line.local_name:
|
||||||
|
out.keyval(f"level{line.admin_level}", line.local_name)
|
||||||
|
out.end_object().next()
|
||||||
|
|
||||||
|
out.end_object().next().end_object().next()
|
||||||
|
|
||||||
|
out.key('geometry').raw(result.geometry.get('geojson')
|
||||||
|
or result.centroid.to_geojson()).next()
|
||||||
|
|
||||||
|
out.end_object().next()
|
||||||
|
|
||||||
|
out.end_array().next().end_object()
|
||||||
|
|
||||||
|
return out()
|
||||||
|
|
||||||
|
|
||||||
|
GEOCODEJSON_RANKS = {
|
||||||
|
3: 'locality',
|
||||||
|
4: 'country',
|
||||||
|
5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
|
||||||
|
10: 'county', 11: 'county', 12: 'county',
|
||||||
|
13: 'city', 14: 'city', 15: 'city', 16: 'city',
|
||||||
|
17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
|
||||||
|
22: 'locality', 23: 'locality', 24: 'locality',
|
||||||
|
25: 'street', 26: 'street', 27: 'street', 28: 'house'}
|
126
nominatim/api/v1/format_xml.py
Normal file
126
nominatim/api/v1/format_xml.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 by the Nominatim developer community.
|
||||||
|
# For a full list of authors see the git log.
|
||||||
|
"""
|
||||||
|
Helper functions for output of results in XML format.
|
||||||
|
"""
|
||||||
|
from typing import Mapping, Any, Optional
|
||||||
|
import datetime as dt
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import nominatim.api as napi
|
||||||
|
from nominatim.api.v1.constants import OSM_ATTRIBUTION, OSM_TYPE_NAME, bbox_from_result
|
||||||
|
from nominatim.api.v1.classtypes import ICONS, get_label_tag
|
||||||
|
|
||||||
|
def _write_xml_address(root: ET.Element, address: napi.AddressLines,
|
||||||
|
country_code: Optional[str]) -> None:
|
||||||
|
parts = {}
|
||||||
|
for line in address:
|
||||||
|
if line.isaddress and line.local_name:
|
||||||
|
label = get_label_tag(line.category, line.extratags,
|
||||||
|
line.rank_address, country_code)
|
||||||
|
if label not in parts:
|
||||||
|
parts[label] = line.local_name
|
||||||
|
|
||||||
|
for k,v in parts.items():
|
||||||
|
ET.SubElement(root, k).text = v
|
||||||
|
|
||||||
|
if country_code:
|
||||||
|
ET.SubElement(root, 'country_code').text = country_code
|
||||||
|
|
||||||
|
|
||||||
|
def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-branches
|
||||||
|
root: ET.Element, simple: bool,
|
||||||
|
locales: napi.Locales) -> ET.Element:
|
||||||
|
if result.address_rows:
|
||||||
|
label_parts = result.address_rows.localize(locales)
|
||||||
|
else:
|
||||||
|
label_parts = []
|
||||||
|
|
||||||
|
place = ET.SubElement(root, 'result' if simple else 'place')
|
||||||
|
if result.place_id is not None:
|
||||||
|
place.set('place_id', str(result.place_id))
|
||||||
|
if result.osm_object:
|
||||||
|
osm_type = OSM_TYPE_NAME.get(result.osm_object[0], None)
|
||||||
|
if osm_type is not None:
|
||||||
|
place.set('osm_type', osm_type)
|
||||||
|
place.set('osm_id', str(result.osm_object[1]))
|
||||||
|
if result.names and 'ref' in result.names:
|
||||||
|
place.set('place_id', result.names['ref'])
|
||||||
|
place.set('lat', str(result.centroid.lat))
|
||||||
|
place.set('lon', str(result.centroid.lon))
|
||||||
|
|
||||||
|
bbox = bbox_from_result(result)
|
||||||
|
place.set('boundingbox', ','.join(map(str, [bbox.minlat, bbox.maxlat,
|
||||||
|
bbox.minlon, bbox.maxlon])))
|
||||||
|
|
||||||
|
place.set('place_rank', str(result.rank_search))
|
||||||
|
place.set('address_rank', str(result.rank_address))
|
||||||
|
|
||||||
|
if result.geometry:
|
||||||
|
for key in ('text', 'svg'):
|
||||||
|
if key in result.geometry:
|
||||||
|
place.set('geo' + key, result.geometry[key])
|
||||||
|
if 'kml' in result.geometry:
|
||||||
|
ET.SubElement(root if simple else place, 'geokml')\
|
||||||
|
.append(ET.fromstring(result.geometry['kml']))
|
||||||
|
if 'geojson' in result.geometry:
|
||||||
|
place.set('geojson', result.geometry['geojson'])
|
||||||
|
|
||||||
|
if simple:
|
||||||
|
place.text = ', '.join(label_parts)
|
||||||
|
else:
|
||||||
|
place.set('display_name', ', '.join(label_parts))
|
||||||
|
place.set('class', result.category[0])
|
||||||
|
place.set('type', result.category[1])
|
||||||
|
place.set('importance', str(result.calculated_importance()))
|
||||||
|
|
||||||
|
return place
|
||||||
|
|
||||||
|
|
||||||
|
def format_base_xml(results: napi.ReverseResults,
|
||||||
|
options: Mapping[str, Any],
|
||||||
|
simple: bool, xml_root_tag: str,
|
||||||
|
xml_extra_info: Mapping[str, str]) -> str:
|
||||||
|
""" Format the result into an XML response. With 'simple' exactly one
|
||||||
|
result will be output, otherwise a list.
|
||||||
|
"""
|
||||||
|
locales = options.get('locales', napi.Locales())
|
||||||
|
|
||||||
|
root = ET.Element(xml_root_tag)
|
||||||
|
root.set('timestamp', dt.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +00:00'))
|
||||||
|
root.set('attribution', OSM_ATTRIBUTION)
|
||||||
|
for k, v in xml_extra_info.items():
|
||||||
|
root.set(k, v)
|
||||||
|
|
||||||
|
if simple and not results:
|
||||||
|
ET.SubElement(root, 'error').text = 'Unable to geocode'
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
place = _create_base_entry(result, root, simple, locales)
|
||||||
|
|
||||||
|
if not simple and options.get('icon_base_url', None):
|
||||||
|
icon = ICONS.get(result.category)
|
||||||
|
if icon:
|
||||||
|
place.set('icon', icon)
|
||||||
|
|
||||||
|
if options.get('addressdetails', False) and result.address_rows:
|
||||||
|
_write_xml_address(ET.SubElement(root, 'addressparts') if simple else place,
|
||||||
|
result.address_rows, result.country_code)
|
||||||
|
|
||||||
|
if options.get('extratags', False):
|
||||||
|
eroot = ET.SubElement(root if simple else place, 'extratags')
|
||||||
|
if result.extratags:
|
||||||
|
for k, v in result.extratags.items():
|
||||||
|
ET.SubElement(eroot, 'tag', attrib={'key': k, 'value': v})
|
||||||
|
|
||||||
|
if options.get('namedetails', False):
|
||||||
|
eroot = ET.SubElement(root if simple else place, 'namedetails')
|
||||||
|
if result.names:
|
||||||
|
for k,v in result.names.items():
|
||||||
|
ET.SubElement(eroot, 'name', attrib={'desc': k}).text = v
|
||||||
|
|
||||||
|
return '<?xml version="1.0" encoding="UTF-8" ?>\n' + ET.tostring(root, encoding='unicode')
|
@ -6,6 +6,9 @@
|
|||||||
# For a full list of authors see the git log.
|
# For a full list of authors see the git log.
|
||||||
"""
|
"""
|
||||||
Tests for formatting results for the V1 API.
|
Tests for formatting results for the V1 API.
|
||||||
|
|
||||||
|
These test only ensure that the Python code is correct.
|
||||||
|
For functional tests see BDD test suite.
|
||||||
"""
|
"""
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import json
|
import json
|
||||||
@ -165,6 +168,28 @@ def test_search_details_with_geometry():
|
|||||||
assert js['isarea'] == False
|
assert js['isarea'] == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_details_with_icon_available():
|
||||||
|
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
||||||
|
('amenity', 'restaurant'),
|
||||||
|
napi.Point(1.0, 2.0))
|
||||||
|
|
||||||
|
result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
|
||||||
|
js = json.loads(result)
|
||||||
|
|
||||||
|
assert js['icon'] == 'foo/food_restaurant.p.20.png'
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_details_with_icon_not_available():
|
||||||
|
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
||||||
|
('amenity', 'tree'),
|
||||||
|
napi.Point(1.0, 2.0))
|
||||||
|
|
||||||
|
result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
|
||||||
|
js = json.loads(result)
|
||||||
|
|
||||||
|
assert 'icon' not in js
|
||||||
|
|
||||||
|
|
||||||
def test_search_details_with_address_minimal():
|
def test_search_details_with_address_minimal():
|
||||||
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
||||||
('place', 'thing'),
|
('place', 'thing'),
|
||||||
@ -193,28 +218,32 @@ def test_search_details_with_address_minimal():
|
|||||||
'isaddress': False}]
|
'isaddress': False}]
|
||||||
|
|
||||||
|
|
||||||
def test_search_details_with_address_full():
|
@pytest.mark.parametrize('field,outfield', [('address_rows', 'address'),
|
||||||
|
('linked_rows', 'linked_places'),
|
||||||
|
('parented_rows', 'hierarchy')
|
||||||
|
])
|
||||||
|
def test_search_details_with_further_infos(field, outfield):
|
||||||
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
||||||
('place', 'thing'),
|
('place', 'thing'),
|
||||||
napi.Point(1.0, 2.0),
|
napi.Point(1.0, 2.0))
|
||||||
address_rows=[
|
|
||||||
napi.AddressLine(place_id=3498,
|
setattr(search, field, [napi.AddressLine(place_id=3498,
|
||||||
osm_object=('R', 442),
|
osm_object=('R', 442),
|
||||||
category=('bnd', 'note'),
|
category=('bnd', 'note'),
|
||||||
names={'name': 'Trespass'},
|
names={'name': 'Trespass'},
|
||||||
extratags={'access': 'no',
|
extratags={'access': 'no',
|
||||||
'place_type': 'spec'},
|
'place_type': 'spec'},
|
||||||
admin_level=4,
|
admin_level=4,
|
||||||
fromarea=True,
|
fromarea=True,
|
||||||
isaddress=True,
|
isaddress=True,
|
||||||
rank_address=10,
|
rank_address=10,
|
||||||
distance=0.034)
|
distance=0.034)
|
||||||
])
|
])
|
||||||
|
|
||||||
result = api_impl.format_result(search, 'json', {})
|
result = api_impl.format_result(search, 'json', {})
|
||||||
js = json.loads(result)
|
js = json.loads(result)
|
||||||
|
|
||||||
assert js['address'] == [{'localname': 'Trespass',
|
assert js[outfield] == [{'localname': 'Trespass',
|
||||||
'place_id': 3498,
|
'place_id': 3498,
|
||||||
'osm_id': 442,
|
'osm_id': 442,
|
||||||
'osm_type': 'R',
|
'osm_type': 'R',
|
||||||
@ -225,3 +254,70 @@ def test_search_details_with_address_full():
|
|||||||
'rank_address': 10,
|
'rank_address': 10,
|
||||||
'distance': 0.034,
|
'distance': 0.034,
|
||||||
'isaddress': True}]
|
'isaddress': True}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_details_grouped_hierarchy():
|
||||||
|
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
parented_rows =
|
||||||
|
[napi.AddressLine(place_id=3498,
|
||||||
|
osm_object=('R', 442),
|
||||||
|
category=('bnd', 'note'),
|
||||||
|
names={'name': 'Trespass'},
|
||||||
|
extratags={'access': 'no',
|
||||||
|
'place_type': 'spec'},
|
||||||
|
admin_level=4,
|
||||||
|
fromarea=True,
|
||||||
|
isaddress=True,
|
||||||
|
rank_address=10,
|
||||||
|
distance=0.034)
|
||||||
|
])
|
||||||
|
|
||||||
|
result = api_impl.format_result(search, 'json', {'group_hierarchy': True})
|
||||||
|
js = json.loads(result)
|
||||||
|
|
||||||
|
assert js['hierarchy'] == {'note': [{'localname': 'Trespass',
|
||||||
|
'place_id': 3498,
|
||||||
|
'osm_id': 442,
|
||||||
|
'osm_type': 'R',
|
||||||
|
'place_type': 'spec',
|
||||||
|
'class': 'bnd',
|
||||||
|
'type': 'note',
|
||||||
|
'admin_level': 4,
|
||||||
|
'rank_address': 10,
|
||||||
|
'distance': 0.034,
|
||||||
|
'isaddress': True}]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_details_keywords_name():
|
||||||
|
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
name_keywords=[
|
||||||
|
napi.WordInfo(23, 'foo', 'mefoo'),
|
||||||
|
napi.WordInfo(24, 'foo', 'bafoo')])
|
||||||
|
|
||||||
|
result = api_impl.format_result(search, 'json', {'keywords': True})
|
||||||
|
js = json.loads(result)
|
||||||
|
|
||||||
|
assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'},
|
||||||
|
{'id': 24, 'token': 'foo'}],
|
||||||
|
'address': []}
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_details_keywords_address():
|
||||||
|
search = napi.DetailedResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
address_keywords=[
|
||||||
|
napi.WordInfo(23, 'foo', 'mefoo'),
|
||||||
|
napi.WordInfo(24, 'foo', 'bafoo')])
|
||||||
|
|
||||||
|
result = api_impl.format_result(search, 'json', {'keywords': True})
|
||||||
|
js = json.loads(result)
|
||||||
|
|
||||||
|
assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'},
|
||||||
|
{'id': 24, 'token': 'foo'}],
|
||||||
|
'name': []}
|
||||||
|
|
||||||
|
320
test/python/api/test_result_formatting_v1_reverse.py
Normal file
320
test/python/api/test_result_formatting_v1_reverse.py
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
#
|
||||||
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 by the Nominatim developer community.
|
||||||
|
# For a full list of authors see the git log.
|
||||||
|
"""
|
||||||
|
Tests for formatting reverse results for the V1 API.
|
||||||
|
|
||||||
|
These test only ensure that the Python code is correct.
|
||||||
|
For functional tests see BDD test suite.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import nominatim.api.v1 as api_impl
|
||||||
|
import nominatim.api as napi
|
||||||
|
|
||||||
|
FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml']
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', FORMATS)
|
||||||
|
def test_format_reverse_minimal(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('amenity', 'post_box'),
|
||||||
|
napi.Point(0.3, -8.9))
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.tag == 'reversegeocode'
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', FORMATS)
|
||||||
|
def test_format_reverse_no_result(fmt):
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults(), fmt, {})
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.find('error').text == 'Unable to geocode'
|
||||||
|
else:
|
||||||
|
assert json.loads(raw) == {'error': 'Unable to geocode'}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', FORMATS)
|
||||||
|
def test_format_reverse_with_osm_id(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('amenity', 'post_box'),
|
||||||
|
napi.Point(0.3, -8.9),
|
||||||
|
place_id=5564,
|
||||||
|
osm_object=('N', 23))
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw).find('result')
|
||||||
|
assert root.attrib['osm_type'] == 'node'
|
||||||
|
assert root.attrib['osm_id'] == '23'
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
if fmt == 'geocodejson':
|
||||||
|
props = result['features'][0]['properties']['geocoding']
|
||||||
|
elif fmt == 'geojson':
|
||||||
|
props = result['features'][0]['properties']
|
||||||
|
else:
|
||||||
|
props = result
|
||||||
|
assert props['osm_type'] == 'node'
|
||||||
|
assert props['osm_id'] == 23
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', FORMATS)
|
||||||
|
def test_format_reverse_with_address(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
country_code='fe',
|
||||||
|
address_rows=napi.AddressLines([
|
||||||
|
napi.AddressLine(place_id=None,
|
||||||
|
osm_object=None,
|
||||||
|
category=('place', 'county'),
|
||||||
|
names={'name': 'Hello'},
|
||||||
|
extratags=None,
|
||||||
|
admin_level=5,
|
||||||
|
fromarea=False,
|
||||||
|
isaddress=True,
|
||||||
|
rank_address=10,
|
||||||
|
distance=0.0),
|
||||||
|
napi.AddressLine(place_id=None,
|
||||||
|
osm_object=None,
|
||||||
|
category=('place', 'county'),
|
||||||
|
names={'name': 'ByeBye'},
|
||||||
|
extratags=None,
|
||||||
|
admin_level=5,
|
||||||
|
fromarea=False,
|
||||||
|
isaddress=False,
|
||||||
|
rank_address=10,
|
||||||
|
distance=0.0)
|
||||||
|
]))
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'addressdetails': True})
|
||||||
|
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.find('addressparts').find('county').text == 'Hello'
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
|
if fmt == 'geocodejson':
|
||||||
|
props = result['features'][0]['properties']['geocoding']
|
||||||
|
assert 'admin' in props
|
||||||
|
assert props['county'] == 'Hello'
|
||||||
|
else:
|
||||||
|
if fmt == 'geojson':
|
||||||
|
props = result['features'][0]['properties']
|
||||||
|
else:
|
||||||
|
props = result
|
||||||
|
assert 'address' in props
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_reverse_geocodejson_special_parts():
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'house'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
place_id=33,
|
||||||
|
country_code='fe',
|
||||||
|
address_rows=napi.AddressLines([
|
||||||
|
napi.AddressLine(place_id=None,
|
||||||
|
osm_object=None,
|
||||||
|
category=('place', 'house_number'),
|
||||||
|
names={'ref': '1'},
|
||||||
|
extratags=None,
|
||||||
|
admin_level=15,
|
||||||
|
fromarea=False,
|
||||||
|
isaddress=True,
|
||||||
|
rank_address=10,
|
||||||
|
distance=0.0),
|
||||||
|
napi.AddressLine(place_id=None,
|
||||||
|
osm_object=None,
|
||||||
|
category=('place', 'postcode'),
|
||||||
|
names={'ref': '99446'},
|
||||||
|
extratags=None,
|
||||||
|
admin_level=11,
|
||||||
|
fromarea=False,
|
||||||
|
isaddress=True,
|
||||||
|
rank_address=10,
|
||||||
|
distance=0.0),
|
||||||
|
napi.AddressLine(place_id=33,
|
||||||
|
osm_object=None,
|
||||||
|
category=('place', 'county'),
|
||||||
|
names={'name': 'Hello'},
|
||||||
|
extratags=None,
|
||||||
|
admin_level=5,
|
||||||
|
fromarea=False,
|
||||||
|
isaddress=True,
|
||||||
|
rank_address=10,
|
||||||
|
distance=0.0)
|
||||||
|
]))
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), 'geocodejson',
|
||||||
|
{'addressdetails': True})
|
||||||
|
|
||||||
|
props = json.loads(raw)['features'][0]['properties']['geocoding']
|
||||||
|
assert props['housenumber'] == '1'
|
||||||
|
assert props['postcode'] == '99446'
|
||||||
|
assert 'county' not in props
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', FORMATS)
|
||||||
|
def test_format_reverse_with_address_none(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
address_rows=napi.AddressLines())
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'addressdetails': True})
|
||||||
|
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.find('addressparts') is None
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
|
if fmt == 'geocodejson':
|
||||||
|
props = result['features'][0]['properties']['geocoding']
|
||||||
|
print(props)
|
||||||
|
assert 'admin' in props
|
||||||
|
else:
|
||||||
|
if fmt == 'geojson':
|
||||||
|
props = result['features'][0]['properties']
|
||||||
|
else:
|
||||||
|
props = result
|
||||||
|
assert 'address' in props
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
|
||||||
|
def test_format_reverse_with_extratags(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
extratags={'one': 'A', 'two':'B'})
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'extratags': True})
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.find('extratags').find('tag').attrib['key'] == 'one'
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
if fmt == 'geojson':
|
||||||
|
extra = result['features'][0]['properties']['extratags']
|
||||||
|
else:
|
||||||
|
extra = result['extratags']
|
||||||
|
|
||||||
|
assert extra == {'one': 'A', 'two':'B'}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
|
||||||
|
def test_format_reverse_with_extratags_none(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0))
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'extratags': True})
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.find('extratags') is not None
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
if fmt == 'geojson':
|
||||||
|
extra = result['features'][0]['properties']['extratags']
|
||||||
|
else:
|
||||||
|
extra = result['extratags']
|
||||||
|
|
||||||
|
assert extra is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
|
||||||
|
def test_format_reverse_with_namedetails_with_name(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0),
|
||||||
|
names={'name': 'A', 'ref':'1'})
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'namedetails': True})
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.find('namedetails').find('name').text == 'A'
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
if fmt == 'geojson':
|
||||||
|
extra = result['features'][0]['properties']['namedetails']
|
||||||
|
else:
|
||||||
|
extra = result['namedetails']
|
||||||
|
|
||||||
|
assert extra == {'name': 'A', 'ref':'1'}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
|
||||||
|
def test_format_reverse_with_namedetails_without_name(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('place', 'thing'),
|
||||||
|
napi.Point(1.0, 2.0))
|
||||||
|
|
||||||
|
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'namedetails': True})
|
||||||
|
|
||||||
|
if fmt == 'xml':
|
||||||
|
root = ET.fromstring(raw)
|
||||||
|
assert root.find('namedetails') is not None
|
||||||
|
else:
|
||||||
|
result = json.loads(raw)
|
||||||
|
if fmt == 'geojson':
|
||||||
|
extra = result['features'][0]['properties']['namedetails']
|
||||||
|
else:
|
||||||
|
extra = result['namedetails']
|
||||||
|
|
||||||
|
assert extra is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
|
||||||
|
def test_search_details_with_icon_available(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('amenity', 'restaurant'),
|
||||||
|
napi.Point(1.0, 2.0))
|
||||||
|
|
||||||
|
result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'icon_base_url': 'foo'})
|
||||||
|
|
||||||
|
js = json.loads(result)
|
||||||
|
|
||||||
|
assert js['icon'] == 'foo/food_restaurant.p.20.png'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
|
||||||
|
def test_search_details_with_icon_not_available(fmt):
|
||||||
|
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
|
||||||
|
('amenity', 'tree'),
|
||||||
|
napi.Point(1.0, 2.0))
|
||||||
|
|
||||||
|
result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||||
|
{'icon_base_url': 'foo'})
|
||||||
|
|
||||||
|
assert 'icon' not in json.loads(result)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user