Add map markers for search results
This commit is contained in:
@@ -16,7 +16,7 @@ import Feature from 'ol/Feature.js';
|
|||||||
import GeoJSON from 'ol/format/GeoJSON.js';
|
import GeoJSON from 'ol/format/GeoJSON.js';
|
||||||
import Point from 'ol/geom/Point.js';
|
import Point from 'ol/geom/Point.js';
|
||||||
import Geolocation from 'ol/Geolocation.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';
|
import { apply } from 'ol-mapbox-style';
|
||||||
|
|
||||||
export default class MapComponent extends Component {
|
export default class MapComponent extends Component {
|
||||||
@@ -28,6 +28,7 @@ export default class MapComponent extends Component {
|
|||||||
|
|
||||||
mapInstance;
|
mapInstance;
|
||||||
bookmarkSource;
|
bookmarkSource;
|
||||||
|
searchResultsSource;
|
||||||
selectedShapeSource;
|
selectedShapeSource;
|
||||||
searchOverlay;
|
searchOverlay;
|
||||||
searchOverlayElement;
|
searchOverlayElement;
|
||||||
@@ -110,6 +111,47 @@ export default class MapComponent extends Component {
|
|||||||
zIndex: 10, // Ensure it sits above the map tiles
|
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 = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 32 40" width="40" height="50">
|
||||||
|
<defs>
|
||||||
|
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="1.5" flood-color="black" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<path d="M12 2C6.5 2 2 6.5 2 12C2 17.5 12 24 12 24C12 24 22 17.5 22 12C22 6.5 17.5 2 12 2Z" fill="white" filter="url(#shadow)"/>
|
||||||
|
<circle cx="12" cy="12" r="8" fill="${markerColor}"/>
|
||||||
|
</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
|
// Default view settings
|
||||||
let center = [14.21683569, 27.060114248];
|
let center = [14.21683569, 27.060114248];
|
||||||
let zoom = 2.661;
|
let zoom = 2.661;
|
||||||
@@ -143,7 +185,7 @@ export default class MapComponent extends Component {
|
|||||||
|
|
||||||
this.mapInstance = new Map({
|
this.mapInstance = new Map({
|
||||||
target: element,
|
target: element,
|
||||||
layers: [openfreemap, selectedShapeLayer, bookmarkLayer],
|
layers: [openfreemap, selectedShapeLayer, searchResultLayer, bookmarkLayer],
|
||||||
view: view,
|
view: view,
|
||||||
controls: defaultControls({
|
controls: defaultControls({
|
||||||
zoom: true,
|
zoom: true,
|
||||||
@@ -178,7 +220,7 @@ export default class MapComponent extends Component {
|
|||||||
const pinIcon = document.createElement('div');
|
const pinIcon = document.createElement('div');
|
||||||
pinIcon.className = 'selected-pin';
|
pinIcon.className = 'selected-pin';
|
||||||
// Simple SVG for Map Pin
|
// Simple SVG for Map Pin
|
||||||
pinIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3" style="fill: #b31412; stroke: none;"></circle></svg>`;
|
pinIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3" style="fill: var(--marker-color-dark); stroke: none;"></circle></svg>`;
|
||||||
|
|
||||||
const pinShadow = document.createElement('div');
|
const pinShadow = document.createElement('div');
|
||||||
pinShadow.className = 'selected-pin-shadow';
|
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)
|
// Track the selected place from the UI Service (Router -> Map)
|
||||||
updateSelectedPin = modifier(() => {
|
updateSelectedPin = modifier(() => {
|
||||||
const selected = this.mapUi.selectedPlace;
|
const selected = this.mapUi.selectedPlace;
|
||||||
@@ -946,6 +1015,7 @@ export default class MapComponent extends Component {
|
|||||||
hitTolerance: 10,
|
hitTolerance: 10,
|
||||||
});
|
});
|
||||||
let clickedBookmark = null;
|
let clickedBookmark = null;
|
||||||
|
let clickedSearchResult = null;
|
||||||
let selectedFeatureName = null;
|
let selectedFeatureName = null;
|
||||||
|
|
||||||
if (features && features.length > 0) {
|
if (features && features.length > 0) {
|
||||||
@@ -954,8 +1024,12 @@ export default class MapComponent extends Component {
|
|||||||
console.debug(f);
|
console.debug(f);
|
||||||
}
|
}
|
||||||
const bookmarkFeature = features.find((f) => f.get('isBookmark'));
|
const bookmarkFeature = features.find((f) => f.get('isBookmark'));
|
||||||
|
const searchResultFeature = features.find((f) => f.get('isSearchResult'));
|
||||||
|
|
||||||
if (bookmarkFeature) {
|
if (bookmarkFeature) {
|
||||||
clickedBookmark = bookmarkFeature.get('originalPlace');
|
clickedBookmark = bookmarkFeature.get('originalPlace');
|
||||||
|
} else if (searchResultFeature) {
|
||||||
|
clickedSearchResult = searchResultFeature.get('originalPlace');
|
||||||
}
|
}
|
||||||
// Also get visual props for standard map click logic later
|
// Also get visual props for standard map click logic later
|
||||||
const props = features[0].getProperties();
|
const props = features[0].getProperties();
|
||||||
@@ -966,14 +1040,15 @@ export default class MapComponent extends Component {
|
|||||||
|
|
||||||
// Special handling when sidebar is OPEN
|
// Special handling when sidebar is OPEN
|
||||||
if (this.args.isSidebarOpen) {
|
if (this.args.isSidebarOpen) {
|
||||||
// If it's a bookmark, we allow "switching" to it even if sidebar is open
|
// If it's a bookmark or search result, we allow "switching" to it even if sidebar is open
|
||||||
if (clickedBookmark) {
|
const targetPlace = clickedBookmark || clickedSearchResult;
|
||||||
|
if (targetPlace) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'Clicked bookmark while sidebar open (switching):',
|
'Clicked feature while sidebar open (switching):',
|
||||||
clickedBookmark
|
targetPlace
|
||||||
);
|
);
|
||||||
this.mapUi.preventNextZoom = true;
|
this.mapUi.preventNextZoom = true;
|
||||||
this.router.transitionTo('place', clickedBookmark);
|
this.router.transitionTo('place', targetPlace);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,6 +1067,13 @@ export default class MapComponent extends Component {
|
|||||||
return;
|
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
|
// Require Zoom >= 17 for generic map searches
|
||||||
// This prevents accidental searches when interacting with the map at a high level
|
// This prevents accidental searches when interacting with the map at a high level
|
||||||
const currentZoom = this.mapInstance.getView().getZoom();
|
const currentZoom = this.mapInstance.getView().getZoom();
|
||||||
@@ -1041,6 +1123,7 @@ export default class MapComponent extends Component {
|
|||||||
{{this.setupMap}}
|
{{this.setupMap}}
|
||||||
{{this.updateInteractions}}
|
{{this.updateInteractions}}
|
||||||
{{this.updateBookmarks}}
|
{{this.updateBookmarks}}
|
||||||
|
{{this.updateSearchResults}}
|
||||||
{{this.updateSelectedPin}}
|
{{this.updateSelectedPin}}
|
||||||
{{this.syncPulse}}
|
{{this.syncPulse}}
|
||||||
{{this.syncCreationMode}}
|
{{this.syncCreationMode}}
|
||||||
|
|||||||
10
app/routes/index.js
Normal file
10
app/routes/index.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -169,6 +169,7 @@ export default class SearchRoute extends Route {
|
|||||||
super.setupController(controller, model);
|
super.setupController(controller, model);
|
||||||
// Ensure pulse is stopped if we reach here
|
// Ensure pulse is stopped if we reach here
|
||||||
this.mapUi.stopSearch();
|
this.mapUi.stopSearch();
|
||||||
|
this.mapUi.setSearchResults(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default class MapUiService extends Service {
|
|||||||
@tracked searchBoxHasFocus = false;
|
@tracked searchBoxHasFocus = false;
|
||||||
@tracked selectionOptions = {};
|
@tracked selectionOptions = {};
|
||||||
@tracked preventNextZoom = false;
|
@tracked preventNextZoom = false;
|
||||||
|
@tracked searchResults = [];
|
||||||
|
|
||||||
selectPlace(place, options = {}) {
|
selectPlace(place, options = {}) {
|
||||||
this.selectedPlace = place;
|
this.selectedPlace = place;
|
||||||
@@ -24,6 +25,14 @@ export default class MapUiService extends Service {
|
|||||||
this.preventNextZoom = false;
|
this.preventNextZoom = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSearchResults(results) {
|
||||||
|
this.searchResults = results || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearchResults() {
|
||||||
|
this.searchResults = [];
|
||||||
|
}
|
||||||
|
|
||||||
startSearch() {
|
startSearch() {
|
||||||
this.isSearching = true;
|
this.isSearching = true;
|
||||||
this.isCreating = false;
|
this.isCreating = false;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
--sidebar-width: 350px;
|
--sidebar-width: 350px;
|
||||||
--link-color: #2a7fff;
|
--link-color: #2a7fff;
|
||||||
--link-color-visited: #6a4fbf;
|
--link-color-visited: #6a4fbf;
|
||||||
|
--marker-color-primary: #ea4335;
|
||||||
|
--marker-color-dark: #b31412;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -872,15 +874,15 @@ span.icon {
|
|||||||
.selected-pin {
|
.selected-pin {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
color: #ea4335; /* Google Red */
|
color: var(--marker-color-primary);
|
||||||
filter: drop-shadow(0 4px 6px rgb(0 0 0 / 30%));
|
filter: drop-shadow(0 4px 6px rgb(0 0 0 / 30%));
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-pin svg {
|
.selected-pin svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
fill: #ea4335;
|
fill: var(--marker-color-primary);
|
||||||
stroke: #b31412; /* Darker red stroke */
|
stroke: var(--marker-color-dark);
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user