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,
41 locales: napi.Locales) -> ET.Element:
42 if result.address_rows:
43 label_parts = result.address_rows.localize(locales)
47 place = ET.SubElement(root, 'result' if simple else 'place')
48 if result.place_id is not None:
49 place.set('place_id', str(result.place_id))
51 osm_type = cl.OSM_TYPE_NAME.get(result.osm_object[0], None)
52 if osm_type is not None:
53 place.set('osm_type', osm_type)
54 place.set('osm_id', str(result.osm_object[1]))
55 if result.names and 'ref' in result.names:
56 place.set('ref', result.names['ref'])
58 # bug reproduced from PHP
59 place.set('ref', label_parts[0])
60 place.set('lat', f"{result.centroid.lat:.7f}")
61 place.set('lon', f"{result.centroid.lon:.7f}")
63 bbox = cl.bbox_from_result(result)
64 place.set('boundingbox',
65 f"{bbox.minlat:.7f},{bbox.maxlat:.7f},{bbox.minlon:.7f},{bbox.maxlon:.7f}")
67 place.set('place_rank', str(result.rank_search))
68 place.set('address_rank', str(result.rank_address))
71 for key in ('text', 'svg'):
72 if key in result.geometry:
73 place.set('geo' + key, result.geometry[key])
74 if 'kml' in result.geometry:
75 ET.SubElement(root if simple else place, 'geokml')\
76 .append(ET.fromstring(result.geometry['kml']))
77 if 'geojson' in result.geometry:
78 place.set('geojson', result.geometry['geojson'])
81 place.text = ', '.join(label_parts)
83 place.set('display_name', ', '.join(label_parts))
84 place.set('class', result.category[0])
85 place.set('type', result.category[1])
86 place.set('importance', str(result.calculated_importance()))
91 def format_base_xml(results: Union[napi.ReverseResults, napi.SearchResults],
92 options: Mapping[str, Any],
93 simple: bool, xml_root_tag: str,
94 xml_extra_info: Mapping[str, str]) -> str:
95 """ Format the result into an XML response. With 'simple' exactly one
96 result will be output, otherwise a list.
98 locales = options.get('locales', napi.Locales())
100 root = ET.Element(xml_root_tag)
101 root.set('timestamp', dt.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +00:00'))
102 root.set('attribution', cl.OSM_ATTRIBUTION)
103 for k, v in xml_extra_info.items():
106 if simple and not results:
107 ET.SubElement(root, 'error').text = 'Unable to geocode'
109 for result in results:
110 place = _create_base_entry(result, root, simple, locales)
112 if not simple and options.get('icon_base_url', None):
113 icon = cl.ICONS.get(result.category)
115 place.set('icon', icon)
117 if options.get('addressdetails', False) and result.address_rows:
118 _write_xml_address(ET.SubElement(root, 'addressparts') if simple else place,
119 result.address_rows, result.country_code)
121 if options.get('extratags', False):
122 eroot = ET.SubElement(root if simple else place, 'extratags')
124 for k, v in result.extratags.items():
125 ET.SubElement(eroot, 'tag', attrib={'key': k, 'value': v})
127 if options.get('namedetails', False):
128 eroot = ET.SubElement(root if simple else place, 'namedetails')
130 for k,v in result.names.items():
131 ET.SubElement(eroot, 'name', attrib={'desc': k}).text = v
133 return '<?xml version="1.0" encoding="UTF-8" ?>\n' + ET.tostring(root, encoding='unicode')