* Fetch results asynchronously after app launch * Hide sidebar and search results when new search is issued
193 lines
5.8 KiB
JavaScript
193 lines
5.8 KiB
JavaScript
import Controller from '@ember/controller';
|
|
import { service } from '@ember/service';
|
|
import { task } from 'ember-concurrency';
|
|
import { getDistance } from '../utils/geo';
|
|
|
|
export default class SearchController extends Controller {
|
|
@service osm;
|
|
@service photon;
|
|
@service mapUi;
|
|
@service storage;
|
|
@service router;
|
|
@service toast;
|
|
|
|
queryParams = ['lat', 'lon', 'q', 'selected', 'category'];
|
|
|
|
lat = null;
|
|
lon = null;
|
|
q = null;
|
|
selected = null;
|
|
category = null;
|
|
|
|
fetchResultsTask = task({ restartable: true }, async (params) => {
|
|
// Hide sidebar and clear previous results immediately to signal a new search
|
|
this.mapUi.hideSidebar();
|
|
this.mapUi.clearSearchResults();
|
|
|
|
const lat = params.lat ? parseFloat(params.lat) : null;
|
|
const lon = params.lon ? parseFloat(params.lon) : null;
|
|
let pois = [];
|
|
let loadingType = null;
|
|
let loadingValue = null;
|
|
|
|
try {
|
|
// Case 0: Category Search (category parameter present)
|
|
if (params.category && lat && lon) {
|
|
loadingType = 'category';
|
|
loadingValue = params.category;
|
|
this.mapUi.startLoading(loadingType, loadingValue);
|
|
|
|
// We need bounds. If we have active map state, use it.
|
|
let bounds = this.mapUi.currentBounds;
|
|
|
|
// If we don't have bounds (direct URL visit), estimate them from lat/lon/zoom(16)
|
|
// or just use a fixed box around the center.
|
|
if (!bounds) {
|
|
// Approximate 0.01 degrees ~ 1km at equator. A viewport is roughly 0.02x0.01 at zoom 16?
|
|
// Let's take a safe box of ~1km radius.
|
|
const delta = 0.01;
|
|
bounds = {
|
|
minLat: lat - delta,
|
|
maxLat: lat + delta,
|
|
minLon: lon - delta,
|
|
maxLon: lon + delta,
|
|
};
|
|
}
|
|
|
|
pois = await this.osm.getCategoryPois(
|
|
bounds,
|
|
params.category,
|
|
lat,
|
|
lon
|
|
);
|
|
|
|
// Sort by distance from center
|
|
pois = pois
|
|
.map((p) => ({
|
|
...p,
|
|
_distance: getDistance(lat, lon, p.lat, p.lon),
|
|
}))
|
|
.sort((a, b) => a._distance - b._distance);
|
|
}
|
|
// Case 1: Text Search (q parameter present)
|
|
else if (params.q) {
|
|
loadingType = 'text';
|
|
loadingValue = params.q;
|
|
this.mapUi.startLoading(loadingType, loadingValue);
|
|
|
|
// 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) {
|
|
// Nearby search does NOT trigger loading state (pulse is used instead)
|
|
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);
|
|
}
|
|
} catch (error) {
|
|
console.error('Search request failed.', error);
|
|
this.toast.show('Search request failed. Please try again.');
|
|
this.mapUi.stopSearch();
|
|
return;
|
|
} finally {
|
|
if (loadingType && loadingValue) {
|
|
this.mapUi.stopLoading(loadingType, loadingValue);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
});
|
|
|
|
const targetName = params.selected || params.q;
|
|
|
|
if (targetName && pois.length > 0) {
|
|
let matchedPlace = null;
|
|
|
|
// 1. Exact Name Match
|
|
matchedPlace = pois.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 = pois[0];
|
|
if (topCandidate._distance <= 10) {
|
|
matchedPlace = topCandidate;
|
|
}
|
|
}
|
|
|
|
if (matchedPlace) {
|
|
// Direct transition!
|
|
this.router.replaceWith('place', matchedPlace);
|
|
this.mapUi.stopSearch();
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.mapUi.setSearchResults(pois);
|
|
this.mapUi.showSidebar();
|
|
this.mapUi.stopSearch();
|
|
});
|
|
}
|