From 7e2bce84dbfa6e14e63ebf3777c7e890f2bb5186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 3 Sep 2026 14:16:11 -0600 Subject: [PATCH 1/2] Immediately load OSM place when selecting a map feature No waiting for Overpass when selecting visible POIs anymore --- app/components/map.gjs | 110 ++++++++++++++++++++++++++++++ tests/unit/components/map-test.js | 60 ++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 tests/unit/components/map-test.js diff --git a/app/components/map.gjs b/app/components/map.gjs index 17064ea..dc0a3b0 100644 --- a/app/components/map.gjs +++ b/app/components/map.gjs @@ -1005,6 +1005,62 @@ export default class MapComponent extends Component { } }); + decodeVectorTileOsmFeature(feature) { + if (!feature?.getId || !feature?.get) { + return { decoded: null, reason: 'feature does not expose get()/getId()' }; + } + + // ol-mapbox-style stores the source-layer name on `mvt:layer`. + const sourceLayer = feature.get('mvt:layer') || feature.get('layer'); + + // OpenFreeMap uses Planetiler/OpenMapTiles tiles, which encode OSM-backed + // feature ids as (osmId * 10) + sourceType. + if (sourceLayer !== 'poi') { + return { + decoded: null, + reason: `unsupported source layer: ${sourceLayer || 'unknown'}`, + }; + } + + const rawId = feature.getId(); + const encodedId = Number(rawId); + if (!Number.isSafeInteger(encodedId) || encodedId <= 0) { + return { + decoded: null, + reason: `feature id is not a positive safe integer: ${rawId}`, + }; + } + + const osmType = { + 1: 'node', + 2: 'way', + 3: 'relation', + }[encodedId % 10]; + + if (!osmType) { + return { + decoded: null, + reason: `feature id suffix ${encodedId % 10} is not an OSM type`, + }; + } + + const osmId = Math.floor(encodedId / 10); + if (osmId <= 0) { + return { + decoded: null, + reason: `decoded OSM id is invalid: ${osmId}`, + }; + } + + return { + decoded: { + osmId: String(osmId), + osmType, + }, + reason: null, + }; + } + animateToCrosshair(targetCoords) { if (!this.mapInstance || !this.crosshairElement) return; @@ -1141,6 +1197,7 @@ export default class MapComponent extends Component { }); let clickedBookmark = null; let clickedSearchResult = null; + let clickedTileOsmFeature = null; let selectedFeatureName = null; if (features && features.length > 0) { @@ -1155,6 +1212,31 @@ export default class MapComponent extends Component { clickedBookmark = bookmarkFeature.get('originalPlace'); } else if (searchResultFeature) { clickedSearchResult = searchResultFeature.get('originalPlace'); + } else { + for (const feature of features) { + const sourceLayer = feature.get?.('mvt:layer') || feature.get?.('layer'); + const featureName = feature.get?.('name'); + const featureId = feature.getId?.(); + const { decoded, reason } = this.decodeVectorTileOsmFeature(feature); + + if (decoded) { + console.debug('Decoded vector tile feature to explicit OSM place:', { + sourceLayer, + featureName, + featureId, + decoded, + }); + clickedTileOsmFeature = decoded; + break; + } + + console.debug('Vector tile feature could not be decoded directly:', { + sourceLayer, + featureName, + featureId, + reason, + }); + } } // Also get visual props for standard map click logic later const props = features[0].getProperties(); @@ -1179,6 +1261,19 @@ export default class MapComponent extends Component { this.router.transitionTo('place', place); }; + const transitionToExplicitOsmPlace = ({ osmId, osmType }) => { + if ( + this.router.currentRouteName === 'search' || + (this.mapUi.currentSearch && this.mapUi.searchResults.length > 0) + ) { + this.mapUi.returnToSearch = true; + } + + this.mapUi.preventNextZoom = true; + this.mapUi.showSidebar(); + this.router.transitionTo('place', `osm:${osmType}:${osmId}`); + }; + // Special handling when sidebar is OPEN if (this.args.isSidebarOpen) { // If it's a bookmark or search result, we allow "switching" to it even if sidebar is open @@ -1192,6 +1287,15 @@ export default class MapComponent extends Component { return; } + if (clickedTileOsmFeature) { + console.debug( + 'Clicked vector tile POI while sidebar open (switching):', + clickedTileOsmFeature + ); + transitionToExplicitOsmPlace(clickedTileOsmFeature); + return; + } + // Otherwise (empty map or non-bookmark feature), close the sidebar if (this.args.onOutsideClick) { this.args.onOutsideClick(); @@ -1212,6 +1316,12 @@ export default class MapComponent extends Component { return; } + if (clickedTileOsmFeature) { + console.debug('Clicked vector tile POI:', clickedTileOsmFeature); + transitionToExplicitOsmPlace(clickedTileOsmFeature); + return; + } + if (this.mapUi.searchResults && this.mapUi.searchResults.length > 0) { console.debug('Clearing active search and markers on map click'); this.router.transitionTo('index'); diff --git a/tests/unit/components/map-test.js b/tests/unit/components/map-test.js new file mode 100644 index 0000000..612e9ab --- /dev/null +++ b/tests/unit/components/map-test.js @@ -0,0 +1,60 @@ +import MapComponent from 'marco/components/map'; +import { module, test } from 'qunit'; + +module('Unit | Component | map', function () { + test('it decodes Planetiler vector tile POI ids into OSM ids and types', function (assert) { + const feature = { + get(key) { + if (key === 'mvt:layer') return 'poi'; + return undefined; + }, + getId() { + return 12342; + }, + }; + + const result = MapComponent.prototype.decodeVectorTileOsmFeature(feature); + + assert.deepEqual(result, { + decoded: { + osmId: '1234', + osmType: 'way', + }, + reason: null, + }); + }); + + test('it ignores non-POI vector tile features', function (assert) { + const feature = { + get(key) { + if (key === 'mvt:layer') return 'transportation'; + return undefined; + }, + getId() { + return 12342; + }, + }; + + assert.deepEqual(MapComponent.prototype.decodeVectorTileOsmFeature(feature), { + decoded: null, + reason: 'unsupported source layer: transportation', + }); + }); + + test('it ignores unsupported or invalid vector tile ids', function (assert) { + const feature = { + get(key) { + if (key === 'mvt:layer') return 'poi'; + return undefined; + }, + getId() { + return 12340; + }, + }; + + assert.deepEqual(MapComponent.prototype.decodeVectorTileOsmFeature(feature), { + decoded: null, + reason: 'feature id suffix 0 is not an OSM type', + }); + }); +}); -- 2.50.1 From 0d92fc9937fabcab8133f0fa14728e3a7f37c9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 3 Sep 2026 14:21:22 -0600 Subject: [PATCH 2/2] Fix lint errors --- app/components/map.gjs | 18 +++++++++++------- tests/unit/components/map-test.js | 22 ++++++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/app/components/map.gjs b/app/components/map.gjs index dc0a3b0..557ea33 100644 --- a/app/components/map.gjs +++ b/app/components/map.gjs @@ -1214,18 +1214,22 @@ export default class MapComponent extends Component { clickedSearchResult = searchResultFeature.get('originalPlace'); } else { for (const feature of features) { - const sourceLayer = feature.get?.('mvt:layer') || feature.get?.('layer'); + const sourceLayer = + feature.get?.('mvt:layer') || feature.get?.('layer'); const featureName = feature.get?.('name'); const featureId = feature.getId?.(); const { decoded, reason } = this.decodeVectorTileOsmFeature(feature); if (decoded) { - console.debug('Decoded vector tile feature to explicit OSM place:', { - sourceLayer, - featureName, - featureId, - decoded, - }); + console.debug( + 'Decoded vector tile feature to explicit OSM place:', + { + sourceLayer, + featureName, + featureId, + decoded, + } + ); clickedTileOsmFeature = decoded; break; } diff --git a/tests/unit/components/map-test.js b/tests/unit/components/map-test.js index 612e9ab..f023b8c 100644 --- a/tests/unit/components/map-test.js +++ b/tests/unit/components/map-test.js @@ -35,10 +35,13 @@ module('Unit | Component | map', function () { }, }; - assert.deepEqual(MapComponent.prototype.decodeVectorTileOsmFeature(feature), { - decoded: null, - reason: 'unsupported source layer: transportation', - }); + assert.deepEqual( + MapComponent.prototype.decodeVectorTileOsmFeature(feature), + { + decoded: null, + reason: 'unsupported source layer: transportation', + } + ); }); test('it ignores unsupported or invalid vector tile ids', function (assert) { @@ -52,9 +55,12 @@ module('Unit | Component | map', function () { }, }; - assert.deepEqual(MapComponent.prototype.decodeVectorTileOsmFeature(feature), { - decoded: null, - reason: 'feature id suffix 0 is not an OSM type', - }); + assert.deepEqual( + MapComponent.prototype.decodeVectorTileOsmFeature(feature), + { + decoded: null, + reason: 'feature id suffix 0 is not an OSM type', + } + ); }); }); -- 2.50.1