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