diff --git a/.gitignore b/.gitignore
index bb57409..a55e99e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
/node_modules/
/log.html
/dist/
+.codex
\ No newline at end of file
diff --git a/build/webpack.common.js b/build/webpack.common.js
index 463dfda..65602bb 100644
--- a/build/webpack.common.js
+++ b/build/webpack.common.js
@@ -82,7 +82,6 @@ module.exports = {
from: path.resolve(LIB, 'index.php'),
to: 'index.php'
},
- { from: 'src/images/footprint_mapbox.png', to: 'images' },
{ from: 'src/images/logo_black.png', to: 'images' },
{ from: 'src/images/spot-logo-only.svg', to: 'images' }
],
diff --git a/lib/Spot.php b/lib/Spot.php
index bbd0342..121c2b5 100755
--- a/lib/Spot.php
+++ b/lib/Spot.php
@@ -280,6 +280,12 @@ class Spot extends Main
usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
$bHasMsg = !empty($asMessages);
+ foreach($asMessages as &$asMessage) {
+ $asMessage['id'] = $asMessage[Db::getId(Feed::MSG_TABLE)];
+ $asMessage['type'] = 'message';
+ $asMessage['subtype'] = 'message';
+ }
+
//Add medias
$asMedias = $this->getMedias('taken_on', $asMediaIds);
usort($asMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
@@ -289,11 +295,13 @@ class Spot extends Main
$iMaxIndex = count($asMessages) - 1;
foreach($asMedias as $asMedia) {
if($asMedia['latitude']!='' && $asMedia['longitude']!='') {
+ $asMedia['id'] = $asMedia[Db::getId(Media::MEDIA_TABLE)];
+ $asMedia['type'] = 'media';
$asMedia['lat_dms'] = self::decToDms($asMedia['latitude'], 'lat');
$asMedia['lon_dms'] = self::decToDms($asMedia['longitude'], 'lon');
$asGeoMedias[] = $asMedia;
}
- elseif($bHasMsg) {
+ if($bHasMsg) {
while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) $iIndex++;
//All medias before first message or after last message are assigned to first/last message respectively
@@ -308,13 +316,15 @@ class Spot extends Main
}
}
+ $asMarkers = [...$asMessages, ...$asGeoMedias];
+ usort($asMarkers, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
+
//Spot Last Update
$asLastUpdate = array();
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
$asResult = array(
- 'messages' => $asMessages,
- 'medias' => $asGeoMedias,
+ 'markers' => $asMarkers,
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId()),
'last_update' => $asLastUpdate
);
diff --git a/src/components/project.vue b/src/components/project.vue
index e169473..71e0979 100644
--- a/src/components/project.vue
+++ b/src/components/project.vue
@@ -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;
- }
}
}
}
diff --git a/src/components/projectMediaLink.vue b/src/components/projectMediaLink.vue
index dd4aea0..7115f62 100644
--- a/src/components/projectMediaLink.vue
+++ b/src/components/projectMediaLink.vue
@@ -11,6 +11,7 @@ export default {
options: Object,
type: String
},
+ emits: ['opening-lightbox'],
data() {
return {
title:''
@@ -41,6 +42,7 @@ export default {
:data-id="options.id_media"
:data-title="title"
:data-orientation="options.rotate"
+ @click="$emit('opening-lightbox', $event)"
ref="link"
>
-
-