]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/api/v1/format_xml.py
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / nominatim / api / v1 / format_xml.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Helper functions for output of results in XML format.
9 """
10 from typing import Mapping, Any, Optional, Union
11 import datetime as dt
12 import xml.etree.ElementTree as ET
13
14 import nominatim.api as napi
15 import nominatim.api.v1.classtypes as cl
16
17 #pylint: disable=too-many-branches
18
19 def _write_xml_address(root: ET.Element, address: napi.AddressLines,
20                        country_code: Optional[str]) -> None:
21     parts = {}
22     for line in address:
23         if line.isaddress:
24             if line.local_name:
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']
31
32     for k,v in parts.items():
33         ET.SubElement(root, k).text = v
34
35     if country_code:
36         ET.SubElement(root, 'country_code').text = country_code
37
38
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))
44     if result.osm_object:
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}")
56
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}")
60
61     place.set('place_rank', str(result.rank_search))
62     place.set('address_rank', str(result.rank_address))
63
64     if result.geometry:
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'])
73
74     if simple:
75         place.text = result.display_name or ''
76     else:
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()))
81
82     return place
83
84
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.
91     """
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():
96         root.set(k, v)
97
98     if simple and not results:
99         ET.SubElement(root, 'error').text = 'Unable to geocode'
100
101     for result in results:
102         place = _create_base_entry(result, root, simple)
103
104         if not simple and options.get('icon_base_url', None):
105             icon = cl.ICONS.get(result.category)
106             if icon:
107                 place.set('icon', icon)
108
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)
112
113         if options.get('extratags', False):
114             eroot = ET.SubElement(root if simple else place, 'extratags')
115             if result.extratags:
116                 for k, v in result.extratags.items():
117                     ET.SubElement(eroot, 'tag', attrib={'key': k, 'value': v})
118
119         if options.get('namedetails', False):
120             eroot = ET.SubElement(root if simple else place, 'namedetails')
121             if result.names:
122                 for k,v in result.names.items():
123                     ET.SubElement(eroot, 'name', attrib={'desc': k}).text = v
124
125     return '<?xml version="1.0" encoding="UTF-8" ?>\n' + ET.tostring(root, encoding='unicode')