diff --git a/app/components/map.gjs b/app/components/map.gjs
index 6b95cbf..0812a42 100644
--- a/app/components/map.gjs
+++ b/app/components/map.gjs
@@ -21,6 +21,7 @@ export default class MapComponent extends Component {
@service osm;
@service storage;
@service mapUi;
+ @service router;
mapInstance;
bookmarkSource;
@@ -510,6 +511,17 @@ export default class MapComponent extends Component {
}
}
+ // Sync the pulse animation with the UI service state
+ syncPulse = modifier(() => {
+ if (!this.searchOverlayElement) return;
+
+ if (this.mapUi.isSearching) {
+ this.searchOverlayElement.classList.add('active');
+ } else {
+ this.searchOverlayElement.classList.remove('active');
+ }
+ });
+
handleMapMove = async () => {
if (!this.mapInstance) return;
@@ -546,7 +558,6 @@ export default class MapComponent extends Component {
});
let clickedBookmark = null;
let selectedFeatureName = null;
- let selectedFeatureType = null;
if (features && features.length > 0) {
console.debug(`Found ${features.length} features in map layer:`);
@@ -561,7 +572,6 @@ export default class MapComponent extends Component {
const props = features[0].getProperties();
if (props.name) {
selectedFeatureName = props.name;
- selectedFeatureType = props.class || props.subclass;
}
}
@@ -573,9 +583,7 @@ export default class MapComponent extends Component {
'Clicked bookmark while sidebar open (switching):',
clickedBookmark
);
- if (this.args.onPlacesFound) {
- this.args.onPlacesFound([], clickedBookmark);
- }
+ this.router.transitionTo('place', clickedBookmark);
return;
}
@@ -589,9 +597,7 @@ export default class MapComponent extends Component {
// Normal behavior (sidebar is closed)
if (clickedBookmark) {
console.log('Clicked bookmark:', clickedBookmark);
- if (this.args.onPlacesFound) {
- this.args.onPlacesFound([], clickedBookmark);
- }
+ this.router.transitionTo('place', clickedBookmark);
return;
}
@@ -615,76 +621,21 @@ export default class MapComponent extends Component {
this.searchOverlayElement.style.width = `${diameterInPixels}px`;
this.searchOverlayElement.style.height = `${diameterInPixels}px`;
this.searchOverlay.setPosition(event.coordinate);
- this.searchOverlayElement.classList.add('active');
}
- // 2. Fetch authoritative data via Overpass
- try {
- let pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
+ // Start Search State
+ this.mapUi.startSearch();
- // Sort by distance from click
- pois = pois
- .map((p) => {
- // p is already normalized by service, so lat/lon are at top level
- return {
- ...p,
- _distance: getDistance(lat, lon, p.lat, p.lon),
- };
- })
- .sort((a, b) => a._distance - b._distance);
-
- let matchedPlace = null;
-
- if (selectedFeatureName && pois.length > 0) {
- // Heuristic:
- // 1. Exact Name Match
- matchedPlace = pois.find(
- (p) =>
- p.osmTags &&
- (p.osmTags.name === selectedFeatureName ||
- p.osmTags['name:en'] === selectedFeatureName)
- );
-
- // 2. If no exact match, look for VERY close (<=20m) and matching type
- if (!matchedPlace) {
- const topCandidate = pois[0];
- if (topCandidate._distance <= 20) {
- // Check type compatibility if available
- // (visual tile 'class' is often 'cafe', osm tag is 'amenity'='cafe')
- const pType =
- topCandidate.osmTags.amenity ||
- topCandidate.osmTags.shop ||
- topCandidate.osmTags.tourism;
- if (
- selectedFeatureType &&
- pType &&
- (selectedFeatureType === pType ||
- pType.includes(selectedFeatureType))
- ) {
- console.log(
- 'Heuristic match found (distance + type):',
- topCandidate
- );
- matchedPlace = topCandidate;
- } else if (topCandidate._distance <= 10) {
- // Even without type match, if it's super close (<=10m), it's likely the one.
- console.log('Heuristic match found (proximity):', topCandidate);
- matchedPlace = topCandidate;
- }
- }
- }
- }
-
- if (this.args.onPlacesFound) {
- this.args.onPlacesFound(pois, matchedPlace);
- }
- } catch (error) {
- console.error('Failed to fetch POIs:', error);
- } finally {
- if (this.searchOverlayElement) {
- this.searchOverlayElement.classList.remove('active');
- }
+ // Transition to Search Route
+ const queryParams = {
+ lat: lat.toFixed(6),
+ lon: lon.toFixed(6),
+ };
+ if (selectedFeatureName) {
+ queryParams.q = selectedFeatureName;
}
+
+ this.router.transitionTo('search', { queryParams });
};
@@ -693,6 +644,7 @@ export default class MapComponent extends Component {
{{this.setupMap}}
{{this.updateBookmarks}}
{{this.updateSelectedPin}}
+ {{this.syncPulse}}
>
}
diff --git a/app/components/places-sidebar.gjs b/app/components/places-sidebar.gjs
index 8bd63f4..8a942d5 100644
--- a/app/components/places-sidebar.gjs
+++ b/app/components/places-sidebar.gjs
@@ -23,13 +23,6 @@ export default class PlacesSidebar extends Component {
if (this.args.onSelect) {
this.args.onSelect(null);
}
-
- // Fallback logic: if no list available, close sidebar
- if (!this.args.places || this.args.places.length === 0) {
- if (this.args.onClose) {
- this.args.onClose();
- }
- }
}
@action
diff --git a/app/router.js b/app/router.js
index 151dbf8..89c9419 100644
--- a/app/router.js
+++ b/app/router.js
@@ -8,4 +8,5 @@ export default class Router extends EmberRouter {
Router.map(function () {
this.route('place', { path: '/place/:place_id' });
+ this.route('search');
});
diff --git a/app/routes/place.js b/app/routes/place.js
index 1140696..bfc4c75 100644
--- a/app/routes/place.js
+++ b/app/routes/place.js
@@ -49,6 +49,8 @@ export default class PlaceRoute extends Route {
if (model) {
this.mapUi.selectPlace(model);
}
+ // Stop the pulse animation if it was running (e.g. redirected from search)
+ this.mapUi.stopSearch();
}
deactivate() {
diff --git a/app/routes/search.js b/app/routes/search.js
new file mode 100644
index 0000000..cbb37c4
--- /dev/null
+++ b/app/routes/search.js
@@ -0,0 +1,96 @@
+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 mapUi;
+ @service storage;
+ @service router;
+
+ queryParams = {
+ lat: { refreshModel: true },
+ lon: { refreshModel: true },
+ q: { refreshModel: true },
+ };
+
+ async model(params) {
+ // If no coordinates, we can't search
+ if (!params.lat || !params.lon) {
+ return [];
+ }
+
+ const lat = parseFloat(params.lat);
+ const lon = parseFloat(params.lon);
+ const searchRadius = params.q ? 30 : 50;
+
+ // Fetch POIs
+ let pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
+
+ // 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 } = transition.to.queryParams;
+
+ // Heuristic Match Logic (ported from MapComponent)
+ if (q && model.length > 0) {
+ let matchedPlace = null;
+
+ // 1. Exact Name Match
+ matchedPlace = model.find(
+ (p) => p.osmTags && (p.osmTags.name === q || p.osmTags['name:en'] === q)
+ );
+
+ // 2. High Proximity Match (<= 10m)
+ // 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
+ }
+}
diff --git a/app/services/map-ui.js b/app/services/map-ui.js
index 54bb6bf..d3d9892 100644
--- a/app/services/map-ui.js
+++ b/app/services/map-ui.js
@@ -3,6 +3,7 @@ import { tracked } from '@glimmer/tracking';
export default class MapUiService extends Service {
@tracked selectedPlace = null;
+ @tracked isSearching = false;
selectPlace(place) {
this.selectedPlace = place;
@@ -11,4 +12,12 @@ export default class MapUiService extends Service {
clearSelection() {
this.selectedPlace = null;
}
+
+ startSearch() {
+ this.isSearching = true;
+ }
+
+ stopSearch() {
+ this.isSearching = false;
+ }
}
diff --git a/app/templates/application.gjs b/app/templates/application.gjs
index a41932b..4ec36d4 100644
--- a/app/templates/application.gjs
+++ b/app/templates/application.gjs
@@ -1,26 +1,28 @@
import Component from '@glimmer/component';
import { pageTitle } from 'ember-page-title';
import Map from '#components/map';
-import PlacesSidebar from '#components/places-sidebar';
import AppHeader from '#components/app-header';
import SettingsPane from '#components/settings-pane';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
-import { eq } from 'ember-truth-helpers';
-import { and, or } from 'ember-truth-helpers';
+import { or } from 'ember-truth-helpers';
import { on } from '@ember/modifier';
export default class ApplicationComponent extends Component {
@service storage;
+ @service mapUi;
@service router;
- @tracked nearbyPlaces = null;
@tracked isSettingsOpen = false;
- // @tracked bookmarksVersion = 0; // Moved to storage service
get isSidebarOpen() {
- return !!this.nearbyPlaces || this.router.currentRouteName === 'place';
+ // We consider the sidebar "open" if we are in search or place routes.
+ // This helps the map know if it should shift the center or adjust view.
+ return (
+ this.router.currentRouteName === 'place' ||
+ this.router.currentRouteName === 'search'
+ );
}
constructor() {
@@ -30,32 +32,6 @@ export default class ApplicationComponent extends Component {
this.storage;
}
- @action
- showPlaces(places, selectedPlace = null) {
- // Helper to resolve a place to its bookmark if it exists
- const resolvePlace = (p) => {
- if (!p) return null;
- // We use the OSM ID to check if we already have this place saved
- const saved = this.storage.findPlaceById(p.osmId);
- return saved || p;
- };
-
- const resolvedSelected = resolvePlace(selectedPlace);
- const resolvedPlaces = places ? places.map(resolvePlace) : [];
-
- // If we have a specific place, transition to the route
- if (resolvedSelected) {
- // Pass the FULL object model to avoid re-fetching!
- // The Route's serialize() hook handles URL generation.
- this.router.transitionTo('place', resolvedSelected);
- this.nearbyPlaces = null; // Clear list when selecting specific
- } else if (resolvedPlaces && resolvedPlaces.length > 0) {
- // Show list case
- this.nearbyPlaces = resolvedPlaces;
- this.router.transitionTo('index');
- }
- }
-
@action
toggleSettings() {
this.isSettingsOpen = !this.isSettingsOpen;
@@ -66,29 +42,20 @@ export default class ApplicationComponent extends Component {
this.isSettingsOpen = false;
}
- @action
- selectFromList(place) {
- if (place) {
- // Optimize: Pass full object to avoid fetch
- this.router.transitionTo('place', place);
- }
- }
-
@action
handleOutsideClick() {
if (this.isSettingsOpen) {
this.closeSettings();
- } else {
- this.closeSidebar();
+ } else if (this.router.currentRouteName === 'search') {
+ this.router.transitionTo('index');
+ } else if (this.router.currentRouteName === 'place') {
+ // If in place route, decide if we want to go back to search or index
+ // For now, let's go to index or maybe back to search if search params exist?
+ // Simplest behavior: clear selection
+ this.router.transitionTo('index');
}
}
- @action
- closeSidebar() {
- this.nearbyPlaces = null;
- this.router.transitionTo('index');
- }
-
@action
refreshBookmarks() {
this.storage.notifyChange();
@@ -113,19 +80,10 @@ export default class ApplicationComponent extends Component {
{{/if}}
- {{#if (and (eq this.router.currentRouteName "index") this.nearbyPlaces)}}
-
- {{/if}}
-
{{#if this.isSettingsOpen}}
{{/if}}
diff --git a/app/templates/place.gjs b/app/templates/place.gjs
index 2cde9d6..22b7843 100644
--- a/app/templates/place.gjs
+++ b/app/templates/place.gjs
@@ -7,6 +7,7 @@ import { tracked } from '@glimmer/tracking';
export default class PlaceTemplate extends Component {
@service router;
@service storage;
+ @service mapUi;
@tracked localPlace = null;
@@ -72,8 +73,26 @@ export default class PlaceTemplate extends Component {
this.storage.notifyChange();
}
+ @action
+ navigateBack(place) {
+ // The sidebar calls this with null when "Back" is clicked.
+ if (place === null) {
+ // If we have history, go back (preserves search state)
+ if (window.history.length > 1) {
+ window.history.back();
+ } else {
+ // Fallback if opened directly
+ this.router.transitionTo('index');
+ }
+ } else {
+ // If a place is selected (unlikely in this view, but possible if we add related links)
+ this.router.transitionTo('place', place);
+ }
+ }
+
@action
close() {
+ // Clear search results so we don't fall back to the list
this.router.transitionTo('index');
}
@@ -81,6 +100,7 @@ export default class PlaceTemplate extends Component {
diff --git a/app/templates/search.gjs b/app/templates/search.gjs
new file mode 100644
index 0000000..ed2f59b
--- /dev/null
+++ b/app/templates/search.gjs
@@ -0,0 +1,28 @@
+import Component from '@glimmer/component';
+import PlacesSidebar from '#components/places-sidebar';
+import { service } from '@ember/service';
+import { action } from '@ember/object';
+
+export default class SearchTemplate extends Component {
+ @service router;
+
+ @action
+ selectPlace(place) {
+ if (place) {
+ this.router.transitionTo('place', place);
+ }
+ }
+
+ @action
+ close() {
+ this.router.transitionTo('index');
+ }
+
+
+
+
+}