1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Helper functions for output of results in XML format.
10 from typing import Mapping, Any, Optional
12 import xml.etree.ElementTree as ET
14 import nominatim.api as napi
15 from nominatim.api.v1.constants import OSM_ATTRIBUTION, OSM_TYPE_NAME, bbox_from_result
16 from nominatim.api.v1.classtypes import ICONS, get_label_tag
18 def _write_xml_address(root: ET.Element, address: napi.AddressLines,
19 country_code: Optional[str]) -> None:
22 if line.isaddress and line.local_name:
23 label = get_label_tag(line.category, line.extratags,
24 line.rank_address, country_code)
25 if label not in parts:
26 parts[label] = line.local_name
28 for k,v in parts.items():
29 ET.SubElement(root, k).text = v
32 ET.SubElement(root, 'country_code').text = country_code
35 def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-branches
36 root: ET.Element, simple: bool,
37 locales: napi.Locales) -> ET.Element:
38 if result.address_rows:
39 label_parts = result.address_rows.localize(locales)
43 place = ET.SubElement(root, 'result' if simple else 'place')
44 if result.place_id is not None:
45 place.set('place_id', str(result.place_id))
47 osm_type = OSM_TYPE_NAME.get(result.osm_object[0], None)
48 if osm_type is not None:
49 place.set('osm_type', osm_type)
50 place.set('osm_id', str(result.osm_object[1]))
51 if result.names and 'ref' in result.names:
52 place.set('place_id', result.names['ref'])
53 place.set('lat', str(result.centroid.lat))
54 place.set('lon', str(result.centroid.lon))
56 bbox = bbox_from_result(result)
57 place.set('boundingbox', ','.join(map(str, [bbox.minlat, bbox.maxlat,
58 bbox.minlon, bbox.maxlon])))
60 place.set('place_rank', str(result.rank_search))
61 place.set('address_rank', str(result.rank_address))
64 for key in ('text', 'svg'):
65 if key in result.geometry:
66 place.set('geo' + key, result.geometry[key])
67 if 'kml' in result.geometry:
68 ET.SubElement(root if simple else place, 'geokml')\
69 .append(ET.fromstring(result.geometry['kml']))
70 if 'geojson' in result.geometry:
71 place.set('geojson', result.geometry['geojson'])
74 place.text = ', '.join(label_parts)
76 place.set('display_name', ', '.join(label_parts))
77 place.set('class', result.category[0])
78 place.set('type', result.category[1])
79 place.set('importance', str(result.calculated_importance()))
84 def format_base_xml(results: napi.ReverseResults,
85 options: Mapping[str, Any],
86 simple: bool, xml_root_tag: str,
87 xml_extra_info: Mapping[str, str]) -> str:
88 """ Format the result into an XML response. With 'simple' exactly one
89 result will be output, otherwise a list.
91 locales = options.get('locales', napi.Locales())
93 root = ET.Element(xml_root_tag)
94 root.set('timestamp', dt.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +00:00'))
95 root.set('attribution', OSM_ATTRIBUTION)
96 for k, v in xml_extra_info.items():
99 if simple and not results:
100 ET.SubElement(root, 'error').text = 'Unable to geocode'
102 for result in results:
103 place = _create_base_entry(result, root, simple, locales)
105 if not simple and options.get('icon_base_url', None):
106 icon = ICONS.get(result.category)
108 place.set('icon', icon)
110 if options.get('addressdetails', False) and result.address_rows:
111 _write_xml_address(ET.SubElement(root, 'addressparts') if simple else place,
112 result.address_rows, result.country_code)
114 if options.get('extratags', False):
115 eroot = ET.SubElement(root if simple else place, 'extratags')
117 for k, v in result.extratags.items():
118 ET.SubElement(eroot, 'tag', attrib={'key': k, 'value': v})
120 if options.get('namedetails', False):
121 eroot = ET.SubElement(root if simple else place, 'namedetails')
123 for k,v in result.names.items():
124 ET.SubElement(eroot, 'name', attrib={'desc': k}).text = v
126 return '<?xml version="1.0" encoding="UTF-8" ?>\n' + ET.tostring(root, encoding='unicode')