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')