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