]> git.openstreetmap.org Git - nominatim-ui.git/blob - src/components/Map.svelte
Merge remote-tracking branch 'upstream/master'
[nominatim-ui.git] / src / components / Map.svelte
1
2 <script>
3   import * as L from 'leaflet';
4   import 'leaflet-minimap';
5   import 'leaflet/dist/leaflet.css';
6   import 'leaflet-minimap/dist/Control.MiniMap.min.css';
7
8   import { mapState } from '../state/MapState.svelte.js';
9   import MapPosition from '../components/MapPosition.svelte';
10
11   let {
12     display_minimap = false,
13     current_result = null,
14     position_marker = null
15   } = $props();
16
17   let map;
18   let dataLayers = [];
19
20   function mapViewboxAsString(map) {
21     var bounds = map.getBounds();
22     var west = bounds.getWest();
23     var east = bounds.getEast();
24
25     if ((east - west) >= 360) { // covers more than whole planet
26       west = map.getCenter().lng - 179.999;
27       east = map.getCenter().lng + 179.999;
28     }
29     east = L.latLng(77, east).wrap().lng;
30     west = L.latLng(77, west).wrap().lng;
31
32     return [
33       west.toFixed(5), // left
34       bounds.getNorth().toFixed(5), // top
35       east.toFixed(5), // right
36       bounds.getSouth().toFixed(5) // bottom
37     ].join(',');
38   }
39
40   function setMapState() {
41     mapState.viewboxStr = mapViewboxAsString(map);
42     mapState.center = map.getCenter();
43     mapState.zoom = map.getZoom();
44   }
45
46   function createMap(container) {
47     const attribution = Nominatim_Config.Map_Tile_Attribution;
48
49     map = new L.map(container, {
50       attributionControl: false,
51       scrollWheelZoom: true, // !L.Browser.touch,
52       touchZoom: false,
53       center: L.latLng(Nominatim_Config.Map_Default_Lat,
54                        Nominatim_Config.Map_Default_Lon),
55       zoom: Nominatim_Config.Map_Default_Zoom
56     });
57     if (typeof Nominatim_Config.Map_Default_Bounds !== 'undefined'
58       && Nominatim_Config.Map_Default_Bounds) {
59       map.fitBounds(Nominatim_Config.Map_Default_Bounds);
60     }
61
62     if (attribution && attribution.length) {
63       L.control.attribution({ prefix: '<a href="https://leafletjs.com/">Leaflet</a>' }).addTo(map);
64     }
65
66     setMapState();
67
68     L.control.scale().addTo(map);
69
70     L.tileLayer(Nominatim_Config.Map_Tile_URL, {
71       attribution: attribution
72     }).addTo(map);
73
74     if (display_minimap) {
75       let osm2 = new L.TileLayer(Nominatim_Config.Map_Tile_URL, {
76         minZoom: 0,
77         maxZoom: 13,
78         attribution: attribution
79       });
80       new L.Control.MiniMap(osm2, { toggleDisplay: true }).addTo(map);
81     }
82
83     map.on('move', setMapState);
84     map.on('mousemove', (e) => { mapState.mousePos = e.latlng; });
85     map.on('click', (e) => { mapState.lastClick = e.latlng; });
86   }
87
88   function mapAction(container) {
89     createMap(container);
90     setMapData(position_marker, current_result);
91
92     return {
93       destroy: () => {
94         mapState.reset();
95         map.remove();
96       }
97     };
98   }
99
100   function parse_and_normalize_geojson_string(part) {
101     // normalize places the geometry into a featurecollection, similar to
102     // https://github.com/mapbox/geojson-normalize
103     var parsed_geojson = {
104       type: 'FeatureCollection',
105       features: [
106         {
107           type: 'Feature',
108           geometry: part,
109           properties: {}
110         }
111       ]
112     };
113     return parsed_geojson;
114   }
115
116   function resetMapData() {
117     if (!map) { return; }
118
119     dataLayers.forEach(function (layer) {
120       map.removeLayer(layer);
121     });
122   }
123
124   function setMapData(marker, aFeature) {
125     if (!map) { return; }
126
127     resetMapData();
128
129     if (marker) {
130       // We don't need a marker, but L.circle would change radius when you zoom in/out
131       let cm = L.circleMarker(
132         marker,
133         {
134           radius: 5,
135           weight: 2,
136           fillColor: '#ff7800',
137           color: 'red',
138           opacity: 0.75,
139           zIndexOffset: 100,
140           clickable: false
141         }
142       );
143       cm.bindTooltip(`Search (${marker[0]},${marker[1]})`).openTooltip();
144       cm.addTo(map);
145       dataLayers.push(cm);
146     }
147
148     var search_params = new URLSearchParams(window.location.search);
149     var viewbox = search_params.get('viewbox');
150     if (viewbox) {
151       let coords = viewbox.split(','); // <x1>,<y1>,<x2>,<y2>
152       let bounds = L.latLngBounds([coords[1], coords[0]], [coords[3], coords[2]]);
153       let viewbox_on_map = L.rectangle(bounds, {
154         color: '#69d53e',
155         weight: 3,
156         dashArray: '5 5',
157         opacity: 0.8,
158         fill: false,
159         interactive: false
160       });
161       map.addLayer(viewbox_on_map);
162       dataLayers.push(viewbox_on_map);
163     }
164
165     if (!aFeature) return;
166
167     let lat = aFeature.centroid ? aFeature.centroid.coordinates[1] : aFeature.lat;
168     let lon = aFeature.centroid ? aFeature.centroid.coordinates[0] : aFeature.lon;
169     let geojson = aFeature.geometry || aFeature.geojson;
170
171     if (lat && lon) {
172       let circle = L.circleMarker([lat, lon], {
173         radius: 10, weight: 2, fillColor: '#ff7800', color: 'blue', opacity: 0.75
174       });
175       if (marker) { // reverse result
176         circle.bindTooltip('Result').openTooltip();
177       }
178       map.addLayer(circle);
179       dataLayers.push(circle);
180     }
181
182
183     if (geojson) {
184       var geojson_layer = L.geoJson(
185         // https://leafletjs.com/reference-1.7.1.html#path-option
186         parse_and_normalize_geojson_string(geojson),
187         {
188           style: function () {
189             return { interactive: false, color: 'blue' };
190           }
191         }
192       );
193       map.addLayer(geojson_layer);
194       dataLayers.push(geojson_layer);
195       map.fitBounds(geojson_layer.getBounds());
196     } else if (lat && lon && marker) {
197       map.fitBounds([[lat, lon], marker], { padding: [50, 50] });
198     } else if (lat && lon) {
199       map.setView([lat, lon], 10);
200     }
201   }
202
203   $effect(() => {
204     setMapData(position_marker, current_result);
205   });
206
207 </script>
208
209 <div id="map" use:mapAction></div>
210 <MapPosition />
211
212 <style>
213   #map {
214     height: 100%;
215     background:#eee;
216   }
217
218   @media (max-width: 768px) {
219     #map {
220       height: 300px;
221     }
222   }
223
224 </style>