Improve place routing and loading

* Normalize OSM POIs and always use and store the OSM type and tags
* Pass place objects to place route, do not load from API if passed
* Construct place URLs with osm prefix including the type
* Load specific type from API when given
This commit is contained in:
2026-01-20 16:03:51 +07:00
parent 598ac5e587
commit 42bf8455e5
5 changed files with 83 additions and 55 deletions

View File

@@ -147,7 +147,7 @@ export default class MapComponent extends Component {
clearTimeout(locateTimeout); clearTimeout(locateTimeout);
locateTimeout = null; locateTimeout = null;
} }
// Remove listener // Remove listener
try { try {
if (locateListenerKey) { if (locateListenerKey) {
@@ -181,7 +181,7 @@ export default class MapComponent extends Component {
resolution, resolution,
coordinates coordinates
); );
const diameterInMeters = (accuracy || 50) * 2; const diameterInMeters = (accuracy || 50) * 2;
const diameterInPixels = diameterInMeters / pointResolution; const diameterInPixels = diameterInMeters / pointResolution;
this.locationOverlayElement.style.width = `${diameterInPixels}px`; this.locationOverlayElement.style.width = `${diameterInPixels}px`;
@@ -248,7 +248,7 @@ export default class MapComponent extends Component {
// 4. Start Following // 4. Start Following
locateListenerKey = geolocation.on('change:position', zoomToLocation); locateListenerKey = geolocation.on('change:position', zoomToLocation);
// 5. Set Safety Timeout (10s) // 5. Set Safety Timeout (10s)
locateTimeout = setTimeout(() => { locateTimeout = setTimeout(() => {
stopLocating(); stopLocating();
@@ -425,12 +425,10 @@ export default class MapComponent extends Component {
// Sort by distance from click // Sort by distance from click
pois = pois pois = pois
.map((p) => { .map((p) => {
// Use center lat/lon for ways/relations if available, else lat/lon // p is already normalized by service, so lat/lon are at top level
const pLat = p.lat || p.center?.lat;
const pLon = p.lon || p.center?.lon;
return { return {
...p, ...p,
_distance: pLat && pLon ? getDistance(lat, lon, pLat, pLon) : 9999, _distance: getDistance(lat, lon, p.lat, p.lon),
}; };
}) })
.sort((a, b) => a._distance - b._distance); .sort((a, b) => a._distance - b._distance);
@@ -442,9 +440,9 @@ export default class MapComponent extends Component {
// 1. Exact Name Match // 1. Exact Name Match
matchedPlace = pois.find( matchedPlace = pois.find(
(p) => (p) =>
p.tags && p.osmTags &&
(p.tags.name === selectedFeatureName || (p.osmTags.name === selectedFeatureName ||
p.tags['name:en'] === selectedFeatureName) p.osmTags['name:en'] === selectedFeatureName)
); );
// 2. If no exact match, look for VERY close (<=20m) and matching type // 2. If no exact match, look for VERY close (<=20m) and matching type
@@ -454,9 +452,9 @@ export default class MapComponent extends Component {
// Check type compatibility if available // Check type compatibility if available
// (visual tile 'class' is often 'cafe', osm tag is 'amenity'='cafe') // (visual tile 'class' is often 'cafe', osm tag is 'amenity'='cafe')
const pType = const pType =
topCandidate.tags.amenity || topCandidate.osmTags.amenity ||
topCandidate.tags.shop || topCandidate.osmTags.shop ||
topCandidate.tags.tourism; topCandidate.osmTags.tourism;
if ( if (
selectedFeatureType && selectedFeatureType &&
pType && pType &&

View File

@@ -172,7 +172,8 @@ export default class PlacesSidebar extends Component {
@selectedPlace.osmTags.amenity @selectedPlace.osmTags.amenity
@selectedPlace.osmTags.shop @selectedPlace.osmTags.shop
@selectedPlace.osmTags.tourism @selectedPlace.osmTags.tourism
"Point of Interest" @selectedPlace.osmTags.leisure
@selectedPlace.osmTags.historic
}} }}
{{#if @selectedPlace.description}} {{#if @selectedPlace.description}}
{{@selectedPlace.description}} {{@selectedPlace.description}}
@@ -241,15 +242,17 @@ export default class PlacesSidebar extends Component {
{{on "click" (fn this.selectPlace place)}} {{on "click" (fn this.selectPlace place)}}
> >
<div class="place-name">{{or <div class="place-name">{{or
place.tags.name place.title
place.tags.name:en place.osmTags.name
place.osmTags.name:en
"Unnamed Place" "Unnamed Place"
}}</div> }}</div>
<div class="place-type">{{or <div class="place-type">{{or
place.tags.amenity place.osmTags.amenity
place.tags.shop place.osmTags.shop
place.tags.tourism place.osmTags.tourism
"Point of Interest" place.osmTags.leisure
place.osmTags.historic
}}</div> }}</div>
</button> </button>
</li> </li>

View File

@@ -8,6 +8,13 @@ export default class PlaceRoute extends Route {
async model(params) { async model(params) {
const id = params.place_id; const id = params.place_id;
// Check for explicit OSM prefixes
if (id.startsWith('osm:node:') || id.startsWith('osm:way:')) {
const [, type, osmId] = id.split(':');
console.log(`Fetching explicit OSM ${type}:`, osmId);
return this.loadOsmPlace(osmId, type);
}
// 1. Try to find in local bookmarks // 1. Try to find in local bookmarks
// We rely on the service maintaining the list // We rely on the service maintaining the list
let bookmark = this.storage.findPlaceById(id); let bookmark = this.storage.findPlaceById(id);
@@ -23,29 +30,34 @@ export default class PlaceRoute extends Route {
return bookmark; return bookmark;
} }
// 2. Fallback: Fetch from OSM // 2. Fallback: Fetch from OSM (assuming generic ID or old format)
console.log('Not in bookmarks, fetching from OSM:', id); console.log('Not in bookmarks, fetching from OSM:', id);
return this.loadOsmPlace(id);
}
async loadOsmPlace(id, type = null) {
try { try {
const poi = await this.osm.getPoiById(id); const poi = await this.osm.getPoiById(id, type);
if (poi) { if (poi) {
console.debug('Found OSM POI:', poi); console.debug('Found OSM POI:', poi);
// Map to our Place schema so the sidebar understands it return poi;
return {
title: poi.tags.name || poi.tags['name:en'] || 'Untitled Place',
lat: poi.lat || poi.center?.lat,
lon: poi.lon || poi.center?.lon,
url: poi.tags.website,
osmId: String(poi.id),
osmTags: poi.tags, // raw tags
osmType: poi.type, // "node" or "way"
description: poi.tags.description, // ensure description maps
// No ID/Geohash/CreatedAt means it's not saved
};
} }
} catch (e) { } catch (e) {
console.error('Failed to fetch POI', e); console.error('Failed to fetch POI', e);
} }
return null; return null;
} }
serialize(model) {
// If the model is a saved bookmark, use its ID
if (model.id) {
return { place_id: model.id };
}
// If it's an OSM POI, use the explicit format
if (model.osmId && model.osmType) {
return { place_id: `osm:${model.osmType}:${model.osmId}` };
}
// Fallback
return { place_id: model.osmId };
}
} }

View File

@@ -31,7 +31,9 @@ out center;
const res = await this.fetchWithRetry(url, { signal }); const res = await this.fetchWithRetry(url, { signal });
if (!res.ok) throw new Error('Overpass request failed'); if (!res.ok) throw new Error('Overpass request failed');
const data = await res.json(); const data = await res.json();
return data.elements;
// Normalize data
return data.elements.map(this.normalizePoi);
} catch (e) { } catch (e) {
if (e.name === 'AbortError') { if (e.name === 'AbortError') {
console.log('Overpass request aborted'); console.log('Overpass request aborted');
@@ -41,6 +43,19 @@ out center;
} }
} }
normalizePoi(poi) {
return {
title: poi.tags?.name || poi.tags?.['name:en'] || 'Untitled Place',
lat: poi.lat || poi.center?.lat,
lon: poi.lon || poi.center?.lon,
url: poi.tags?.website,
osmId: String(poi.id),
osmType: poi.type,
osmTags: poi.tags || {},
description: poi.tags?.description,
};
}
async fetchWithRetry(url, options = {}, retries = 3) { async fetchWithRetry(url, options = {}, retries = 3) {
try { try {
const res = await fetch(url, options); const res = await fetch(url, options);
@@ -64,22 +79,25 @@ out center;
} }
} }
async getPoiById(id) { async getPoiById(id, type = null) {
// Assuming 'id' is just the numeric ID. // If type is provided, we can be specific.
// Overpass needs type(id). But we might not know the type (node, way, relation). // If not, we query both node and way.
// We can query all types for this ID. let query;
// However, typical usage often passes just the numeric ID.
// A query for just ID(numeric) is tricky without type.
// Let's assume 'node' first or try to query all three types by ID.
const query = ` if (type === 'node') {
query = `[out:json][timeout:25];node(${id});out center;`;
} else if (type === 'way') {
query = `[out:json][timeout:25];way(${id});out center;`;
} else {
query = `
[out:json][timeout:25]; [out:json][timeout:25];
( (
node(${id}); node(${id});
way(${id}); way(${id});
); );
out center; out center;
`.trim(); `.trim();
}
const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent( const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
query query
@@ -87,6 +105,7 @@ out center;
const res = await this.fetchWithRetry(url); const res = await this.fetchWithRetry(url);
if (!res.ok) throw new Error('Overpass request failed'); if (!res.ok) throw new Error('Overpass request failed');
const data = await res.json(); const data = await res.json();
return data.elements[0]; // Return the first match if (!data.elements[0]) return null;
return this.normalizePoi(data.elements[0]);
} }
} }

View File

@@ -30,11 +30,9 @@ export default class ApplicationComponent extends Component {
showPlaces(places, selectedPlace = null) { showPlaces(places, selectedPlace = null) {
// If we have a specific place, transition to the route // If we have a specific place, transition to the route
if (selectedPlace) { if (selectedPlace) {
// Use ID if available, or osmId // Pass the FULL object model to avoid re-fetching!
const id = selectedPlace.id || selectedPlace.osmId; // The Route's serialize() hook handles URL generation.
if (id) { this.router.transitionTo('place', selectedPlace);
this.router.transitionTo('place', id);
}
this.nearbyPlaces = null; // Clear list when selecting specific this.nearbyPlaces = null; // Clear list when selecting specific
} else if (places && places.length > 0) { } else if (places && places.length > 0) {
// Show list case // Show list case
@@ -46,10 +44,8 @@ export default class ApplicationComponent extends Component {
@action @action
selectFromList(place) { selectFromList(place) {
if (place) { if (place) {
const id = place.id || place.osmId; // Optimize: Pass full object to avoid fetch
if (id) { this.router.transitionTo('place', place);
this.router.transitionTo('place', id);
}
} }
} }