Da un marker leaflet alla foto in una galleria fotografica e ritorno
Quando vado a fare escursioni spesso scatto foto dei dintorni. Per questo motivo cerco di registrare il percorso delle mie passeggiate insieme ai segnalibri con la posizione delle foto.
Al momento uso Leaflet per gestire le mappe geografiche e PhotoSwipe per le gallerie fotografiche. Dal punto di vista tecnico visualizzo sulla mappa:
- i geojson contenenti le tracce gps delle escursioni
- per ogni foto i segnalibri con le coordinate delle foto a cui è connesso un popup contenente una thumbnail della foto ed un collegamento all'immagine nella photogallery corrispondente
Inoltre in questo caso personalizzo la galleria photoswipe
aggiungendo alcuni elementi html custom dell'interfaccia grafica (un'icona SVG che rimanda al segnalibro della foto all'interno della mappa con una funzione onClick()
custom).
Questi componenti possono lavorare insieme grazie ad uno stato globale gestito da pinia
. Per prima cosa ho creato lo store contenente lo stato globale:
// .vitepress/store/stores.ts
import { defineStore } from "pinia";
const mapStore = defineStore('map-store', {
state: () => {
return {
closePopup: Boolean,
markers: [],
selectedPopupCoordinate: Array,
selectedPopupId: Number,
selectedPopupIdWithCoordinates: Number,
selectedImageIndex: Number,
}
}
})
export { mapStore }
Definiamo inoltre due componenti vue: GalleryComponent.vue
e MapComponent.vue
.
1. Come aprire la foto selezionata all'interno della galleria fotografica facendo clic sul popup del marker leaflet
Ogni popup leaflet contiene un collegamento che, al clic, aggiorna lo store usando l'ID della foto selezionata (ovvero l'ID del marker corrispondente). La parte difficile è stata connettere la funzione che aggiorna lo store al HTMLAnchorElement a
contenuto nel popup. Per superare l'ostacolo ho creato una funzione wrapper getPopup()
che prima crea gli elementi HTML necessari, fra cui anche l'elemento HTML a
con il metodo custom onClick()
di cui sopra (righe 17-23):
// MapComponent.vue
// define the pinia state store
if (inBrowser && localStore == null) {
localStore = photoStore();
}
/* ... */
function getPopup(id, titleContent, urlthumb): HTMLDivElement {
// manually build html elements to set global pinia state from here
const title: HTMLSpanElement = document.createElement("span")
title.innerHTML = `${titleContent}`
const a: HTMLAnchorElement = document.createElement("a");
a.id = `popup-a-${id}`
// this action opens the selected photo within the photo gallery and close this marker popup
a.onClick = function eventClick(event) {
event.preventDefault()
localStore.$patch({
selectedImageIndex: id,
closePopup: true
})
}
a.appendChild(title)
const div: HTMLDivElement = document.createElement("div");
div.appendChild(a)
return div
}
Ovviamente il componente Vue dovrebbe contenere anche un modo per leggere lo store aggiornato da cui estrarre la variabile selectedImageIndex
con cui aprire poi l'immagine nella galleria fotografica:
// GalleryComponent.vue
let localMapStore;
if (inBrowser && localMapStore == null) {
localMapStore = mapStore();
localMapStore.$subscribe((mutation, state) => {
const {payload} = mutation;
const {selectedImageIndex} = payload;
// open the selected photo within the photo gallery
if (selectedImageIndex != undefined) {
handleGalleryOpen(selectedImageIndex)
}
})
}
/* ... */
const handleGalleryOpen = (index) => {
// from https://github.com/hzpeng57/vue-preview-imgs/blob/master/packages/example/src/App.vue
lightbox.loadAndOpen(parseInt(index, 10));
};
2. Andando indietro: da una foto all'interno della galleria fotografica al corrispondente indicatore popup
Quando un utente fa clic sul collegamento all'interno del popup per aprire la foto corrispondente, la sequenza di azioni è semplice: prima facendo clic sul collegamento nel popup si aggiorna lo store pinia
. In seguito l'istanza dello store con il metodo .$subscribe({...})
apre la foto selezionata all'interno della galleria fotografica. Facile.
Il processo inverso, però, è più complicato: la mappa leaflet
non può aprire il popup senza aver preventivamente impostato la view corrispondente alle coordinate del marker selezionato. La galleria fotografica però ha soltanto il dato relativo al ID dell'immagine stessa attualmente aperta (righe 24-29):
onMounted(() => {
const galleryDiv: HTMLElement | null = document.getElementById(`gallery-photo-${props.galleryID}`)
const galleryChildren: HTMLCollection | undefined = galleryDiv?.children
const dataSource = {
gallery: galleryDiv,
items: galleryChildren
}
const options = {
gallery: `#gallery-photo-${props.galleryID}`,
children: 'a',
pswpModule: () => import('photoswipe'),
dataSource: dataSource // fix missing gallery on first load with custom lightbox.loadAndOpen() action
}
if (lightbox != new PhotoSwipeLightbox({})) {
lightbox = new PhotoSwipeLightbox(options);
lightbox.on('uiRegister', function () {
lightbox.pswp.ui.registerElement({
name: 'location-button',
order: 8,
isButton: true,
tagName: 'a',
html: '<svg code ... />',
// onClick function for the custom position button within GalleryComponent.vue, onMount() hook
onClick: function (event, el, pswp) {
localMapStore.$patch({
selectedPopupIdFromGallery: parseInt(pswp.currSlide.index, 10)
})
pswp.close()
}
});
});
lightbox.init();
}
})
Notare la riga 12: si tratta di un workaround necessario per evitare che il contenuto della galleria sia mancante al primo caricamento custom (in questo caso usando lightbox.loadAndOpen()
) della galleria.
A causa delle coordinate mancanti del marker nella fase precedente ho aggiunto un passaggio intermedio si filtrano i marker estratti dallo store per ricavare le coordinate del marker selezionato:
// GalleryComponent.vue
import { LatLngTuple } from "leaflet";
// ...
let localMapStore;
if (inBrowser && localMapStore == null) {
localMapStore = mapStore();
localMapStore.$subscribe((mutation, state) => {
const {payload} = mutation;
const {selectedPopupIdFromGallery} = payload;
// filter the markers to select the marker coordinates used to set the marker map view
let {markers} = state;
let selectedMarkers: [] = markers[props.galleryID]
if (selectedPopupIdFromGallery != undefined && selectedMarkers != undefined) {
// m.id: number type!
let filteredMarker = selectedMarkers.find(m => m.id == selectedPopupIdFromGallery)
const coordinate: LatLngTuple = filteredMarker.coordinate
localMapStore.$patch({
selectedPopupCoordinate: coordinate,
selectedPopupIdWithCoordinates: filteredMarker.id
})
}
})
}
Si noti che in questo caso gli oggetti dentro all'array markers
conterranno almeno id
(tipo numero) e coordinate
(LatLngTuple
dalla libreria leaflet
). La fase successiva usa il contenuto del payload dello store (selectedPopupIdWithCoordinates
e selectedPopupCoordinate
in particolare) per impostare la visualizzazione corrente della mappa in cui si trova il segnalibro selezionato ed aprirne quindi finalmente il popup:
// MapComponent.vue
const zoomValue = 18
const popupContent = getPopup(/* ... */)
let popup = L.popup(m.coordinate).setContent(popupContent)
const marker = L.marker(coordinate, {/* ... */}).bindPopup(popup);
// here add the current marker to the marker cluster instance...
localMapStore.$subscribe((mutation, state) => {
const {payload} = mutation;
const {closePopup, selectedPopupIdWithCoordinates, selectedPopupCoordinate} = payload;
if (selectedPopupIdWithCoordinates == m.idx && selectedPopupCoordinate) {
// m.id: number type!
map.setView(selectedPopupCoordinate, zoomValue)
marker.openPopup()
}
if (closePopup) {
marker.closePopup()
}
})
// ...
E... that's the way you do it!