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" >