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, Union
12 import xml.etree.ElementTree as ET
14 import nominatim.api as napi
15 import nominatim.api.v1.classtypes as cl
17 #pylint: disable=too-many-branches
19 def _write_xml_address(root: ET.Element, address: napi.AddressLines,
20 country_code: Optional[str]) -> None:
25 label = cl.get_label_tag(line.category, line.extratags,
26 line.rank_address, country_code)
27 if label not in parts:
28 parts[label] = line.local_name
29 if line.names and 'ISO3166-2' in line.names and line.admin_level:
30 parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2']
32 for k,v in parts.items():
33 ET.SubElement(root, k).text = v
36 ET.SubElement(root, 'country_code').text = country_code
39 def _create_base_entry(result: Union[napi.ReverseResult, napi.SearchResult],
40 root: ET.Element, simple: bool) -> ET.Element:
41 place = ET.SubElement(root, 'result' if simple else 'place')
42 if result.place_id is not None:
43 place.set('place_id', str(result.place_id))
45 osm_type = cl.OSM_TYPE_NAME.get(result.osm_object[0], None)
46 if osm_type is not None:
47 place.set('osm_type', osm_type)
48 place.set('osm_id', str(result.osm_object[1]))
49 if result.names and 'ref' in result.names:
50 place.set('ref', result.names['ref'])
51 elif result.locale_name:
52 # bug reproduced from PHP
53 place.set('ref', result.locale_name)
54 place.set('lat', f"{result.centroid.lat:.7f}")
55 place.set('lon', f"{result.centroid.lon:.7f}")
57 bbox = cl.bbox_from_result(result)
58 place.set('boundingbox',
59 f"{bbox.minlat:.7f},{bbox.maxlat:.7f},{bbox.minlon:.7f},{bbox.maxlon:.7f}")
61 place.set('place_rank', str(result.rank_search))
62 place.set('address_rank', str(result.rank_address))
65 for key in ('text', 'svg'):
66 if key in result.geometry:
67 place.set('geo' + key, result.geometry[key])
68 if 'kml' in result.geometry:
69 ET.SubElement(root if simple else place, 'geokml')\
70 .append(ET.fromstring(result.geometry['kml']))
71 if 'geojson' in result.geometry:
72 place.set('geojson', result.geometry['geojson'])
75 place.text = result.display_name or ''
77 place.set('display_name', result.display_name or '')
78 place.set('class', result.category[0])
79 place.set('type', result.category[1])
80 place.set('importance', str(result.calculated_importance()))
85 def format_base_xml(results: Union[napi.ReverseResults, napi.SearchResults],
86 options: Mapping[str, Any],
87 simple: bool, xml_root_tag: str,
88 xml_extra_info: Mapping[str, str]) -> str:
89 """ Format the result into an XML response. With 'simple' exactly one
90 result will be output, otherwise a list.
92 root = ET.Element(xml_root_tag)
93 root.set('timestamp', dt.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +00:00'))
94 root.set('attribution', cl.OSM_ATTRIBUTION)
95 for k, v in xml_extra_info.items():
98 if simple and not results:
99 ET.SubElement(root, 'error').text = 'Unable to geocode'
101 for result in results:
102 place = _create_base_entry(result, root, simple)
104 if not simple and options.get('icon_base_url', None):
105 icon = cl.ICONS.get(result.category)
107 place.set('icon', icon)
109 if options.get('addressdetails', False) and result.address_rows:
110 _write_xml_address(ET.SubElement(root, 'addressparts') if simple else place,
111 result.address_rows, result.country_code)
113 if options.get('extratags', False):
114 eroot = ET.SubElement(root if simple else place, 'extratags')
116 for k, v in result.extratags.items():
117 ET.SubElement(eroot, 'tag', attrib={'key': k, 'value': v})
119 if options.get('namedetails', False):
120 eroot = ET.SubElement(root if simple else place, 'namedetails')
122 for k,v in result.names.items():
123 ET.SubElement(eroot, 'name', attrib={'desc': k}).text = v
125 return '<?xml version="1.0" encoding="UTF-8" ?>\n' + ET.tostring(root, encoding='unicode')