Add a search box with a quick results popover, as well full results in the sidebar on pressing enter.
150 lines
4.2 KiB
JavaScript
150 lines
4.2 KiB
JavaScript
import Route from '@ember/routing/route';
|
|
import { service } from '@ember/service';
|
|
import { action } from '@ember/object';
|
|
import { getDistance } from '../utils/geo';
|
|
|
|
export default class SearchRoute extends Route {
|
|
@service osm;
|
|
@service photon;
|
|
@service mapUi;
|
|
@service storage;
|
|
@service router;
|
|
|
|
queryParams = {
|
|
lat: { refreshModel: true },
|
|
lon: { refreshModel: true },
|
|
q: { refreshModel: true },
|
|
selected: { refreshModel: true },
|
|
};
|
|
|
|
async model(params) {
|
|
const lat = params.lat ? parseFloat(params.lat) : null;
|
|
const lon = params.lon ? parseFloat(params.lon) : null;
|
|
let pois = [];
|
|
|
|
// Case 1: Text Search (q parameter present)
|
|
if (params.q) {
|
|
// Search with Photon (using lat/lon for bias if available)
|
|
pois = await this.photon.search(params.q, lat, lon);
|
|
|
|
// Search local bookmarks by name
|
|
const queryLower = params.q.toLowerCase();
|
|
const localMatches = this.storage.savedPlaces.filter((p) => {
|
|
return (
|
|
p.title?.toLowerCase().includes(queryLower) ||
|
|
p.description?.toLowerCase().includes(queryLower)
|
|
);
|
|
});
|
|
|
|
// Merge local matches
|
|
localMatches.forEach((local) => {
|
|
const exists = pois.find(
|
|
(poi) =>
|
|
(local.osmId && poi.osmId === local.osmId) ||
|
|
(poi.id && poi.id === local.id)
|
|
);
|
|
if (!exists) {
|
|
pois.push(local);
|
|
}
|
|
});
|
|
}
|
|
// Case 2: Nearby Search (lat/lon present, no q)
|
|
else if (lat && lon) {
|
|
const searchRadius = 50; // Default radius
|
|
|
|
// Fetch POIs from Overpass
|
|
pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
|
|
|
|
// Get cached/saved places in search radius
|
|
const localMatches = this.storage.savedPlaces.filter((p) => {
|
|
const dist = getDistance(lat, lon, p.lat, p.lon);
|
|
return dist <= searchRadius;
|
|
});
|
|
|
|
// Merge local matches
|
|
localMatches.forEach((local) => {
|
|
const exists = pois.find(
|
|
(poi) =>
|
|
(local.osmId && poi.osmId === local.osmId) ||
|
|
(poi.id && poi.id === local.id)
|
|
);
|
|
|
|
if (!exists) {
|
|
pois.push(local);
|
|
}
|
|
});
|
|
|
|
// Sort by distance from click
|
|
pois = pois
|
|
.map((p) => {
|
|
return {
|
|
...p,
|
|
_distance: getDistance(lat, lon, p.lat, p.lon),
|
|
};
|
|
})
|
|
.sort((a, b) => a._distance - b._distance);
|
|
}
|
|
|
|
// Check if any of these are already bookmarked
|
|
// We resolve them to the bookmark version if they exist
|
|
pois = pois.map((p) => {
|
|
const saved = this.storage.findPlaceById(p.osmId);
|
|
return saved || p;
|
|
});
|
|
|
|
return pois;
|
|
}
|
|
|
|
afterModel(model, transition) {
|
|
const { q, selected } = transition.to.queryParams;
|
|
|
|
// Heuristic Match Logic (ported from MapComponent)
|
|
// If 'selected' is provided (from map click), try to find that specific feature.
|
|
// If 'q' is provided (from text search), try to find an exact match to auto-select.
|
|
const targetName = selected || q;
|
|
|
|
if (targetName && model.length > 0) {
|
|
let matchedPlace = null;
|
|
|
|
// 1. Exact Name Match
|
|
matchedPlace = model.find(
|
|
(p) =>
|
|
p.osmTags &&
|
|
(p.osmTags.name === targetName || p.osmTags['name:en'] === targetName)
|
|
);
|
|
|
|
// 2. High Proximity Match (<= 10m) - Only if we don't have a name match
|
|
// Note: MapComponent had logic for <=20m + type match.
|
|
// We might want to pass the 'type' in queryParams if we want to be that precise.
|
|
// For now, let's stick to name or very close proximity.
|
|
if (!matchedPlace) {
|
|
const topCandidate = model[0];
|
|
if (topCandidate._distance <= 10) {
|
|
matchedPlace = topCandidate;
|
|
}
|
|
}
|
|
|
|
if (matchedPlace) {
|
|
// Direct transition!
|
|
this.router.replaceWith('place', matchedPlace);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Stop the pulse animation since search is done (and we are staying here)
|
|
this.mapUi.stopSearch();
|
|
}
|
|
|
|
setupController(controller, model) {
|
|
super.setupController(controller, model);
|
|
// Ensure pulse is stopped if we reach here
|
|
this.mapUi.stopSearch();
|
|
}
|
|
|
|
@action
|
|
error() {
|
|
this.mapUi.stopSearch();
|
|
return true; // Bubble error
|
|
}
|
|
}
|