]> 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       if (!aFeature) map.fitBounds(bounds);
165     }
166
167     if (!aFeature) return;
168
169     let lat = aFeature.centroid ? aFeature.centroid.coordinates[1] : aFeature.lat;
170     let lon = aFeature.centroid ? aFeature.centroid.coordinates[0] : aFeature.lon;
171     let geojson = aFeature.geometry || aFeature.geojson;
172     let entrances = aFeature.entrances;
173
174     if (lat && lon) {
175       let circle = L.circleMarker([lat, lon], {
176         radius: 10, weight: 2, fillColor: '#ff7800', color: 'blue', opacity: 0.75
177       });
178       if (marker) { // reverse result
179         circle.bindTooltip('Result').openTooltip();
180       }
181       map.addLayer(circle);
182       dataLayers.push(circle);
183     }
184
185
186     if (geojson) {
187       var geojson_layer = L.geoJson(
188         // https://leafletjs.com/reference-1.7.1.html#path-option
189         parse_and_normalize_geojson_string(geojson),
190         {
191           style: function () {
192             return { interactive: false, color: 'blue' };
193           }
194         }
195       );
196       map.addLayer(geojson_layer);
197       dataLayers.push(geojson_layer);
198       map.fitBounds(geojson_layer.getBounds());
199     } else if (lat && lon && marker) {
200       map.fitBounds([[lat, lon], marker], { padding: [50, 50] });
201     } else if (lat && lon) {
202       map.setView([lat, lon], 10);
203     }
204
205     if (entrances) {
206       entrances.forEach((entrance, i) => {
207         let entranceCircle = L.circleMarker([entrance.lat, entrance.lon], {
208           radius: 5, weight: 2, fillColor: '#ff7800', color: 'red', opacity: 0.75
209         });
210         entranceCircle.bindTooltip(`Entrance ${i + 1} (type=${entrance.type})`).openTooltip();
211         map.addLayer(entranceCircle);
212         dataLayers.push(entranceCircle);
213       });
214     }
215   }
216
217   $effect(() => {
218     setMapData(position_marker, current_result);
219   });
220
221 </script>
222
223 <div id="map" use:mapAction></div>
224 <MapPosition />
225
226 <style>
227   #map {
228     height: 100%;
229     background:#eee;
230   }
231
232   @media (max-width: 768px) {
233     #map {
234       height: 300px;
235     }
236   }
237
238 </style>