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:
@@ -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 &&
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user