193 lines
5.9 KiB
JavaScript
193 lines
5.9 KiB
JavaScript
import Route from '@ember/routing/route';
|
|
import { service } from '@ember/service';
|
|
|
|
// Detects whether the fresh OSM data differs from the currently-shown model
|
|
// by more than ~1m of coordinate drift, or in any tag. Mirrors the relevant
|
|
// subset of `storage.refreshPlace`'s diff for the non-bookmark path.
|
|
function hasOsmChanges(place, fresh) {
|
|
const latDiff = Math.abs((place.lat ?? 0) - (fresh.lat ?? 0));
|
|
const lonDiff = Math.abs((place.lon ?? 0) - (fresh.lon ?? 0));
|
|
if (latDiff > 0.00001 || lonDiff > 0.00001) return true;
|
|
|
|
const oldTags = place.osmTags || {};
|
|
const newTags = fresh.osmTags || {};
|
|
const allKeys = new Set([...Object.keys(oldTags), ...Object.keys(newTags)]);
|
|
for (const key of allKeys) {
|
|
if (oldTags[key] !== newTags[key]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export default class PlaceRoute extends Route {
|
|
@service storage;
|
|
@service osm;
|
|
@service mapUi;
|
|
|
|
async model(params) {
|
|
const id = params.place_id;
|
|
|
|
let type, osmId;
|
|
let isExplicitOsm = false;
|
|
|
|
if (
|
|
id.startsWith('osm:node:') ||
|
|
id.startsWith('osm:way:') ||
|
|
id.startsWith('osm:relation:')
|
|
) {
|
|
isExplicitOsm = true;
|
|
[, type, osmId] = id.split(':');
|
|
console.debug(`Fetching explicit OSM ${type}:`, osmId);
|
|
}
|
|
|
|
let backgroundFetchPromise = null;
|
|
if (isExplicitOsm) {
|
|
backgroundFetchPromise = this.loadOsmPlace(osmId, type);
|
|
}
|
|
|
|
await this.waitForSync();
|
|
|
|
let lookupId = isExplicitOsm ? osmId : id;
|
|
let bookmark = this.storage.findPlaceById(lookupId);
|
|
|
|
// Ensure type matches if we are looking up by osmId
|
|
if (bookmark && isExplicitOsm && bookmark.osmType !== type) {
|
|
bookmark = null; // Type mismatch, not the same OSM object
|
|
}
|
|
|
|
if (bookmark) {
|
|
console.debug('Found in bookmarks:', bookmark.title);
|
|
return bookmark;
|
|
}
|
|
|
|
if (isExplicitOsm) {
|
|
console.debug(
|
|
`Not in bookmarks, using explicitly fetched OSM ${type}:`,
|
|
osmId
|
|
);
|
|
return await backgroundFetchPromise;
|
|
}
|
|
|
|
console.warn('Not in bookmarks:', id);
|
|
return null;
|
|
}
|
|
|
|
async waitForSync() {
|
|
if (this.storage.initialSyncDone) return;
|
|
|
|
console.debug('Waiting for initial storage sync...');
|
|
const timeout = 5000;
|
|
const start = Date.now();
|
|
|
|
while (!this.storage.initialSyncDone) {
|
|
if (Date.now() - start > timeout) {
|
|
console.warn('Timed out waiting for initial sync');
|
|
break;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
}
|
|
|
|
async afterModel(model) {
|
|
// If the model comes from a search result (e.g. Photon), it might lack detailed geometry.
|
|
// We want to ensure we have the full OSM object (with polygon/linestring) for display.
|
|
if (
|
|
model &&
|
|
model.osmId &&
|
|
model.osmType &&
|
|
model.osmType !== 'node' &&
|
|
!model.geojson
|
|
) {
|
|
// Only fetch if it's NOT a node (nodes don't have interesting geometry anyway, just a point)
|
|
// Although fetching nodes again ensures we have the latest tags too.
|
|
console.debug('Model missing geometry, fetching full OSM details...');
|
|
const fullDetails = await this.loadOsmPlace(model.osmId, model.osmType);
|
|
|
|
if (fullDetails) {
|
|
// Update the model in-place with the fuller details
|
|
Object.assign(model, fullDetails);
|
|
console.debug('Enriched model with full OSM details', model);
|
|
}
|
|
}
|
|
|
|
// Notify the Map UI to show the pin
|
|
if (model) {
|
|
const options = { preventZoom: this.mapUi.preventNextZoom };
|
|
this.mapUi.selectPlace(model, options);
|
|
this.mapUi.showSidebar();
|
|
this.mapUi.preventNextZoom = false;
|
|
}
|
|
// Stop the pulse animation if it was running (e.g. redirected from search)
|
|
this.mapUi.stopSearch();
|
|
}
|
|
|
|
deactivate() {
|
|
// Clear the pin when leaving the route
|
|
this.mapUi.clearSelection();
|
|
// Reset the "return to search" flag so it doesn't persist to subsequent navigations
|
|
this.mapUi.returnToSearch = false;
|
|
this.mapUi.returnToRoute = null;
|
|
}
|
|
|
|
async loadOsmPlace(id, type = null) {
|
|
try {
|
|
// Use the direct OSM API fetch instead of Overpass for single object lookups
|
|
const poi = await this.osm.fetchOsmObject(id, type);
|
|
if (poi) {
|
|
console.debug('Found OSM POI:', poi);
|
|
return poi;
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to fetch POI', e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
setupController(controller, model) {
|
|
super.setupController(controller, model);
|
|
this.checkUpdates(model);
|
|
}
|
|
|
|
async checkUpdates(place) {
|
|
if (!place || !place.osmId || !place.osmType) return;
|
|
|
|
// Bookmarked place — refresh via storage, which persists the update.
|
|
if (place.id) {
|
|
const updatedPlace = await this.storage.refreshPlace(place);
|
|
if (updatedPlace) {
|
|
// If an update occurred, refresh the map UI selection without moving
|
|
// the camera. This ensures the sidebar shows the new data.
|
|
this.mapUi.selectPlace(updatedPlace, { preventZoom: true });
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Non-bookmarked explicit OSM place — fetch fresh and update the model
|
|
// in-place if anything changed, so the sidebar reflects the latest OSM
|
|
// data without requiring a re-open.
|
|
try {
|
|
const fresh = await this.osm.fetchOsmObject(place.osmId, place.osmType, {
|
|
forceFresh: true,
|
|
});
|
|
if (fresh && hasOsmChanges(place, fresh)) {
|
|
Object.assign(place, fresh);
|
|
this.mapUi.selectPlace(place, { preventZoom: true });
|
|
}
|
|
} catch (e) {
|
|
console.debug('[place] Fresh fetch failed for', place.osmId, e);
|
|
}
|
|
}
|
|
|
|
serialize(model) {
|
|
// If it's an OSM POI, use the explicit format first
|
|
if (model.osmId && model.osmType) {
|
|
return { place_id: `osm:${model.osmType}:${model.osmId}` };
|
|
}
|
|
// If the model is a saved bookmark (and not OSM, e.g. custom place), use its ID
|
|
if (model.id) {
|
|
return { place_id: model.id };
|
|
}
|
|
// Fallback
|
|
return { place_id: model.osmId };
|
|
}
|
|
}
|