]> git.openstreetmap.org Git - nominatim-ui.git/blob - src/pages/DetailsPage.svelte
Merge remote-tracking branch 'upstream/master'
[nominatim-ui.git] / src / pages / DetailsPage.svelte
1 <script>
2   import { untrack } from 'svelte';
3   import { update_html_title } from '../lib/api_utils.js';
4   import { appState } from '../state/AppState.svelte.js';
5
6   import {
7     coverageType, isAdminBoundary,
8     formatAddressRank, formatKeywordToken, formatOSMType
9   } from '../lib/helpers.js';
10   import Header from '../components/Header.svelte';
11   import MapIcon from '../components/MapIcon.svelte';
12   import SearchSectionDetails from '../components/SearchSectionDetails.svelte';
13   import DetailsTableHeader from '../components/DetailsTableHeader.svelte';
14   import DetailsOneRow from '../components/DetailsOneRow.svelte';
15   import DetailsLink from '../components/DetailsLink.svelte';
16   import DetailsPostcodeHint from '../components/DetailsPostcodeHint.svelte';
17   import InfoRowList from '../components/DetailsInfoRowList.svelte';
18   import WikipediaLink from '../components/WikipediaLink.svelte';
19   import OsmLink from '../components/OsmLink.svelte';
20   import Map from '../components/Map.svelte';
21
22   let aPlace = $state();
23   let base_url = $state();
24   let api_request_params = $state.raw();
25   let api_request_finished = $state(false);
26
27   function loaddata(search_params) {
28     api_request_params = {
29       place_id: search_params.get('place_id'),
30       osmtype: search_params.get('osmtype'),
31       osmid: search_params.get('osmid'),
32       class: search_params.get('class'),
33       keywords: search_params.get('keywords'),
34       addressdetails: 1,
35       entrances: 1,
36       hierarchy: (search_params.get('hierarchy') === '1' ? 1 : 0),
37       group_hierarchy: 1,
38       polygon_geojson: 1,
39       format: 'json'
40     };
41     api_request_finished = false;
42
43     if (api_request_params.place_id || (api_request_params.osmtype && api_request_params.osmid)) {
44
45       if (api_request_params.place_id) {
46         update_html_title('Details for ' + api_request_params.place_id);
47       } else {
48         update_html_title('Details for ' + api_request_params.osmtype + api_request_params.osmid);
49       }
50
51       appState.fetchFromApi('details', api_request_params, function (data) {
52         window.scrollTo(0, 0);
53         api_request_finished = true;
54         aPlace = (data && !data.error) ? data : undefined;
55       });
56     } else {
57       aPlace = undefined;
58     }
59   }
60
61   function place_has_keywords(aThisPlace) {
62     // Return false if Nominatim API sends 'keywords: { name: [], address: [] }'
63     // Like no longer needed after Nominatim version 4.3
64     return (
65       aThisPlace.keywords && aThisPlace.keywords.name && aThisPlace.keywords.address
66       && (aThisPlace.keywords.name.length > 0 || aThisPlace.keywords.address.length > 0)
67     );
68   }
69
70   function country_code(aThisPlace) {
71     let aLine = aThisPlace.address.find((address_line) => address_line.type === 'country_code');
72     return aLine ? aLine.localname : null;
73   }
74
75   $effect(() => {
76     if (appState.page.tab === 'details') {
77       const params = appState.page.params;
78       untrack(() => {
79         loaddata(params);
80         base_url = window.location.search;
81       });
82     }
83   });
84
85   const reverse_only = Nominatim_Config.Reverse_Only;
86 </script>
87
88 {#snippet subheader()}
89   <SearchSectionDetails api_request_params={api_request_params}/>
90 {/snippet}
91 <Header {subheader} />
92
93 <div class="container">
94   {#if aPlace}
95     <div class="row">
96       <div class="col-sm-10">
97         <h1>
98           {aPlace.localname || `${formatOSMType(aPlace.osm_type)} ${aPlace.osm_id}` }
99           <small><DetailsLink feature={aPlace} text="link to this page" /></small>
100         </h1>
101       </div>
102       <div class="col-sm-2 text-end">
103         <MapIcon aPlace={aPlace} />
104       </div>
105     </div>
106     <div class="row">
107       <div class="col-md-6">
108         <table id="locationdetails" class="table table-striped table-responsive">
109           <tbody>
110             <tr><td>Name</td><td>
111             {#if aPlace.names && typeof (aPlace.names) === 'object'
112               && Object.keys(aPlace.names).length}
113               <InfoRowList items={aPlace.names} />
114             {:else}
115               <span class="noname fw-bold">No Name</span>
116             {/if}
117             </td></tr>
118             <tr><td>Type</td><td>{aPlace.category}:{aPlace.type}</td></tr>
119             <tr><td>Last Updated</td><td>{aPlace.indexed_date}</td></tr>
120             {#if (isAdminBoundary(aPlace)) }
121               <tr><td>Admin Level</td><td>{aPlace.admin_level}</td></tr>
122             {/if}
123             <tr><td>Search Rank</td><td>{aPlace.rank_search}</td></tr>
124             <tr><td>Address Rank</td><td>
125               {aPlace.rank_address} ({formatAddressRank(aPlace.rank_address)})
126             </td></tr>
127             {#if aPlace.calculated_importance}
128               <tr><td>Importance</td><td>
129                   {aPlace.calculated_importance}
130                   {#if !aPlace.importance} (estimated){/if}
131               </td></tr>
132             {/if}
133             <tr><td>Coverage</td><td>{coverageType(aPlace)}</td></tr>
134             <tr><td>Centre Point (lat,lon)</td><td>
135                 {aPlace.centroid.coordinates[1]},{aPlace.centroid.coordinates[0]}
136             </td></tr>
137             <tr><td>OSM</td><td>
138               <OsmLink osmType={aPlace.osm_type} osmId={aPlace.osm_id}/>
139             </td></tr>
140             <tr><td>Place Id</td><td>
141                {aPlace.place_id}
142                (<a href="https://nominatim.org/release-docs/develop/api/Output/#place_id-is-not-a-persistent-id">
143                  on this server
144                </a>)
145             </td></tr>
146             {#if aPlace.calculated_wikipedia}
147               <tr><td>Wikipedia Calculated</td><td>
148               <WikipediaLink wikipedia={aPlace.calculated_wikipedia} />
149               </td></tr>
150             {/if}
151             <tr><td>Computed Postcode</td><td>
152               {#if aPlace.calculated_postcode || (aPlace.type === 'postcode' || !aPlace.osm_id)}
153                 {aPlace.calculated_postcode || aPlace.names.ref}
154                 <DetailsPostcodeHint postcode={aPlace.calculated_postcode || aPlace.names.ref}
155                                      lat={aPlace.centroid.coordinates[1]}
156                                      lon={aPlace.centroid.coordinates[0]}
157                                      country_code={aPlace.country_code} />
158               {/if}
159             </td></tr>
160             <tr><td>Address Tags</td><td>
161               <InfoRowList items={aPlace.addresstags} />
162             </td></tr>
163             <tr><td>Extra Tags</td><td>
164               <InfoRowList items={aPlace.extratags} />
165             </td></tr>
166           </tbody>
167         </table>
168       </div>
169       <div class="col-md-6">
170         <div id="map-wrapper">
171           <Map current_result={aPlace} />
172         </div>
173       </div>
174     </div>
175   {:else if (window.location.search !== '' && api_request_finished)}
176     No such place found.
177   {/if}
178
179   {#if aPlace}
180     <div class="row">
181       <div class="col-md-12">
182
183         <h2>Address</h2>
184         {#if aPlace.address}
185           <table id="address" class="table table-striped table-small">
186             <DetailsTableHeader />
187             <tbody>
188               {#each aPlace.address as addressLine}
189                 <DetailsOneRow addressLine={addressLine}
190                                bMarkUnusedLines={true}
191                                bDistanceInMeters={false}
192                                sCountryCode={country_code(aPlace)} />
193               {/each}
194             </tbody>
195           </table>
196         {/if}
197
198         <h2>Linked Places</h2>
199         {#if aPlace.linked_places}
200           <table class="table table-striped table-small">
201             <DetailsTableHeader />
202             <tbody>
203               {#each aPlace.linked_places as addressLine}
204                 <DetailsOneRow addressLine={addressLine}
205                                bMarkUnusedLines={true}
206                                bDistanceInMeters={true} />
207               {/each}
208             </tbody>
209           </table>
210         {/if}
211
212         <h2>Entrances</h2>
213         {#if aPlace.entrances && aPlace.entrances.length}
214           <table class="table table-striped table-small">
215             <thead>
216               <tr>
217                 <th></th>
218                 <th>Entrance Type</th>
219                 <th>OSM</th>
220                 <th>Extra Tags</th>
221               </tr>
222             </thead>
223             <tbody>
224               {#each aPlace.entrances as entrance, i}
225                 <tr class="all-columns">
226                   <td>{i + 1}</td>
227                   <td>{entrance.type}</td>
228                   <td><OsmLink osmType='N' osmId={entrance.osm_id} /></td>
229                   <td><InfoRowList items={entrance.extratags || {}} /></td>
230                 </tr>
231               {/each}
232             </tbody>
233           </table>
234         {:else}
235           <p>Place does not have entrances</p>
236         {/if}
237
238       {#if !reverse_only}
239         <h2>Keywords</h2>
240         {#if api_request_params.keywords}
241           {#if place_has_keywords(aPlace)}
242             <h3>Name Keywords</h3>
243
244             <table class="table table-striped table-small">
245               <tbody>
246                 {#each aPlace.keywords.name as keyword}
247                   <tr>
248                     <td>{formatKeywordToken(keyword.token)}</td>
249                     {#if keyword.id}
250                       <td>word id: {keyword.id}</td>
251                     {/if}
252                   </tr>
253                 {/each}
254               </tbody>
255             </table>
256             {#if aPlace.keywords.address}
257               <h3>Address Keywords</h3>
258
259               <table class="table table-striped table-small">
260                 <tbody>
261                   {#each aPlace.keywords.address as keyword}
262                     <tr>
263                       <td>{formatKeywordToken(keyword.token)}</td>
264                       <td>word id: {keyword.id || '?'}</td>
265                     </tr>
266                   {/each}
267                 </tbody>
268               </table>
269             {/if}
270           {:else}
271             <p>Place has no keywords</p>
272           {/if}
273         {:else}
274           <a class="btn btn-outline-secondary btn-sm"
275             href="{base_url}&keywords=1">display keywords</a>
276         {/if}
277       {/if}
278
279       <h2>Parent Of</h2>
280       {#if api_request_params.hierarchy}
281         {#if aPlace.hierarchy && typeof (aPlace.hierarchy) === 'object'
282           && Object.keys(aPlace.hierarchy).length}
283
284           {#each Object.keys(aPlace.hierarchy) as type}
285             <h3>{type}</h3>
286             <table class="table table-striped table-small">
287               <DetailsTableHeader />
288               <tbody>
289                 {#each aPlace.hierarchy[type] as line}
290                   <DetailsOneRow addressLine={line} bDistanceInMeters={true} />
291                 {/each}
292               </tbody>
293             </table>
294           {/each}
295           {#if Object.keys(aPlace.hierarchy) > 500}
296             <p>There are more child objects which are not shown.</p>
297           {/if}
298         {:else}
299           <p>Place is not parent of other places</p>
300         {/if}
301       {:else}
302         <a class="btn btn-outline-secondary btn-sm"
303             href="{base_url}&hierarchy=1">display child places</a>
304       {/if}
305     </div>
306   </div>
307   {/if}
308 </div>
309
310
311
312 <style>
313   h1 {
314     margin: 10px 0;
315     padding-left: 8px;
316   }
317
318   h1 small :global(a) {
319     font-size: 0.5em;
320     white-space: nowrap;
321   }
322
323   h2 {
324     font-size: 2em;
325     background-color: var(--bs-body-bg);
326     border-bottom: 2px solid silver;
327     margin-top: 2em;
328     margin-bottom: 0.5em;
329   }
330   h3 {
331     font-size: 1.5em;
332   }
333
334   table#locationdetails td {
335     padding: 2px 8px;
336     font-size: 0.9em;
337   }
338
339   tr.all-columns {
340     background-color: var(--bs-body-bg) !important;
341     border: none;
342   }
343   tr.all-columns td {
344     border-top: none !important;
345     padding-left: 0 !important;
346   }
347   :global(span.noname){
348     color: var(--bs-danger);
349   }
350
351   #map-wrapper {
352     position: relative;
353     width:100%;
354     min-height: auto;
355     height:300px;
356     border: 1px solid #666;
357   }
358 </style>