Show map pin for currently selected place
This commit is contained in:
@@ -16,15 +16,19 @@ import Geolocation from 'ol/Geolocation.js';
|
|||||||
import { Style, Circle, Fill, Stroke } from 'ol/style.js';
|
import { Style, Circle, Fill, Stroke } from 'ol/style.js';
|
||||||
import { apply } from 'ol-mapbox-style';
|
import { apply } from 'ol-mapbox-style';
|
||||||
import { getDistance } from '../utils/geo';
|
import { getDistance } from '../utils/geo';
|
||||||
|
import Icon from '../components/icon';
|
||||||
|
|
||||||
export default class MapComponent extends Component {
|
export default class MapComponent extends Component {
|
||||||
@service osm;
|
@service osm;
|
||||||
@service storage;
|
@service storage;
|
||||||
|
@service mapUi;
|
||||||
|
|
||||||
mapInstance;
|
mapInstance;
|
||||||
bookmarkSource;
|
bookmarkSource;
|
||||||
searchOverlay;
|
searchOverlay;
|
||||||
searchOverlayElement;
|
searchOverlayElement;
|
||||||
|
selectedPinOverlay;
|
||||||
|
selectedPinElement;
|
||||||
|
|
||||||
setupMap = modifier((element) => {
|
setupMap = modifier((element) => {
|
||||||
if (this.mapInstance) return;
|
if (this.mapInstance) return;
|
||||||
@@ -105,6 +109,34 @@ export default class MapComponent extends Component {
|
|||||||
});
|
});
|
||||||
this.mapInstance.addOverlay(this.searchOverlay);
|
this.mapInstance.addOverlay(this.searchOverlay);
|
||||||
|
|
||||||
|
// Selected Pin Overlay (Red Marker)
|
||||||
|
// We create the element in the template (or JS) and attach it.
|
||||||
|
// Using JS creation to ensure it's cleanly managed by OpenLayers
|
||||||
|
this.selectedPinElement = document.createElement('div');
|
||||||
|
this.selectedPinElement.className = 'selected-pin-container';
|
||||||
|
|
||||||
|
// Create the icon structure inside
|
||||||
|
const pinIcon = document.createElement('div');
|
||||||
|
pinIcon.className = 'selected-pin';
|
||||||
|
// We can't use the Glimmer <Icon> component easily inside a raw DOM element created here.
|
||||||
|
// So we'll inject the SVG string directly or mount it.
|
||||||
|
// Feather icons are globally available if we used the script, but we are using the module approach.
|
||||||
|
// 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>`;
|
||||||
|
|
||||||
|
const pinShadow = document.createElement('div');
|
||||||
|
pinShadow.className = 'selected-pin-shadow';
|
||||||
|
|
||||||
|
this.selectedPinElement.appendChild(pinIcon);
|
||||||
|
this.selectedPinElement.appendChild(pinShadow);
|
||||||
|
|
||||||
|
this.selectedPinOverlay = new Overlay({
|
||||||
|
element: this.selectedPinElement,
|
||||||
|
positioning: 'bottom-center', // Important: Pin tip is at the bottom
|
||||||
|
stopEvent: false, // Let clicks pass through
|
||||||
|
});
|
||||||
|
this.mapInstance.addOverlay(this.selectedPinOverlay);
|
||||||
|
|
||||||
// Geolocation Pulse Overlay
|
// Geolocation Pulse Overlay
|
||||||
this.locationOverlayElement = document.createElement('div');
|
this.locationOverlayElement = document.createElement('div');
|
||||||
this.locationOverlayElement.className = 'search-pulse blue';
|
this.locationOverlayElement.className = 'search-pulse blue';
|
||||||
@@ -312,6 +344,28 @@ export default class MapComponent extends Component {
|
|||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track the selected place from the UI Service (Router -> Map)
|
||||||
|
updateSelectedPin = modifier(() => {
|
||||||
|
const selected = this.mapUi.selectedPlace;
|
||||||
|
|
||||||
|
if (!this.selectedPinOverlay || !this.selectedPinElement) return;
|
||||||
|
|
||||||
|
if (selected && selected.lat && selected.lon) {
|
||||||
|
const coords = fromLonLat([selected.lon, selected.lat]);
|
||||||
|
this.selectedPinOverlay.setPosition(coords);
|
||||||
|
|
||||||
|
// Reset animation by removing/adding class
|
||||||
|
this.selectedPinElement.classList.remove('active');
|
||||||
|
// Force reflow
|
||||||
|
void this.selectedPinElement.offsetWidth;
|
||||||
|
this.selectedPinElement.classList.add('active');
|
||||||
|
} else {
|
||||||
|
this.selectedPinElement.classList.remove('active');
|
||||||
|
// Hide it effectively by moving it away or just relying on display:none in CSS
|
||||||
|
this.selectedPinOverlay.setPosition(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Re-fetch bookmarks when the version changes (triggered by parent action or service)
|
// Re-fetch bookmarks when the version changes (triggered by parent action or service)
|
||||||
updateBookmarks = modifier(() => {
|
updateBookmarks = modifier(() => {
|
||||||
// Depend on the tracked storage.savedPlaces to automatically update when they change
|
// Depend on the tracked storage.savedPlaces to automatically update when they change
|
||||||
@@ -532,6 +586,7 @@ export default class MapComponent extends Component {
|
|||||||
class="map-container"
|
class="map-container"
|
||||||
{{this.setupMap}}
|
{{this.setupMap}}
|
||||||
{{this.updateBookmarks}}
|
{{this.updateBookmarks}}
|
||||||
|
{{this.updateSelectedPin}}
|
||||||
style="position: absolute; inset: 0;"
|
style="position: absolute; inset: 0;"
|
||||||
></div>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { service } from '@ember/service';
|
|||||||
export default class PlaceRoute extends Route {
|
export default class PlaceRoute extends Route {
|
||||||
@service storage;
|
@service storage;
|
||||||
@service osm;
|
@service osm;
|
||||||
|
@service mapUi;
|
||||||
|
|
||||||
async model(params) {
|
async model(params) {
|
||||||
const id = params.place_id;
|
const id = params.place_id;
|
||||||
@@ -16,15 +17,8 @@ export default class PlaceRoute extends Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Try to find in local bookmarks
|
// 1. Try to find in local bookmarks
|
||||||
// We rely on the service maintaining the list
|
|
||||||
let bookmark = this.storage.findPlaceById(id);
|
let bookmark = this.storage.findPlaceById(id);
|
||||||
|
|
||||||
// If not found instantly, maybe wait for storage ready?
|
|
||||||
// For now assuming storage is reasonably fast or "ready" has fired.
|
|
||||||
// If we land here directly on refresh, "savedPlaces" might be empty initially.
|
|
||||||
// We could retry or wait, but simpler to fall back to OSM for now.
|
|
||||||
// Ideally, we await `storage.loadAllPlaces()` promise if it's pending.
|
|
||||||
|
|
||||||
if (bookmark) {
|
if (bookmark) {
|
||||||
console.log('Found in bookmarks:', bookmark.title);
|
console.log('Found in bookmarks:', bookmark.title);
|
||||||
return bookmark;
|
return bookmark;
|
||||||
@@ -35,6 +29,18 @@ export default class PlaceRoute extends Route {
|
|||||||
return this.loadOsmPlace(id);
|
return this.loadOsmPlace(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterModel(model) {
|
||||||
|
// Notify the Map UI to show the pin
|
||||||
|
if (model) {
|
||||||
|
this.mapUi.selectPlace(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
// Clear the pin when leaving the route
|
||||||
|
this.mapUi.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
async loadOsmPlace(id, type = null) {
|
async loadOsmPlace(id, type = null) {
|
||||||
try {
|
try {
|
||||||
const poi = await this.osm.getPoiById(id, type);
|
const poi = await this.osm.getPoiById(id, type);
|
||||||
|
|||||||
14
app/services/map-ui.js
Normal file
14
app/services/map-ui.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class MapUiService extends Service {
|
||||||
|
@tracked selectedPlace = null;
|
||||||
|
|
||||||
|
selectPlace(place) {
|
||||||
|
this.selectedPlace = place;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.selectedPlace = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -248,3 +248,67 @@ span.icon {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Selected Pin Animation */
|
||||||
|
.selected-pin-container {
|
||||||
|
position: absolute;
|
||||||
|
/* Center the bottom tip of the pin at the coordinate */
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
pointer-events: none; /* Let clicks pass through to the map features below if needed */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-pin-container.active {
|
||||||
|
display: block;
|
||||||
|
animation: dropIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-pin {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
color: #ea4335; /* Google Red */
|
||||||
|
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-pin svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
fill: #ea4335;
|
||||||
|
stroke: #b31412; /* Darker red stroke */
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Small dot at the bottom to ground it */
|
||||||
|
.selected-pin-shadow {
|
||||||
|
width: 10px;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
animation: shadowFade 0.5s 0.2s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropIn {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -200%) scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -100%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shadowFade {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user