From df88eccbe93870f1c214e31ee43638cd0a1fbf53 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Sat, 23 Aug 2025 01:14:53 +0200 Subject: [PATCH] Replace map store with global map state (#291) * move button for minimized MapPosition into component This means the button can no longer be a leaflet control. But we can use svelte reactivity for switching between modes. * convert MapPosition info fields to properties Instead of registering events on the map itself, have the parent map hand in all the info we want to display. * make map state globally available Removes the existing map_store and exports a globally available set of map states instead. Components can now simply listen to the map state using Svelte reactivity instead of registering their own event handlers to the map. * use Svelte runes for main app --- src/App.svelte | 11 +- src/components/Header.svelte | 18 +-- src/components/Map.svelte | 90 +++++++-------- src/components/MapPosition.svelte | 121 +++++++-------------- src/components/SearchSection.svelte | 69 +----------- src/components/SearchSectionReverse.svelte | 37 +++---- src/lib/stores.js | 10 +- src/pages/ReversePage.svelte | 4 +- src/state/MapState.svelte.js | 12 ++ test/search.js | 8 +- 10 files changed, 141 insertions(+), 239 deletions(-) create mode 100644 src/state/MapState.svelte.js diff --git a/src/App.svelte b/src/App.svelte index 98ba029..fa9ce32 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,6 +2,7 @@ import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/js/bootstrap.bundle.js'; + import { onMount } from 'svelte'; import { page, refresh_page } from './lib/stores.js'; import Footer from './components/Footer.svelte'; @@ -13,7 +14,15 @@ import StatusPage from './pages/StatusPage.svelte'; import AboutPage from './pages/AboutPage.svelte'; - $: view = $page.tab; + let view = $state(); + + onMount(() => { + page.subscribe((pageinfo) => { + if (pageinfo.tab !== view) { + view = pageinfo.tab; + } + }) + }); refresh_page(); diff --git a/src/components/Header.svelte b/src/components/Header.svelte index d77c6b4..a6eb154 100644 --- a/src/components/Header.svelte +++ b/src/components/Header.svelte @@ -4,7 +4,8 @@ import LastUpdated from './LastUpdated.svelte'; import Error from './Error.svelte'; import { onMount } from 'svelte'; - import { map_store, page } from '../lib/stores.js'; + import { page } from '../lib/stores.js'; + import { mapState } from '../state/MapState.svelte.js'; import { initColorToggler } from '../color-mode-toggler.js'; let { subheader } = $props(); @@ -13,17 +14,6 @@ const reverse_only = Nominatim_Config.Reverse_Only; let view = $state(); - let map_lat = $state(); - let map_lon = $state(); - - map_store.subscribe(map => { - if (!map) return; - - map.on('move', function () { - map_lat = map.getCenter().lat.toFixed(5); - map_lon = map.getCenter().lng.toFixed(5); - }); - }); page.subscribe(pg => { view = pg.tab; }); @@ -104,8 +94,8 @@ {/if} diff --git a/src/components/Map.svelte b/src/components/Map.svelte index 70ffd3a..e259186 100644 --- a/src/components/Map.svelte +++ b/src/components/Map.svelte @@ -5,8 +5,7 @@ import 'leaflet/dist/leaflet.css'; import 'leaflet-minimap/dist/Control.MiniMap.min.css'; - import { get } from 'svelte/store'; - import { map_store } from '../lib/stores.js'; + import { mapState } from '../state/MapState.svelte.js'; import MapPosition from '../components/MapPosition.svelte'; let { @@ -15,20 +14,38 @@ position_marker = null } = $props(); + let map; let dataLayers = []; + function mapViewboxAsString(map) { + var bounds = map.getBounds(); + var west = bounds.getWest(); + var east = bounds.getEast(); + + if ((east - west) >= 360) { // covers more than whole planet + west = map.getCenter().lng - 179.999; + east = map.getCenter().lng + 179.999; + } + east = L.latLng(77, east).wrap().lng; + west = L.latLng(77, west).wrap().lng; + + return [ + west.toFixed(5), // left + bounds.getNorth().toFixed(5), // top + east.toFixed(5), // right + bounds.getSouth().toFixed(5) // bottom + ].join(','); + } + function createMap(container) { const attribution = Nominatim_Config.Map_Tile_Attribution; - let map = new L.map(container, { + map = new L.map(container, { attributionControl: false, scrollWheelZoom: true, // !L.Browser.touch, touchZoom: false, - center: [ - Nominatim_Config.Map_Default_Lat, - Nominatim_Config.Map_Default_Lon - ], - zoom: Nominatim_Config.Map_Default_Zoom + center: mapState.center, + zoom: mapState.zoom }); if (typeof Nominatim_Config.Map_Default_Bounds !== 'undefined' && Nominatim_Config.Map_Default_Bounds) { @@ -39,6 +56,8 @@ L.control.attribution({ prefix: 'Leaflet' }).addTo(map); } + mapState.viewboxStr = mapViewboxAsString(map); + L.control.scale().addTo(map); L.tileLayer(Nominatim_Config.Map_Tile_URL, { @@ -54,23 +73,22 @@ new L.Control.MiniMap(osm2, { toggleDisplay: true }).addTo(map); } - const MapPositionControl = L.Control.extend({ - options: { position: 'topright' }, - onAdd: () => { return document.getElementById('show-map-position'); } + map.on('move', () => { + mapState.center = map.getCenter(); + mapState.zoom = map.getZoom(); + mapState.viewboxStr = mapViewboxAsString(map); }); - map.addControl(new MapPositionControl()); - return map; + map.on('mousemove', (e) => { mapState.mousePos = e.latlng; }); + map.on('click', (e) => { mapState.lastClick = e.latlng; }); } function mapAction(container) { - let map = createMap(container); - map_store.set(map); - setMapData(current_result); + createMap(container); + setMapData(position_marker, current_result); return { destroy: () => { - map_store.set(null); map.remove(); } }; @@ -93,7 +111,6 @@ } function resetMapData() { - let map = get(map_store); if (!map) { return; } dataLayers.forEach(function (layer) { @@ -101,16 +118,15 @@ }); } - function setMapData(aFeature) { - let map = get(map_store); + function setMapData(marker, aFeature) { if (!map) { return; } resetMapData(); - if (position_marker) { + if (marker) { // We don't need a marker, but L.circle would change radius when you zoom in/out let cm = L.circleMarker( - position_marker, + marker, { radius: 5, weight: 2, @@ -121,7 +137,7 @@ clickable: false } ); - cm.bindTooltip(`Search (${position_marker[0]},${position_marker[1]})`).openTooltip(); + cm.bindTooltip(`Search (${marker[0]},${marker[1]})`).openTooltip(); cm.addTo(map); dataLayers.push(cm); } @@ -153,7 +169,7 @@ let circle = L.circleMarker([lat, lon], { radius: 10, weight: 2, fillColor: '#ff7800', color: 'blue', opacity: 0.75 }); - if (position_marker) { // reverse result + if (marker) { // reverse result circle.bindTooltip('Result').openTooltip(); } map.addLayer(circle); @@ -174,27 +190,21 @@ map.addLayer(geojson_layer); dataLayers.push(geojson_layer); map.fitBounds(geojson_layer.getBounds()); - } else if (lat && lon && position_marker) { - map.fitBounds([[lat, lon], position_marker], { padding: [50, 50] }); + } else if (lat && lon && marker) { + map.fitBounds([[lat, lon], marker], { padding: [50, 50] }); } else if (lat && lon) { map.setView([lat, lon], 10); } } - $effect(() => { setMapData(current_result); }); + $effect(() => { + setMapData(position_marker, current_result); + }); - function show_map_position_click(e) { - e.stopPropagation(); - e.target.style.display = 'none'; - document.getElementById('map-position').style.display = 'block'; - } -
- + diff --git a/src/components/SearchSection.svelte b/src/components/SearchSection.svelte index 209f2c5..3cf2a8e 100644 --- a/src/components/SearchSection.svelte +++ b/src/components/SearchSection.svelte @@ -1,70 +1,10 @@ {#snippet content()} diff --git a/src/lib/stores.js b/src/lib/stores.js index ae6426b..80f001d 100644 --- a/src/lib/stores.js +++ b/src/lib/stores.js @@ -1,7 +1,7 @@ import { writable } from 'svelte/store'; +import { untrack } from 'svelte'; import { identifyLinkInQuery } from './helpers.js'; -export const map_store = writable(); export const results_store = writable(); export const last_api_request_url_store = writable(); export const error_store = writable(); @@ -66,7 +66,9 @@ export function refresh_page(pagename, params) { } } - page.set({ tab: pagename, params: params }); - last_api_request_url_store.set(null); - error_store.set(null); + untrack(() => { + page.set({ tab: pagename, params: params }); + last_api_request_url_store.set(null); + error_store.set(null); + }); } diff --git a/src/pages/ReversePage.svelte b/src/pages/ReversePage.svelte index dcbf5b3..667ce07 100644 --- a/src/pages/ReversePage.svelte +++ b/src/pages/ReversePage.svelte @@ -7,7 +7,7 @@ import ResultsList from '../components/ResultsList.svelte'; import Map from '../components/Map.svelte'; - let api_request_params = $state(); + let api_request_params = $state.raw(); let current_result = $state(); let position_marker = $state(); // what the user searched for @@ -25,9 +25,9 @@ }; if (api_request_params.lat && api_request_params.lon) { + position_marker = [api_request_params.lat, api_request_params.lon]; fetch_from_api('reverse', api_request_params, function (data) { - position_marker = [api_request_params.lat, api_request_params.lon]; if (data && !data.error) { results_store.set([data]); } else { diff --git a/src/state/MapState.svelte.js b/src/state/MapState.svelte.js new file mode 100644 index 0000000..c30dad6 --- /dev/null +++ b/src/state/MapState.svelte.js @@ -0,0 +1,12 @@ +import {latLng } from 'leaflet'; + +class MapState { + center = $state(latLng(Nominatim_Config.Map_Default_Lat, + Nominatim_Config.Map_Default_Lon)); + zoom = $state(Nominatim_Config.Map_Default_Zoom); + viewboxStr = $state(); + lastClick = $state(); + mousePos = $state(); +} + +export const mapState = new MapState(); diff --git a/test/search.js b/test/search.js index 2a383d7..e9aeaa3 100644 --- a/test/search.js +++ b/test/search.js @@ -35,10 +35,9 @@ describe('Search Page', function () { it('should show map bounds buttons', async function () { await page.waitForSelector('#map'); let show_map_pos_handle = await page.$('#show-map-position'); - let map_pos_handle = await page.$('#map-position'); + assert.strictEqual(await page.$('#map-position-inner'), null); await show_map_pos_handle.click(); - assert.strictEqual(await map_pos_handle.evaluate(node => node.style.display), 'block'); let map_pos_details = await page.$eval('#map-position-inner', el => el.textContent); map_pos_details = map_pos_details.split(' '); @@ -58,10 +57,11 @@ describe('Search Page', function () { assert.deepStrictEqual(map_center_coords.length, 2); assert.ok(map_zoom); assert.deepStrictEqual(map_viewbox.length, 4); - assert.deepStrictEqual(last_click, '65.62128,104.41406'); + assert.deepStrictEqual(last_click, '-'); await page.click('#map-position-close a'); - assert.strictEqual(await map_pos_handle.evaluate(node => node.style.display), 'none'); + assert.strictEqual(await page.$('#map-position-inner'), null); + assert.ok(await page.$('#show-map-position')); }); }); -- 2.39.5