1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2024 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 from ..results import AddressLines, ReverseResult, ReverseResults, \
 
  15                       SearchResult, SearchResults
 
  16 from . import classtypes as cl
 
  17 from ..types import EntranceDetails
 
  20 def _write_xml_address(root: ET.Element, address: AddressLines,
 
  21                        country_code: Optional[str]) -> None:
 
  26                 label = cl.get_label_tag(line.category, line.extratags,
 
  27                                          line.rank_address, country_code)
 
  28                 if label not in parts:
 
  29                     parts[label] = line.local_name
 
  30             if line.names and 'ISO3166-2' in line.names and line.admin_level:
 
  31                 parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2']
 
  33     for k, v in parts.items():
 
  34         ET.SubElement(root, k).text = v
 
  37         ET.SubElement(root, 'country_code').text = country_code
 
  40 def _create_base_entry(result: Union[ReverseResult, SearchResult],
 
  41                        root: ET.Element, simple: bool) -> ET.Element:
 
  42     place = ET.SubElement(root, 'result' if simple else 'place')
 
  43     if result.place_id is not None:
 
  44         place.set('place_id', str(result.place_id))
 
  46         osm_type = cl.OSM_TYPE_NAME.get(result.osm_object[0], None)
 
  47         if osm_type is not None:
 
  48             place.set('osm_type', osm_type)
 
  49         place.set('osm_id', str(result.osm_object[1]))
 
  50     if result.names and 'ref' in result.names:
 
  51         place.set('ref', result.names['ref'])
 
  52     elif result.locale_name:
 
  53         # bug reproduced from PHP
 
  54         place.set('ref', result.locale_name)
 
  55     place.set('lat', f"{result.centroid.lat:.7f}")
 
  56     place.set('lon', f"{result.centroid.lon:.7f}")
 
  58     bbox = cl.bbox_from_result(result)
 
  59     place.set('boundingbox',
 
  60               f"{bbox.minlat:.7f},{bbox.maxlat:.7f},{bbox.minlon:.7f},{bbox.maxlon:.7f}")
 
  62     place.set('place_rank', str(result.rank_search))
 
  63     place.set('address_rank', str(result.rank_address))
 
  66         for key in ('text', 'svg'):
 
  67             if key in result.geometry:
 
  68                 place.set('geo' + key, result.geometry[key])
 
  69         if 'kml' in result.geometry:
 
  70             ET.SubElement(root if simple else place, 'geokml')\
 
  71               .append(ET.fromstring(result.geometry['kml']))
 
  72         if 'geojson' in result.geometry:
 
  73             place.set('geojson', result.geometry['geojson'])
 
  76         place.text = result.display_name or ''
 
  78         place.set('display_name', result.display_name or '')
 
  79         place.set('class', result.category[0])
 
  80         place.set('type', result.category[1])
 
  81         place.set('importance', str(result.calculated_importance()))
 
  86 def _create_entrance(root: ET.Element, entrance: EntranceDetails) -> None:
 
  87     entrance_node = ET.SubElement(root, 'entrance', attrib={
 
  88         "osm_id": str(entrance.osm_id),
 
  89         "type": entrance.type,
 
  90         "lat": f"{entrance.location.lat:0.7f}",
 
  91         "lon": f"{entrance.location.lon:0.7f}",
 
  93     if entrance.extratags:
 
  94         for k, v in entrance.extratags.items():
 
  95             ET.SubElement(entrance_node, 'tag', attrib={'key': k, 'value': v})
 
  98 def format_base_xml(results: Union[ReverseResults, SearchResults],
 
  99                     options: Mapping[str, Any],
 
 100                     simple: bool, xml_root_tag: str,
 
 101                     xml_extra_info: Mapping[str, str]) -> str:
 
 102     """ Format the result into an XML response. With 'simple' exactly one
 
 103         result will be output, otherwise a list.
 
 105     root = ET.Element(xml_root_tag)
 
 106     root.set('timestamp', dt.datetime.now(dt.timezone.utc).strftime('%a, %d %b %Y %H:%M:%S +00:00'))
 
 107     root.set('attribution', cl.OSM_ATTRIBUTION)
 
 108     for k, v in xml_extra_info.items():
 
 111     if simple and not results:
 
 112         ET.SubElement(root, 'error').text = 'Unable to geocode'
 
 114     for result in results:
 
 115         place = _create_base_entry(result, root, simple)
 
 117         if not simple and options.get('icon_base_url', None):
 
 118             icon = cl.ICONS.get(result.category)
 
 120                 place.set('icon', icon)
 
 122         if options.get('addressdetails', False) and result.address_rows:
 
 123             _write_xml_address(ET.SubElement(root, 'addressparts') if simple else place,
 
 124                                result.address_rows, result.country_code)
 
 126         if options.get('extratags', False):
 
 127             eroot = ET.SubElement(root if simple else place, 'extratags')
 
 129                 for k, v in result.extratags.items():
 
 130                     ET.SubElement(eroot, 'tag', attrib={'key': k, 'value': v})
 
 132         if options.get('namedetails', False):
 
 133             eroot = ET.SubElement(root if simple else place, 'namedetails')
 
 135                 for k, v in result.names.items():
 
 136                     ET.SubElement(eroot, 'name', attrib={'desc': k}).text = v
 
 138         if options.get('entrances', False):
 
 139             eroot = ET.SubElement(root if simple else place, 'entrances')
 
 141                 for entrance in result.entrances:
 
 142                     _create_entrance(eroot, entrance)
 
 144     return '<?xml version="1.0" encoding="UTF-8" ?>\n' + ET.tostring(root, encoding='unicode')