Files
marco/app/routes/place.js
T
raucao 1de276fcc8
CI / Test (pull_request) Failing after 8s
CI / Lint (pull_request) Failing after 12s
Fix OSM data refreshes not immediately rendering
2026-08-19 11:01:42 -06:00

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 };
}
}