Show map pin for currently selected place
This commit is contained in:
parent
25f50f9091
commit
c61c2c0e7a
@ -16,15 +16,19 @@ import Geolocation from 'ol/Geolocation.js';
|
||||
import { Style, Circle, Fill, Stroke } from 'ol/style.js';
|
||||
import { apply } from 'ol-mapbox-style';
|
||||
import { getDistance } from '../utils/geo';
|
||||
import Icon from '../components/icon';
|
||||
|
||||
export default class MapComponent extends Component {
|
||||
@service osm;
|
||||
@service storage;
|
||||
@service mapUi;
|
||||
|
||||
mapInstance;
|
||||
bookmarkSource;
|
||||
searchOverlay;
|
||||
searchOverlayElement;
|
||||
selectedPinOverlay;
|
||||
selectedPinElement;
|
||||
|
||||
setupMap = modifier((element) => {
|
||||
if (this.mapInstance) return;
|
||||
@ -105,6 +109,34 @@ export default class MapComponent extends Component {
|
||||
});
|
||||
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
|
||||
this.locationOverlayElement = document.createElement('div');
|
||||
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)
|
||||
updateBookmarks = modifier(() => {
|
||||
// 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"
|
||||
{{this.setupMap}}
|
||||
{{this.updateBookmarks}}
|
||||
{{this.updateSelectedPin}}
|
||||
style="position: absolute; inset: 0;"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
@ -4,6 +4,7 @@ import { service } from '@ember/service';
|
||||
export default class PlaceRoute extends Route {
|
||||
@service storage;
|
||||
@service osm;
|
||||
@service mapUi;
|
||||
|
||||
async model(params) {
|
||||
const id = params.place_id;
|
||||
@ -16,15 +17,8 @@ export default class PlaceRoute extends Route {
|
||||
}
|
||||
|
||||
// 1. Try to find in local bookmarks
|
||||
// We rely on the service maintaining the list
|
||||
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) {
|
||||
console.log('Found in bookmarks:', bookmark.title);
|
||||
return bookmark;
|
||||
@ -35,6 +29,18 @@ export default class PlaceRoute extends Route {
|
||||
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) {
|
||||
try {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user