Harmonize marker / popup rendering

This commit is contained in:
2026-05-02 00:16:37 +02:00
parent 560b22c039
commit 95ebc96484
9 changed files with 169 additions and 232 deletions

View File

@@ -28,8 +28,14 @@ export default {
lastUpdate: { unix_time: 0, relative_time: '', formatted_time: ''},
feedPanelOpen: false,
settingsPanelOpen: false,
markers: {messages: null, medias: null},
track: null,
markers: [],
markerSize: {width: 32, height: 32},
markerProps: {
image: {mainClasses: 'media', iconMain: 'marker', iconSub: 'image'},
video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
},
currProject: {},
modeHisto: false,
posts: [],
@@ -38,6 +44,7 @@ export default {
baseMaps: {},
baseMap: null,
map: null,
mapReady: false,
hikes: {
colors:{'main':'#00ff78', 'off-track':'#0000ff', 'hitchhiking':'#FF7814'},
width: 4
@@ -49,7 +56,8 @@ export default {
projectClasses() {
return [
this.feedPanelOpen?'with-feed':'',
this.settingsPanelOpen?'with-settings':''
this.settingsPanelOpen?'with-settings':'',
this.mapReady?'map-ready':''
].filter(n => n).join(' ');
},
nlClasses() {
@@ -75,6 +83,7 @@ export default {
'hash.items.0'(newProjectCodename, oldProjectCodename) {
if(newProjectCodename && newProjectCodename != oldProjectCodename) {
this.hash.items = [newProjectCodename];
this.toggleSettingsPanel(false, 'none');
this.init();
}
}
@@ -122,8 +131,10 @@ export default {
this.initMap()
]);
//Direct link: Scroll to post
if(this.hash.items.length == 3) this.findPost({type: this.hash.items[1], id: this.hash.items[2]});
//Direct link or default project positioning
await this.setInitialMapPosition();
this.mapReady = true;
},
quit() {
lightbox.end();
@@ -152,6 +163,7 @@ export default {
this.modeHisto = (this.currProject.mode == this.consts.modes.histo);
this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true};
this.posts = [];
this.mapReady = false;
//this.baseMap = null;
this.baseMaps = {};
},
@@ -166,7 +178,7 @@ export default {
hasVideo: true,
onMediaChange: (oMedia) => {
this.hash.items = [this.currProject.codename, 'media', oMedia.id];
if(oMedia.set == 'post-medias') this.goToPost({type: 'media', id: oMedia.id});
if(oMedia.set == 'post-medias') this.goToPost('media', oMedia.id);
},
onClosing: () => {this.hash.items = [this.hash.items[0]];}
});
@@ -178,6 +190,8 @@ export default {
//Mobile Touchscreen Events
//TODO
this.toggleFeedPanel(!this.isMobile(), 'none');
//Add post Event handling
//TODO
@@ -209,8 +223,7 @@ export default {
const aoMarkers = await oMarkersPromise;
this.baseMaps = aoMarkers.maps;
this.baseMap = this.baseMaps.find((asBM) => asBM.default_map)?.codename ?? null;
this.markers.messages = aoMarkers.messages;
this.markers.medias = aoMarkers.medias;
this.markers = aoMarkers.markers;
this.lastUpdate = aoMarkers.last_update;
//Force wait for load event
@@ -237,21 +250,19 @@ export default {
}
//Add track
const oTrack = await oTrackPromise;
this.addTrack(oTrack);
//Centering map
await this.positionMap(oTrack);
this.addTrack(await oTrackPromise);
//Add Markers
this.addMarkers();
this.markers.forEach(oMarker => this.addMarker(oMarker));
//Force wait for idle event
await new Promise((resolve) => {
this.map.once('idle', resolve);
if(this.map.loaded() && this.map.areTilesLoaded()) resolve();
else this.map.once('idle', resolve);
});
},
addTrack(oTrack) {
addTrack(oTrack, bCenter=false) {
this.track = oTrack;
this.map.addSource('track', {
'type': 'geojson',
'data': oTrack
@@ -280,167 +291,37 @@ export default {
}
});
},
async addMarkers() {
this.map.addImage('markerIcon', (await this.map.loadImage('images/footprint_mapbox.png')).data);
this.map.addSource('markers', {
type:'geojson',
data: {
type: 'FeatureCollection',
features: this.convertMsgToFeatures(this.markers.messages)
}
});
this.map.addLayer({
'id': 'markers',
'type': 'symbol',
'source': 'markers',
'layout': {'icon-image': 'markerIcon'}
});
this.map.on('click', 'markers', (e) => {
this.openMarkerPopup(e.features[0]);
});
addMarker(oMarker) {
const $Marker = document.createElement('div');
createApp(SpotIconStack, this.markerProps[oMarker.subtype]).mount($Marker);
/*
this.markers.messages.forEach(msg => {
const el = document.createElement('div');
const app = createApp(SpotIconStack, {iconMain: 'message', iconSub:'message-in', iconSubRotation: 270});
app.mount(el);
new Marker({element: el, anchor: 'bottom'})
.setLngLat([msg.longitude, msg.latitude])
.addTo(this.map);
});
*/
//Medias
//TODO Use same way of displaying markers (so that openMarkerPopup works on all markers)
this.markers.medias.forEach(msg => {
const $Marker = document.createElement('div');
createApp(SpotIconStack, {mainClasses: 'media', iconMain: 'marker', iconSub: msg.subtype}).mount($Marker);
const marker = new Marker({element: $Marker, anchor: 'bottom'})
.setLngLat([msg.longitude, msg.latitude])
.addTo(this.map)
.getElement().addEventListener('click', (oEvent) => {
oEvent.preventDefault();
oEvent.stopPropagation();
this.openMediaPopup(msg);
});
});
},
async positionMap(oTrack) {
let bOpenFeedPanel = !this.isMobile();
let oBounds = new LngLatBounds();
if( //Blog Mode: Fit to last message
this.currProject.mode == this.consts.modes.blog &&
this.markers.messages.length > 0 &&
this.hash.items[2] != 'message'
) {
let oLastMsg = this.markers.messages[this.markers.messages.length - 1];
oBounds.extend(new LngLat(oLastMsg.longitude, oLastMsg.latitude));
}
else { //Pre/Histo Mode: Fit to track
for(const iFeatureId in oTrack.features) {
oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce(
(bounds, coord) => {
return bounds.extend(coord);
},
oBounds
);
}
}
const iFeedPanelPadding = bOpenFeedPanel?(getOuterWidth(this.$refs.feed)/2):0;
await this.map.fitBounds(
oBounds,
{
padding: {
top: 20,
bottom: 20,
left: (20 + iFeedPanelPadding),
right: (20 + iFeedPanelPadding)
},
animate: false,
maxZoom: 15
}
);
//Toggle only when map is ready, for the tilt effet
this.toggleFeedPanel(bOpenFeedPanel);
},
convertMsgToFeatures(oMsg) {
return oMsg.map(oMsg => ({
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [oMsg.longitude, oMsg.latitude]
},
'properties': {
...oMsg,
...{'description': ''}
}
}));
},
openMediaPopup(oMedia) {
this.closeMarkerPopup();
const $Popup = document.createElement('div');
this.popup.element = new Popup({
anchor: 'bottom',
offset: [0, this.markerSize.height * -1],
closeButton: false
})
.setDOMContent($Popup)
.setLngLat([oMedia.longitude, oMedia.latitude])
.setMaxWidth(300)
.addTo(this.map);
this.popup.content = createApp(ProjectPopup, {
type: 'media',
options: oMedia,
medias: [oMedia],
project: this.currProject
});
this.popup.content
.provide('spot', this.spot)
.provide('lang', this.lang)
.provide('consts', this.consts)
.mount($Popup);
},
openMarkerPopup(oFeature) {
this.closeMarkerPopup();
//Convert ID Message to feature
if(typeof oFeature == 'number') {
const oMatchingFeatures = this.map.querySourceFeatures('markers', {
filter: ['==', ['get', 'id_message'], oFeature]
new Marker({element: $Marker, anchor: 'bottom'})
.setLngLat([oMarker.longitude, oMarker.latitude])
.addTo(this.map)
.getElement()
.addEventListener('click', (oEvent) => {
oEvent.preventDefault();
oEvent.stopPropagation();
this.openMarkerPopup(oMarker.id, oMarker.type);
});
},
openMarkerPopup(iMarkerId, sMarkerType) {
this.closeMarkerPopup();
if(!oMatchingFeatures.length) {
console.warn('Marker not found: ', oFeature);
return;
}
else oFeature = oMatchingFeatures[0];
}
let oMarker = this.markers.find((oCandidate) => oCandidate.id == iMarkerId && oCandidate.type == sMarkerType);
const $Popup = document.createElement('div');
this.popup.element = new Popup({
anchor: 'bottom',
offset: [0, this.markerSize.height * -1],
offset: [0, this.markerSize.height * -1], //FIXME
closeButton: false
})
.setDOMContent($Popup)
.setLngLat(oFeature.geometry.coordinates)
.setMaxWidth(300)
.setLngLat([oMarker.longitude, oMarker.latitude])
.addTo(this.map);
this.popup.content = createApp(ProjectPopup, {
type: 'message',
options: oFeature.properties,
medias: JSON.parse(oFeature.properties.medias || '[]'),
options: oMarker,
project: this.currProject
});
this.popup.content
@@ -459,6 +340,75 @@ export default {
this.popup.element = null;
}
},
async setInitialMapPosition() {
if(this.hash.items.length == 3) {
await this.findPost(this.hash.items[1], this.hash.items[2]);
}
else {
let oBounds = new LngLatBounds();
if( //Blog Mode: Fit to last message
this.currProject.mode == this.consts.modes.blog &&
this.markers.length > 0
) {
let oLastMsg = this.markers.at(-1);
oBounds.extend(new LngLat(oLastMsg.longitude, oLastMsg.latitude));
}
else { //Pre/Histo Mode: Fit to track
for(const iFeatureId in this.track.features) {
oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
(bounds, coord) => {
return bounds.extend(coord);
},
oBounds
);
}
}
const iFeedPanelPadding = this.feedPanelOpen?(getOuterWidth(this.$refs.feed)):0;
await this.map.fitBounds(
oBounds,
{
padding: {
top: 20,
bottom: 20,
left: 20,
right: 20 + iFeedPanelPadding
},
animate: false,
maxZoom: 15
}
);
}
},
async findPost(sPostType, iPostId) {
let oRef = this.goToPost(sPostType, iPostId);
if(oRef) {
await oRef.executeMainAction(0);
}
else if(!this.feed.outOfData) {
await this.getNextFeed();
await this.findPost(sPostType, iPostId);
}
else console.log('Missing element ID "'+iPostId+'" of type "'+sPostType+'"');
},
goToPost(sPostType, iPostId) {
let bFound = false;
let aoRefs = this.$refs.posts.filter((post) => {return post.postId == sPostType+'-'+iPostId;});
if(aoRefs.length > 0) {
let oRef = aoRefs[0];
this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
oRef.$el.getBoundingClientRect().top
+ window.pageYOffset
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
);
//this.hash.items = [this.hash.items[0]];
return oRef;
}
},
async getNextFeed() {
if(!this.feed.outOfData && !this.feed.loading) {
//Get next chunk
@@ -504,31 +454,28 @@ export default {
}
//Add new Markers
if(Object.keys(aoData.messages).length > 0) {
const oMarkerSource = this.map.getSource('markers');
oMarkerSource.setData({
type: 'FeatureCollection',
features: [...oMarkerSource._data.features, ...this.convertMsgToFeatures(aoData.messages)]
});
if(Object.keys(aoData.markers).length > 0) {
this.markers.push(...aoData.markers);
aoData.messages.forEach(oMarker => this.addMarker(oMarker));
}
//TODO medias
//Message Last Update
this.lastUpdate = aoData.last_update;
//Reschedule
this.setFeedUpdateTimer(this.refreshRate);
},
panToBetweenPanels(oLngLat, iZoom, fCallback) {
panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
const iXOffset = (this.settingsPanelOpen?getOuterWidth(this.$refs.settings):0) - (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0);
this.map.once('moveend', fCallback);
this.map.easeTo({
center: oLngLat,
zoom: iZoom,
offset: [iXOffset / 2, 0],
duration: 500
return new Promise((resolve) => {
this.map.once('moveend', resolve);
this.map.easeTo({
center: oLngLat,
zoom: iZoom,
offset: [iXOffset / 2, 0],
duration: iAnimDuration
});
});
},
isMarkerVisible(oLngLat){
@@ -610,33 +557,6 @@ export default {
break;
}
}
},
async findPost(oPost) {
let oRef = this.goToPost(oPost);
if(oRef) {
oRef.executeMainAction();
}
else if(!this.feed.outOfData) {
await this.getNextFeed();
this.findPost(oPost);
}
else console.log('Missing element ID "'+oPost.id+'" of type "'+oPost.type+'"');
},
goToPost(oPost) {
let bFound = false;
let aoRefs = this.$refs.posts.filter((post) => {return post.postId == oPost.type+'-'+oPost.id;});
if(aoRefs.length == 1) {
let oRef = aoRefs[0];
this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
oRef.$el.getBoundingClientRect().top
+ window.pageYOffset
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
);
//this.hash.items = [this.hash.items[0]];
return oRef;
}
}
}
}