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 import nominatim.api.v1.classtypes as cl
17 def _write_xml_address(root: ET.Element, address: napi.AddressLines,
18 country_code: Optional[str]) -> None:
23 label = cl.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
27 if line.names and 'ISO3166-2' in line.names and line.admin_level:
28 parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2']
30 for k,v in parts.items():
31 ET.SubElement(root, k).text = v
34 ET.SubElement(root, 'country_code').text = country_code
37 def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-branches
38 root: ET.Element, simple: bool,
39 locales: napi.Locales) -> ET.Element:
40 if result.address_rows:
41 label_parts = result.address_rows.localize(locales)
45 place = ET.SubElement(root, 'result' if simple else 'place')
46 if result.place_id is not None:
47 place.set('place_id', str(result.place_id))
49 osm_type = cl.OSM_TYPE_NAME.get(result.osm_object[0], None)
50 if osm_type is not None:
51 place.set('osm_type', osm_type)
52 place.set('osm_id', str(result.osm_object[1]))
53 if result.names and 'ref' in result.names:
54 place.set('ref', result.names['ref'])
56 # bug reproduced from PHP
57 place.set('ref', label_parts[0])
58 place.set('lat', f"{result.centroid.lat:.7f}")
59 place.set('lon', f"{result.centroid.lon:.7f}")
61 bbox = cl.bbox_from_result(result)
62 place.set('boundingbox',
63 f"{bbox.minlat:.7f},{bbox.maxlat:.7f},{bbox.minlon:.7f},{bbox.maxlon:.7f}")
65 place.set('place_rank', str(result.rank_search))
66 place.set('address_rank', str(result.rank_address))
69 for key in ('text', 'svg'):
70 if key in result.geometry:
71 place.set('geo' + key, result.geometry[key])
72 if 'kml' in result.geometry:
73 ET.SubElement(root if simple else place, 'geokml')\
74 .append(ET.fromstring(result.geometry['kml']))
75 if 'geojson' in result.geometry:
76 place.set('geojson', result.geometry['geojson'])
79 place.text = ', '.join(label_parts)
81 place.set('display_name', ', '.join(label_parts))
82 place.set('class', result.category[0])
83 place.set('type', result.category[1])
84 place.set('importance', str(result.calculated_importance()))
89 def format_base_xml(results: napi.ReverseResults,
90 options: Mapping[str, Any],
91 simple: bool, xml_root_tag: str,
92 xml_extra_info: Mapping[str, str]) -> str:
93 """ Format the result into an XML response. With 'simple' exactly one
94 result will be output, otherwise a list.
96 locales = options.get('locales', napi.Locales())
98 root = ET.Element(xml_root_tag)
99 root.set('timestamp', dt.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +00:00'))
100 root.set('attribution', cl.OSM_ATTRIBUTION)
101 for k, v in xml_extra_info.items():
104 if simple and not results:
105 ET.SubElement(root, 'error').text = 'Unable to geocode'
107 for result in results:
108 place = _create_base_entry(result, root, simple, locales)
110 if not simple and options.get('icon_base_url', None):
111 icon = cl.ICONS.get(result.category)
113 place.set('icon', icon)
115 if options.get('addressdetails', False) and result.address_rows:
116 _write_xml_address(ET.SubElement(root, 'addressparts') if simple else place,
117 result.address_rows, result.country_code)
119 if options.get('extratags', False):
120 eroot = ET.SubElement(root if simple else place, 'extratags')
122 for k, v in result.extratags.items():
123 ET.SubElement(eroot, 'tag', attrib={'key': k, 'value': v})
125 if options.get('namedetails', False):
126 eroot = ET.SubElement(root if simple else place, 'namedetails')
128 for k,v in result.names.items():
129 ET.SubElement(eroot, 'name', attrib={'desc': k}).text = v
131 return '<?xml version="1.0" encoding="UTF-8" ?>\n' + ET.tostring(root, encoding='unicode')