diff --git a/app/components/map.gjs b/app/components/map.gjs
index ace5138..fad1a0c 100644
--- a/app/components/map.gjs
+++ b/app/components/map.gjs
@@ -16,7 +16,7 @@ import Feature from 'ol/Feature.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import Point from 'ol/geom/Point.js';
import Geolocation from 'ol/Geolocation.js';
-import { Style, Circle, Fill, Stroke } from 'ol/style.js';
+import { Style, Circle, Fill, Stroke, Icon } from 'ol/style.js';
import { apply } from 'ol-mapbox-style';
export default class MapComponent extends Component {
@@ -28,6 +28,7 @@ export default class MapComponent extends Component {
mapInstance;
bookmarkSource;
+ searchResultsSource;
selectedShapeSource;
searchOverlay;
searchOverlayElement;
@@ -110,6 +111,47 @@ export default class MapComponent extends Component {
zIndex: 10, // Ensure it sits above the map tiles
});
+ // Create a vector source and layer for search results
+ this.searchResultsSource = new VectorSource();
+ let cachedSearchResultSvgUrl = null;
+
+ const searchResultStyle = (feature) => {
+ if (!cachedSearchResultSvgUrl) {
+ const markerColor =
+ getComputedStyle(document.documentElement)
+ .getPropertyValue('--marker-color-primary')
+ .trim() || '#ea4335';
+
+ const svg = `
+
+ `;
+ cachedSearchResultSvgUrl =
+ 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg.trim());
+ }
+
+ return new Style({
+ image: new Icon({
+ src: cachedSearchResultSvgUrl,
+ anchor: [0.5, 0.65],
+ scale: 1,
+ }),
+ });
+ };
+
+ const searchResultLayer = new VectorLayer({
+ source: this.searchResultsSource,
+ style: searchResultStyle,
+ zIndex: 11, // Above bookmarks (10)
+ });
+
// Default view settings
let center = [14.21683569, 27.060114248];
let zoom = 2.661;
@@ -143,7 +185,7 @@ export default class MapComponent extends Component {
this.mapInstance = new Map({
target: element,
- layers: [openfreemap, selectedShapeLayer, bookmarkLayer],
+ layers: [openfreemap, selectedShapeLayer, searchResultLayer, bookmarkLayer],
view: view,
controls: defaultControls({
zoom: true,
@@ -178,7 +220,7 @@ export default class MapComponent extends Component {
const pinIcon = document.createElement('div');
pinIcon.className = 'selected-pin';
// Simple SVG for Map Pin
- pinIcon.innerHTML = ``;
+ pinIcon.innerHTML = ``;
const pinShadow = document.createElement('div');
pinShadow.className = 'selected-pin-shadow';
@@ -464,6 +506,33 @@ export default class MapComponent extends Component {
);
});
+ updateSearchResults = modifier(() => {
+ if (!this.searchResultsSource) return;
+
+ this.searchResultsSource.clear();
+ const results = this.mapUi.searchResults;
+
+ if (!results || results.length === 0) return;
+
+ const features = [];
+ results.forEach((place) => {
+ if (place.lat && place.lon) {
+ const feature = new Feature({
+ geometry: new Point(fromLonLat([place.lon, place.lat])),
+ name: place.title,
+ id: place.id,
+ isSearchResult: true,
+ originalPlace: place,
+ });
+ features.push(feature);
+ }
+ });
+
+ if (features.length > 0) {
+ this.searchResultsSource.addFeatures(features);
+ }
+ });
+
// Track the selected place from the UI Service (Router -> Map)
updateSelectedPin = modifier(() => {
const selected = this.mapUi.selectedPlace;
@@ -946,6 +1015,7 @@ export default class MapComponent extends Component {
hitTolerance: 10,
});
let clickedBookmark = null;
+ let clickedSearchResult = null;
let selectedFeatureName = null;
if (features && features.length > 0) {
@@ -954,8 +1024,12 @@ export default class MapComponent extends Component {
console.debug(f);
}
const bookmarkFeature = features.find((f) => f.get('isBookmark'));
+ const searchResultFeature = features.find((f) => f.get('isSearchResult'));
+
if (bookmarkFeature) {
clickedBookmark = bookmarkFeature.get('originalPlace');
+ } else if (searchResultFeature) {
+ clickedSearchResult = searchResultFeature.get('originalPlace');
}
// Also get visual props for standard map click logic later
const props = features[0].getProperties();
@@ -966,14 +1040,15 @@ export default class MapComponent extends Component {
// Special handling when sidebar is OPEN
if (this.args.isSidebarOpen) {
- // If it's a bookmark, we allow "switching" to it even if sidebar is open
- if (clickedBookmark) {
+ // If it's a bookmark or search result, we allow "switching" to it even if sidebar is open
+ const targetPlace = clickedBookmark || clickedSearchResult;
+ if (targetPlace) {
console.debug(
- 'Clicked bookmark while sidebar open (switching):',
- clickedBookmark
+ 'Clicked feature while sidebar open (switching):',
+ targetPlace
);
this.mapUi.preventNextZoom = true;
- this.router.transitionTo('place', clickedBookmark);
+ this.router.transitionTo('place', targetPlace);
return;
}
@@ -992,6 +1067,13 @@ export default class MapComponent extends Component {
return;
}
+ if (clickedSearchResult) {
+ console.debug('Clicked search result:', clickedSearchResult);
+ this.mapUi.preventNextZoom = true;
+ this.router.transitionTo('place', clickedSearchResult);
+ return;
+ }
+
// Require Zoom >= 17 for generic map searches
// This prevents accidental searches when interacting with the map at a high level
const currentZoom = this.mapInstance.getView().getZoom();
@@ -1041,6 +1123,7 @@ export default class MapComponent extends Component {
{{this.setupMap}}
{{this.updateInteractions}}
{{this.updateBookmarks}}
+ {{this.updateSearchResults}}
{{this.updateSelectedPin}}
{{this.syncPulse}}
{{this.syncCreationMode}}
diff --git a/app/routes/index.js b/app/routes/index.js
new file mode 100644
index 0000000..79e789c
--- /dev/null
+++ b/app/routes/index.js
@@ -0,0 +1,10 @@
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class IndexRoute extends Route {
+ @service mapUi;
+
+ activate() {
+ this.mapUi.clearSearchResults();
+ }
+}
diff --git a/app/routes/search.js b/app/routes/search.js
index a1261a6..da437ef 100644
--- a/app/routes/search.js
+++ b/app/routes/search.js
@@ -169,6 +169,7 @@ export default class SearchRoute extends Route {
super.setupController(controller, model);
// Ensure pulse is stopped if we reach here
this.mapUi.stopSearch();
+ this.mapUi.setSearchResults(model);
}
@action
diff --git a/app/services/map-ui.js b/app/services/map-ui.js
index 3f38696..296fa5e 100644
--- a/app/services/map-ui.js
+++ b/app/services/map-ui.js
@@ -12,6 +12,7 @@ export default class MapUiService extends Service {
@tracked searchBoxHasFocus = false;
@tracked selectionOptions = {};
@tracked preventNextZoom = false;
+ @tracked searchResults = [];
selectPlace(place, options = {}) {
this.selectedPlace = place;
@@ -24,6 +25,14 @@ export default class MapUiService extends Service {
this.preventNextZoom = false;
}
+ setSearchResults(results) {
+ this.searchResults = results || [];
+ }
+
+ clearSearchResults() {
+ this.searchResults = [];
+ }
+
startSearch() {
this.isSearching = true;
this.isCreating = false;
diff --git a/app/styles/app.css b/app/styles/app.css
index e0dfbc2..706e4c7 100644
--- a/app/styles/app.css
+++ b/app/styles/app.css
@@ -6,6 +6,8 @@
--sidebar-width: 350px;
--link-color: #2a7fff;
--link-color-visited: #6a4fbf;
+ --marker-color-primary: #ea4335;
+ --marker-color-dark: #b31412;
}
html,
@@ -872,15 +874,15 @@ span.icon {
.selected-pin {
width: 40px;
height: 40px;
- color: #ea4335; /* Google Red */
+ color: var(--marker-color-primary);
filter: drop-shadow(0 4px 6px rgb(0 0 0 / 30%));
}
.selected-pin svg {
width: 100%;
height: 100%;
- fill: #ea4335;
- stroke: #b31412; /* Darker red stroke */
+ fill: var(--marker-color-primary);
+ stroke: var(--marker-color-dark);
stroke-width: 1;
}