]> git.openstreetmap.org Git - nominatim-ui.git/blob - dist/assets/js/nominatim-ui.js
ac6e97b18979df8ad9df6a19aeb27a4a8a157b06
[nominatim-ui.git] / dist / assets / js / nominatim-ui.js
1 'use strict';
2
3 var map;
4 var last_click_latlng;
5
6
7 // *********************************************************
8 // HELPERS
9 // *********************************************************
10
11 function get_config_value(str, default_val) {
12   return (typeof Nominatim_Config[str] !== 'undefined' ? Nominatim_Config[str] : default_val);
13 }
14
15 function parse_and_normalize_geojson_string(part) {
16   // normalize places the geometry into a featurecollection, similar to
17   // https://github.com/mapbox/geojson-normalize
18   var parsed_geojson = {
19     type: 'FeatureCollection',
20     features: [
21       {
22         type: 'Feature',
23         geometry: part,
24         properties: {}
25       }
26     ]
27   };
28   return parsed_geojson;
29 }
30
31 function map_link_to_osm() {
32   var zoom = map.getZoom();
33   var lat = map.getCenter().lat;
34   var lng = map.getCenter().lng;
35   return 'https://openstreetmap.org/#map=' + zoom + '/' + lat + '/' + lng;
36 }
37
38 function map_viewbox_as_string() {
39   var bounds = map.getBounds();
40   var west = bounds.getWest();
41   var east = bounds.getEast();
42
43   if ((east - west) >= 360) { // covers more than whole planet
44     west = map.getCenter().lng - 179.999;
45     east = map.getCenter().lng + 179.999;
46   }
47   east = L.latLng(77, east).wrap().lng;
48   west = L.latLng(77, west).wrap().lng;
49
50   return [
51     west.toFixed(5), // left
52     bounds.getNorth().toFixed(5), // top
53     east.toFixed(5), // right
54     bounds.getSouth().toFixed(5) // bottom
55   ].join(',');
56 }
57
58
59 // *********************************************************
60 // PAGE HELPERS
61 // *********************************************************
62
63 function fetch_from_api(endpoint_name, params, callback) {
64   //
65   // `&a=&b=&c=1` => '&c=1'
66   var param_names = Object.keys(params);
67   for (var i = 0; i < param_names.length; i += 1) {
68     var val = params[param_names[i]];
69     if (typeof (val) === 'undefined' || val === '' || val === null) {
70       delete params[param_names[i]];
71     }
72   }
73
74   var api_url = get_config_value('Nominatim_API_Endpoint') + endpoint_name + '.php?'
75                   + $.param(params);
76   if (endpoint_name !== 'status') {
77     $('#api-request-link').attr('href', api_url);
78   }
79   $.get(api_url, function (data) {
80     callback(data);
81   });
82 }
83
84 function update_data_date() {
85   fetch_from_api('status', { format: 'json' }, function (data) {
86     $('#data-date').text(data.data_updated);
87   });
88 }
89
90 function render_template(el, template_name, page_context) {
91   var template_source = $('#' + template_name).text();
92   var template = Handlebars.compile(template_source);
93   var html = template(page_context);
94   el.html(html);
95 }
96
97 function update_html_title(title) {
98   var prefix = '';
99   if (title && title.length > 1) {
100     prefix = title + ' | ';
101   }
102   $('head title').text(prefix + 'OpenStreetMap Nominatim');
103 }
104
105 function show_error(html) {
106   $('#error-overlay').html(html).show();
107 }
108
109 function hide_error() {
110   $('#error-overlay').empty().hide();
111 }
112
113
114 $(document).ajaxError(function (event, jqXHR, ajaxSettings/* , thrownError */) {
115   // console.log(thrownError);
116   // console.log(ajaxSettings);
117   var url = ajaxSettings.url;
118   show_error('Error fetching results from <a href="' + url + '">' + url + '</a>');
119 });
120
121
122 jQuery(document).ready(function () {
123   hide_error();
124 });
125 // *********************************************************
126 // DETAILS PAGE
127 // *********************************************************
128
129
130 function init_map_on_detail_page(lat, lon, geojson) {
131   var attribution = get_config_value('Map_Tile_Attribution') || null;
132   map = new L.map('map', {
133     // center: [nominatim_map_init.lat, nominatim_map_init.lon],
134     // zoom:   nominatim_map_init.zoom,
135     attributionControl: (attribution && attribution.length),
136     scrollWheelZoom: true, // !L.Browser.touch,
137     touchZoom: false
138   });
139
140   L.tileLayer(get_config_value('Map_Tile_URL'), {
141     // moved to footer
142     // '&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
143     attribution: attribution
144   }).addTo(map);
145
146   // var layerGroup = new L.layerGroup().addTo(map);
147
148   var circle = L.circleMarker([lat, lon], {
149     radius: 10, weight: 2, fillColor: '#ff7800', color: 'blue', opacity: 0.75
150   });
151   map.addLayer(circle);
152
153   if (geojson) {
154     var geojson_layer = L.geoJson(
155       // https://leafletjs.com/reference-1.0.3.html#path-option
156       parse_and_normalize_geojson_string(geojson),
157       {
158         style: function () {
159           return { interactive: false, color: 'blue' };
160         }
161       }
162     );
163     map.addLayer(geojson_layer);
164     map.fitBounds(geojson_layer.getBounds());
165   } else {
166     map.setView([lat, lon], 10);
167   }
168
169   var osm2 = new L.TileLayer(
170     get_config_value('Map_Tile_URL'),
171     {
172       minZoom: 0,
173       maxZoom: 13,
174       attribution: (get_config_value('Map_Tile_Attribution') || null)
175     }
176   );
177   (new L.Control.MiniMap(osm2, { toggleDisplay: true })).addTo(map);
178 }
179
180
181 jQuery(document).ready(function () {
182   if (!$('#details-page').length) { return; }
183
184   var search_params = new URLSearchParams(window.location.search);
185   // var place_id = search_params.get('place_id');
186
187   var api_request_params = {
188     place_id: search_params.get('place_id'),
189     osmtype: search_params.get('osmtype'),
190     osmid: search_params.get('osmid'),
191     keywords: search_params.get('keywords'),
192     addressdetails: 1,
193     hierarchy: (search_params.get('hierarchy') === '1' ? 1 : 0),
194     group_hierarchy: 1,
195     polygon_geojson: 1,
196     format: 'json'
197   };
198
199   if (api_request_params.place_id || (api_request_params.osmtype && api_request_params.osmid)) {
200     fetch_from_api('details', api_request_params, function (aFeature) {
201       var context = { aPlace: aFeature, base_url: window.location.search };
202
203       render_template($('main'), 'detailspage-template', context);
204       if (api_request_params.place_id) {
205         update_html_title('Details for ' + api_request_params.place_id);
206       } else {
207         update_html_title('Details for ' + api_request_params.osmtype + api_request_params.osmid);
208       }
209
210       update_data_date();
211
212       var lat = aFeature.centroid.coordinates[1];
213       var lon = aFeature.centroid.coordinates[0];
214       init_map_on_detail_page(lat, lon, aFeature.geometry);
215     });
216   } else {
217     render_template($('main'), 'detailspage-index-template');
218   }
219
220   $('#form-by-type-and-id,#form-by-osm-url').on('submit', function (e) {
221     e.preventDefault();
222
223     var val = $(this).find('input[type=edit]').val();
224     var matches = val.match(/^\s*([NWR])(\d+)\s*$/i);
225
226     if (!matches) {
227       matches = val.match(/\/(relation|way|node)\/(\d+)\s*$/);
228     }
229
230     if (matches) {
231       $(this).find('input[name=osmtype]').val(matches[1].charAt(0).toUpperCase());
232       $(this).find('input[name=osmid]').val(matches[2]);
233       $(this).get(0).submit();
234     } else {
235       alert('invalid input');
236     }
237   });
238 });
239
240 // *********************************************************
241 // FORWARD/REVERSE SEARCH PAGE
242 // *********************************************************
243
244
245 function display_map_position(mouse_lat_lng) {
246   //
247   if (mouse_lat_lng) {
248     mouse_lat_lng = map.wrapLatLng(mouse_lat_lng);
249   }
250
251   var html_mouse = 'mouse position: -';
252   if (mouse_lat_lng) {
253     html_mouse = 'mouse position: '
254                   + [mouse_lat_lng.lat.toFixed(5), mouse_lat_lng.lng.toFixed(5)].join(',');
255   }
256   var html_click = 'last click: -';
257   if (last_click_latlng) {
258     html_click = 'last click: '
259                   + [last_click_latlng.lat.toFixed(5), last_click_latlng.lng.toFixed(5)].join(',');
260   }
261
262   var html_center = 'map center: '
263     + map.getCenter().lat.toFixed(5) + ',' + map.getCenter().lng.toFixed(5)
264     + ' <a target="_blank" href="' + map_link_to_osm() + '">view on osm.org</a>';
265
266   var html_zoom = 'map zoom: ' + map.getZoom();
267   var html_viewbox = 'viewbox: ' + map_viewbox_as_string();
268
269   $('#map-position-inner').html([
270     html_center,
271     html_zoom,
272     html_viewbox,
273     html_click,
274     html_mouse
275   ].join('<br/>'));
276
277   var center_lat_lng = map.wrapLatLng(map.getCenter());
278   var reverse_params = {
279     lat: center_lat_lng.lat.toFixed(5),
280     lon: center_lat_lng.lng.toFixed(5)
281     // zoom: 2,
282     // format: 'html'
283   };
284   $('#switch-to-reverse').attr('href', 'reverse.html?' + $.param(reverse_params));
285
286   $('input#use_viewbox').trigger('change');
287 }
288
289 function init_map_on_search_page(is_reverse_search, nominatim_results, request_lat,
290   request_lon, init_zoom) {
291
292   var attribution = get_config_value('Map_Tile_Attribution') || null;
293   map = new L.map('map', {
294     // center: [nominatim_map_init.lat, nominatim_map_init.lon],
295     // zoom:   nominatim_map_init.zoom,
296     attributionControl: (attribution && attribution.length),
297     scrollWheelZoom: true, // !L.Browser.touch,
298     touchZoom: false
299   });
300
301
302   L.tileLayer(get_config_value('Map_Tile_URL'), {
303     // moved to footer
304     // '&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
305     attribution: attribution
306   }).addTo(map);
307
308   // console.log(Nominatim_Config);
309
310   map.setView([request_lat, request_lon], init_zoom);
311
312   var osm2 = new L.TileLayer(get_config_value('Map_Tile_URL'), {
313     minZoom: 0,
314     maxZoom: 13,
315     attribution: attribution
316   });
317   new L.Control.MiniMap(osm2, { toggleDisplay: true }).addTo(map);
318
319   if (is_reverse_search) {
320     // We don't need a marker, but an L.circle instance changes radius once you zoom in/out
321     var cm = L.circleMarker(
322       [request_lat, request_lon],
323       {
324         radius: 5,
325         weight: 2,
326         fillColor: '#ff7800',
327         color: 'red',
328         opacity: 0.75,
329         zIndexOffset: 100,
330         clickable: false
331       }
332     );
333     cm.addTo(map);
334   } else {
335     var search_params = new URLSearchParams(window.location.search);
336     var viewbox = search_params.get('viewbox');
337     if (viewbox) {
338       var coords = viewbox.split(','); // <x1>,<y1>,<x2>,<y2>
339       var bounds = L.latLngBounds([coords[1], coords[0]], [coords[3], coords[2]]);
340       L.rectangle(bounds, {
341         color: '#69d53e',
342         weight: 3,
343         dashArray: '5 5',
344         opacity: 0.8,
345         fill: false
346       }).addTo(map);
347     }
348   }
349
350   var MapPositionControl = L.Control.extend({
351     options: {
352       position: 'topright'
353     },
354     onAdd: function (/* map */) {
355       var container = L.DomUtil.create('div', 'my-custom-control');
356
357       $(container).text('show map bounds')
358         .addClass('leaflet-bar btn btn-sm btn-outline-secondary')
359         .on('click', function (e) {
360           e.preventDefault();
361           e.stopPropagation();
362           $('#map-position').show();
363           $(container).hide();
364         });
365       $('#map-position-close a').on('click', function (e) {
366         e.preventDefault();
367         e.stopPropagation();
368         $('#map-position').hide();
369         $(container).show();
370       });
371
372       return container;
373     }
374   });
375
376   map.addControl(new MapPositionControl());
377
378
379
380
381
382   function update_viewbox_field() {
383     // hidden HTML field
384     $('input[name=viewbox]')
385       .val($('input#use_viewbox')
386         .prop('checked') ? map_viewbox_as_string() : '');
387   }
388
389   map.on('move', function () {
390     display_map_position();
391     update_viewbox_field();
392   });
393
394   map.on('mousemove', function (e) {
395     display_map_position(e.latlng);
396   });
397
398   map.on('click', function (e) {
399     last_click_latlng = e.latlng;
400     display_map_position();
401   });
402
403   map.on('load', function () {
404     display_map_position();
405   });
406
407   $('input#use_viewbox').on('change', function () {
408     update_viewbox_field();
409   });
410
411   $("input[name='query-selector']").click(function () {
412     var query_val = $("input[name='query-selector']:checked").val();
413     if (query_val === 'simple') {
414       $('div.form-group-simple').removeClass('hidden');
415       $('div.form-group-structured').addClass('hidden');
416       $('.form-group-structured').find('input:text').val('');
417     } else if (query_val === 'structured') {
418       console.log('here');
419       $('div.form-group-simple').addClass('hidden');
420       $('div.form-group-structured').removeClass('hidden');
421       $('.form-group-simple').find('input:text').val('');
422     }
423   });
424
425   function get_result_element(position) {
426     return $('.result').eq(position);
427   }
428   // function marker_for_result(result) {
429   //   return L.marker([result.lat, result.lon], { riseOnHover: true, title: result.name });
430   // }
431   function circle_for_result(result) {
432     var cm_style = {
433       radius: 10,
434       weight: 2,
435       fillColor: '#ff7800',
436       color: 'blue',
437       opacity: 0.75,
438       clickable: !is_reverse_search
439     };
440     return L.circleMarker([result.lat, result.lon], cm_style);
441   }
442
443   var layerGroup = (new L.layerGroup()).addTo(map);
444
445   function highlight_result(position, bool_focus) {
446     var result = nominatim_results[position];
447     if (!result) { return; }
448     var result_el = get_result_element(position);
449
450     $('.result').removeClass('highlight');
451     result_el.addClass('highlight');
452
453     layerGroup.clearLayers();
454
455     if (result.lat) {
456       var circle = circle_for_result(result);
457       circle.on('click', function () {
458         highlight_result(position);
459       });
460       layerGroup.addLayer(circle);
461     }
462
463     if (result.boundingbox) {
464       var bbox = [
465         [result.boundingbox[0] * 1, result.boundingbox[2] * 1],
466         [result.boundingbox[1] * 1, result.boundingbox[3] * 1]
467       ];
468       map.fitBounds(bbox);
469
470       if (result.geojson && result.geojson.type.match(/(Polygon)|(Line)/)) {
471         //
472         var geojson_layer = L.geoJson(
473           parse_and_normalize_geojson_string(result.geojson),
474           {
475             // https://leafletjs.com/reference-1.0.3.html#path-option
476             style: function (/* feature */) {
477               return { interactive: false, color: 'blue' };
478             }
479           }
480         );
481         layerGroup.addLayer(geojson_layer);
482       }
483       // else {
484       //     var layer = L.rectangle(bounds, {color: "#ff7800", weight: 1} );
485       //     layerGroup.addLayer(layer);
486       // }
487     } else {
488       var result_coord = L.latLng(result.lat, result.lon);
489       if (result_coord) {
490         if (is_reverse_search) {
491           // console.dir([result_coord, [request_lat, request_lon]]);
492           // make sure the search coordinates are in the map view as well
493           map.fitBounds(
494             [result_coord, [request_lat, request_lon]],
495             {
496               padding: [50, 50],
497               maxZoom: map.getZoom()
498             }
499           );
500         } else {
501           map.panTo(result_coord, result.zoom || get_config_value('Map_Default_Zoom'));
502         }
503       }
504     }
505     if (bool_focus) {
506       $('#map').focus();
507     }
508   }
509
510
511   $('.result').on('click', function () {
512     highlight_result($(this).data('position'), true);
513   });
514
515   if (is_reverse_search) {
516     map.on('click', function (e) {
517       $('form input[name=lat]').val(e.latlng.lat);
518       $('form input[name=lon]').val(e.latlng.wrap().lng);
519       $('form').submit();
520     });
521
522     $('#switch-coords').on('click', function (e) {
523       e.preventDefault();
524       e.stopPropagation();
525       var lat = $('form input[name=lat]').val();
526       var lon = $('form input[name=lon]').val();
527       $('form input[name=lat]').val(lon);
528       $('form input[name=lon]').val(lat);
529       $('form').submit();
530     });
531   }
532
533   highlight_result(0, false);
534
535   // common mistake is to copy&paste latitude and longitude into the 'lat' search box
536   $('form input[name=lat]').on('change', function () {
537     var coords_split = $(this).val().split(',');
538     if (coords_split.length === 2) {
539       $(this).val(L.Util.trim(coords_split[0]));
540       $(this).siblings('input[name=lon]').val(L.Util.trim(coords_split[1]));
541     }
542   });
543 }
544
545
546
547
548 jQuery(document).ready(function () {
549   //
550   if (!$('#search-page,#reverse-page').length) { return; }
551
552   var is_reverse_search = !!($('#reverse-page').length);
553
554   var search_params = new URLSearchParams(window.location.search);
555
556   // return view('search', [
557   //     'sQuery' => $sQuery,
558   //     'bAsText' => '',
559   //     'sViewBox' => '',
560   //     'aSearchResults' => $aSearchResults,
561   //     'sMoreURL' => 'example.com',
562   //     'sDataDate' => $this->fetch_status_date(),
563   //     'sApiURL' => $url
564   // ]);
565
566   var api_request_params;
567   var context;
568
569   if (is_reverse_search) {
570     api_request_params = {
571       lat: search_params.get('lat'),
572       lon: search_params.get('lon'),
573       zoom: (search_params.get('zoom') > 1
574         ? search_params.get('zoom')
575         : get_config_value('Reverse_Default_Search_Zoom')),
576       format: 'jsonv2'
577     };
578
579     context = {
580       // aPlace: aPlace,
581       fLat: api_request_params.lat,
582       fLon: api_request_params.lon,
583       iZoom: (search_params.get('zoom') > 1
584         ? api_request_params.zoom
585         : get_config_value('Reverse_Default_Search_Zoom'))
586     };
587
588     update_html_title();
589     if (api_request_params.lat && api_request_params.lon) {
590
591       fetch_from_api('reverse', api_request_params, function (aPlace) {
592
593         if (aPlace.error) {
594           aPlace = null;
595         }
596
597         context.aPlace = aPlace;
598
599         render_template($('main'), 'reversepage-template', context);
600         update_html_title('Reverse result for '
601                             + api_request_params.lat
602                             + ','
603                             + api_request_params.lon);
604
605         init_map_on_search_page(
606           is_reverse_search,
607           [aPlace],
608           api_request_params.lat,
609           api_request_params.lon,
610           api_request_params.zoom
611         );
612
613         update_data_date();
614       });
615     } else {
616       render_template($('main'), 'reversepage-template', context);
617
618       init_map_on_search_page(
619         is_reverse_search,
620         [],
621         get_config_value('Map_Default_Lat'),
622         get_config_value('Map_Default_Lon'),
623         get_config_value('Map_Default_Zoom')
624       );
625     }
626
627   } else {
628     api_request_params = {
629       q: search_params.get('q'),
630       street: search_params.get('street'),
631       city: search_params.get('city'),
632       county: search_params.get('county'),
633       state: search_params.get('state'),
634       country: search_params.get('country'),
635       postalcode: search_params.get('postalcode'),
636       polygon_geojson: search_params.get('polygon_geojson') ? 1 : 0,
637       viewbox: search_params.get('viewbox'),
638       exclude_place_ids: search_params.get('exclude_place_ids'),
639       format: 'jsonv2'
640     };
641
642     context = {
643       sQuery: api_request_params.q,
644       sViewBox: search_params.get('viewbox'),
645       env: Nominatim_Config
646     };
647
648     if (api_request_params.street || api_request_params.city || api_request_params.county
649       || api_request_params.state || api_request_params.country || api_request_params.postalcode) {
650       context.hStructured = {
651         street: api_request_params.street,
652         city: api_request_params.city,
653         county: api_request_params.county,
654         state: api_request_params.state,
655         country: api_request_params.country,
656         postalcode: api_request_params.postalcode
657       };
658     }
659
660     if (api_request_params.q || context.hStructured) {
661
662       fetch_from_api('search', api_request_params, function (aResults) {
663
664         context.aSearchResults = aResults;
665
666         if (aResults.length >= 10) {
667           var aExcludePlaceIds = [];
668           if (search_params.has('exclude_place_ids')) {
669             aExcludePlaceIds.search_params.get('exclude_place_ids').split(',');
670           }
671           for (var i = 0; i < aResults.length; i += 1) {
672             aExcludePlaceIds.push(aResults[i].place_id);
673           }
674
675           var parsed_url = new URLSearchParams(window.location.search);
676           parsed_url.set('exclude_place_ids', aExcludePlaceIds.join(','));
677           context.sMoreURL = '?' + parsed_url.toString();
678         }
679
680         render_template($('main'), 'searchpage-template', context);
681         update_html_title('Result for ' + api_request_params.q);
682
683         init_map_on_search_page(
684           is_reverse_search,
685           aResults,
686           get_config_value('Map_Default_Lat'),
687           get_config_value('Map_Default_Lon'),
688           get_config_value('Map_Default_Zoom')
689         );
690
691         $('#q').focus();
692
693         update_data_date();
694       });
695     } else {
696       render_template($('main'), 'searchpage-template', context);
697
698       init_map_on_search_page(
699         is_reverse_search,
700         [],
701         get_config_value('Map_Default_Lat'),
702         get_config_value('Map_Default_Lon'),
703         get_config_value('Map_Default_Zoom')
704       );
705     }
706   }
707 });
708 // *********************************************************
709 // DELETABLE PAGE
710 // *********************************************************
711
712 jQuery(document).ready(function () {
713   if (!$('#deletable-page').length) { return; }
714
715   var api_request_params = {
716     format: 'json'
717   };
718
719   fetch_from_api('deletable', api_request_params, function (aPolygons) {
720     var context = { aPolygons: aPolygons };
721
722     render_template($('main'), 'deletable-template', context);
723     update_html_title('Deletable objects');
724
725     update_data_date();
726   });
727 });
728 // *********************************************************
729 // BROKEN POLYGON PAGE
730 // *********************************************************
731
732 jQuery(document).ready(function () {
733   if (!$('#polygons-page').length) { return; }
734
735   var api_request_params = {
736     format: 'json'
737   };
738
739   fetch_from_api('polygons', api_request_params, function (aPolygons) {
740     var context = { aPolygons: aPolygons };
741
742     render_template($('main'), 'polygons-template', context);
743     update_html_title('Broken polygons');
744
745     update_data_date();
746   });
747 });