]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/api/v1/format.py
2442bb76a74aac562e867296782164b3386e394a
[nominatim.git] / nominatim / api / v1 / format.py
1 # SPDX-License-Identifier: GPL-2.0-only
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 Output formatters for API version v1.
9 """
10 from typing import List, Dict, Mapping, Any
11 import collections
12
13 import nominatim.api as napi
14 from nominatim.api.result_formatting import FormatDispatcher
15 from nominatim.api.v1.classtypes import ICONS
16 from nominatim.api.v1 import format_json, format_xml
17 from nominatim.utils.json_writer import JsonWriter
18
19 class RawDataList(List[Dict[str, Any]]):
20     """ Data type for formatting raw data lists 'as is' in json.
21     """
22
23 dispatch = FormatDispatcher()
24
25 @dispatch.format_func(napi.StatusResult, 'text')
26 def _format_status_text(result: napi.StatusResult, _: Mapping[str, Any]) -> str:
27     if result.status:
28         return f"ERROR: {result.message}"
29
30     return 'OK'
31
32
33 @dispatch.format_func(napi.StatusResult, 'json')
34 def _format_status_json(result: napi.StatusResult, _: Mapping[str, Any]) -> str:
35     out = JsonWriter()
36
37     out.start_object()\
38          .keyval('status', result.status)\
39          .keyval('message', result.message)\
40          .keyval_not_none('data_updated', result.data_updated,
41                           lambda v: v.isoformat())\
42          .keyval('software_version', str(result.software_version))\
43          .keyval_not_none('database_version', result.database_version, str)\
44        .end_object()
45
46     return out()
47
48
49 def _add_address_row(writer: JsonWriter, row: napi.AddressLine,
50                      locales: napi.Locales) -> None:
51     writer.start_object()\
52             .keyval('localname', locales.display_name(row.names))\
53             .keyval_not_none('place_id', row.place_id)
54
55     if row.osm_object is not None:
56         writer.keyval('osm_id', row.osm_object[1])\
57               .keyval('osm_type', row.osm_object[0])
58
59     if row.extratags:
60         writer.keyval_not_none('place_type', row.extratags.get('place_type'))
61
62     writer.keyval('class', row.category[0])\
63           .keyval('type', row.category[1])\
64           .keyval_not_none('admin_level', row.admin_level)\
65           .keyval('rank_address', row.rank_address)\
66           .keyval('distance', row.distance)\
67           .keyval('isaddress', row.isaddress)\
68         .end_object()
69
70
71 def _add_address_rows(writer: JsonWriter, section: str, rows: napi.AddressLines,
72                       locales: napi.Locales) -> None:
73     writer.key(section).start_array()
74     for row in rows:
75         _add_address_row(writer, row, locales)
76         writer.next()
77     writer.end_array().next()
78
79
80 def _add_parent_rows_grouped(writer: JsonWriter, rows: napi.AddressLines,
81                              locales: napi.Locales) -> None:
82     # group by category type
83     data = collections.defaultdict(list)
84     for row in rows:
85         sub = JsonWriter()
86         _add_address_row(sub, row, locales)
87         data[row.category[1]].append(sub())
88
89     writer.key('hierarchy').start_object()
90     for group, grouped in data.items():
91         writer.key(group).start_array()
92         grouped.sort() # sorts alphabetically by local name
93         for line in grouped:
94             writer.raw(line).next()
95         writer.end_array().next()
96
97     writer.end_object().next()
98
99
100 @dispatch.format_func(napi.DetailedResult, 'json')
101 def _format_details_json(result: napi.DetailedResult, options: Mapping[str, Any]) -> str:
102     locales = options.get('locales', napi.Locales())
103     geom = result.geometry.get('geojson')
104     centroid = result.centroid.to_geojson()
105
106     out = JsonWriter()
107     out.start_object()\
108          .keyval_not_none('place_id', result.place_id)\
109          .keyval_not_none('parent_place_id', result.parent_place_id)
110
111     if result.osm_object is not None:
112         out.keyval('osm_type', result.osm_object[0])\
113            .keyval('osm_id', result.osm_object[1])
114
115     out.keyval('category', result.category[0])\
116          .keyval('type', result.category[1])\
117          .keyval('admin_level', result.admin_level)\
118          .keyval('localname', result.locale_name or '')\
119          .keyval('names', result.names or {})\
120          .keyval('addresstags', result.address or {})\
121          .keyval_not_none('housenumber', result.housenumber)\
122          .keyval_not_none('calculated_postcode', result.postcode)\
123          .keyval_not_none('country_code', result.country_code)\
124          .keyval_not_none('indexed_date', result.indexed_date, lambda v: v.isoformat())\
125          .keyval_not_none('importance', result.importance)\
126          .keyval('calculated_importance', result.calculated_importance())\
127          .keyval('extratags', result.extratags or {})\
128          .keyval_not_none('calculated_wikipedia', result.wikipedia)\
129          .keyval('rank_address', result.rank_address)\
130          .keyval('rank_search', result.rank_search)\
131          .keyval('isarea', 'Polygon' in (geom or result.geometry.get('type') or ''))\
132          .key('centroid').raw(centroid).next()\
133          .key('geometry').raw(geom or centroid).next()
134
135     if options.get('icon_base_url', None):
136         icon = ICONS.get(result.category)
137         if icon:
138             out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
139
140     if result.address_rows is not None:
141         _add_address_rows(out, 'address', result.address_rows, locales)
142
143     if result.linked_rows is not None:
144         _add_address_rows(out, 'linked_places', result.linked_rows, locales)
145
146     if result.name_keywords is not None or result.address_keywords is not None:
147         out.key('keywords').start_object()
148
149         for sec, klist in (('name', result.name_keywords), ('address', result.address_keywords)):
150             out.key(sec).start_array()
151             for word in (klist or []):
152                 out.start_object()\
153                      .keyval('id', word.word_id)\
154                      .keyval('token', word.word_token)\
155                    .end_object().next()
156             out.end_array().next()
157
158         out.end_object().next()
159
160     if result.parented_rows is not None:
161         if options.get('group_hierarchy', False):
162             _add_parent_rows_grouped(out, result.parented_rows, locales)
163         else:
164             _add_address_rows(out, 'hierarchy', result.parented_rows, locales)
165
166     out.end_object()
167
168     return out()
169
170
171 @dispatch.format_func(napi.ReverseResults, 'xml')
172 def _format_reverse_xml(results: napi.ReverseResults, options: Mapping[str, Any]) -> str:
173     return format_xml.format_base_xml(results,
174                                       options, True, 'reversegeocode',
175                                       {'querystring': options.get('query', '')})
176
177
178 @dispatch.format_func(napi.ReverseResults, 'geojson')
179 def _format_reverse_geojson(results: napi.ReverseResults,
180                             options: Mapping[str, Any]) -> str:
181     return format_json.format_base_geojson(results, options, True)
182
183
184 @dispatch.format_func(napi.ReverseResults, 'geocodejson')
185 def _format_reverse_geocodejson(results: napi.ReverseResults,
186                                 options: Mapping[str, Any]) -> str:
187     return format_json.format_base_geocodejson(results, options, True)
188
189
190 @dispatch.format_func(napi.ReverseResults, 'json')
191 def _format_reverse_json(results: napi.ReverseResults,
192                          options: Mapping[str, Any]) -> str:
193     return format_json.format_base_json(results, options, True,
194                                         class_label='class')
195
196
197 @dispatch.format_func(napi.ReverseResults, 'jsonv2')
198 def _format_reverse_jsonv2(results: napi.ReverseResults,
199                            options: Mapping[str, Any]) -> str:
200     return format_json.format_base_json(results, options, True,
201                                         class_label='category')
202
203
204 @dispatch.format_func(napi.SearchResults, 'xml')
205 def _format_search_xml(results: napi.SearchResults, options: Mapping[str, Any]) -> str:
206     extra = {'querystring': options.get('query', '')}
207     for attr in ('more_url', 'exclude_place_ids', 'viewbox'):
208         if options.get(attr):
209             extra[attr] = options[attr]
210     return format_xml.format_base_xml(results, options, False, 'searchresults',
211                                       extra)
212
213
214
215 @dispatch.format_func(napi.SearchResults, 'geojson')
216 def _format_search_geojson(results: napi.SearchResults,
217                             options: Mapping[str, Any]) -> str:
218     return format_json.format_base_geojson(results, options, False)
219
220
221 @dispatch.format_func(napi.SearchResults, 'geocodejson')
222 def _format_search_geocodejson(results: napi.SearchResults,
223                                 options: Mapping[str, Any]) -> str:
224     return format_json.format_base_geocodejson(results, options, False)
225
226
227 @dispatch.format_func(napi.SearchResults, 'json')
228 def _format_search_json(results: napi.SearchResults,
229                          options: Mapping[str, Any]) -> str:
230     return format_json.format_base_json(results, options, False,
231                                         class_label='class')
232
233
234 @dispatch.format_func(napi.SearchResults, 'jsonv2')
235 def _format_search_jsonv2(results: napi.SearchResults,
236                            options: Mapping[str, Any]) -> str:
237     return format_json.format_base_json(results, options, False,
238                                         class_label='category')
239
240 @dispatch.format_func(RawDataList, 'json')
241 def _format_raw_data_json(results: RawDataList,  _: Mapping[str, Any]) -> str:
242     out = JsonWriter()
243     out.start_array()
244     for res in results:
245         out.start_object()
246         for k, v in res.items():
247             out.keyval(k, v)
248         out.end_object().next()
249
250     out.end_array()
251
252     return out()