diff --git a/src/components/project.vue b/src/components/project.vue index 04fee38..b771f61 100644 --- a/src/components/project.vue +++ b/src/components/project.vue @@ -37,9 +37,10 @@ export default { currProject: null, modeHisto: null, posts: [], - baseMaps: {}, + baseMaps: [], baseMap: null, map: null, + mapInitializing: false, lightbox: null, hikes: { colors: {}, @@ -110,37 +111,40 @@ export default { 'hitchhiking': this.getStyleProperty('--track-hitchhiking') }; + //Reset values + this.setFeedUpdateTimer(-1); + this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true}; + this.posts = []; + this.track = null; + this.currProject = null; + this.removeMapContent(); + + //Build Map + this.mapInitializing = true; if(this.hash.items[0] && this.projects[this.hash.items[0]]) await this.initProject(this.hash.items[0]); - else await this.initProjectOverview(); + else await this.initOverview(); + this.mapInitializing = false; }, quit() { this.lightbox.end(); this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll); this.setFeedUpdateTimer(-1); - this.map.remove(); + this.removeMap(); }, - async initProjectOverview() { + async initOverview() { + this.modeHisto = true; this.hash.items = [this.overview.codename]; - this.setFeedUpdateTimer(-1); - this.currProject = null; - this.modeHisto = null; - this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true}; - this.posts = []; - this.baseMaps = this.consts.default_maps; + this.toggleFeedPanel(false, 'none'); - await this.initMapOverview(); + await this.initOverviewMap(); }, async initProject(iProjectId) { - this.setFeedUpdateTimer(-1); this.currProject = this.projects[iProjectId]; 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.baseMaps = {}; await Promise.all([ this.initFeed(), - this.initMapProject() + this.initProjectMap() ]); //Direct link post action @@ -183,9 +187,6 @@ export default { this.toggleFeedPanel(!this.isMobile(), 'none'); - //Add post Event handling - //TODO - //Get first posts batch await this.getNextFeed(); if(this.$refs.feedSimpleBar) this.$refs.feedSimpleBar.scrollElement.scrollTop = 0; @@ -193,8 +194,7 @@ export default { //Start auto-update if(!this.modeHisto) this.setFeedUpdateTimer(this.refreshRate); }, - async initMapProject() { - //Start async calls + async initProjectMap() { [ { maps: this.baseMaps, @@ -207,25 +207,26 @@ export default { this.api.get('geojson', {id_project: this.currProject.id}) ]); - await this.initMapBase({ + await this.initMap({ setCamera: () => { this.map.fitBounds(this.getInitialMapBounds(), { padding: 20, animate: false, maxZoom: 15 }); - }, - addMarkers: () => { - this.addTrack(this.track); - this.markers.forEach(oMarker => this.addMarker( - oMarker, - () => this.openMarkerPopup(oMarker.id, oMarker.type) - )); } }); }, - async initMapOverview() { - await this.initMapBase({ + async initOverviewMap() { + this.baseMaps = this.consts.default_maps; + this.markers = Object.values(this.projects).map((asProject) => ({ + type: 'project', + subtype: 'project', + ...asProject, + opacityWhenCovered: 0.3 + })); + + await this.initMap({ setCamera: () => { //Center on default project const oDefaultProject = this.projects.getDefaultProject(); @@ -239,25 +240,30 @@ export default { center: new LngLat(oDefaultProject.longitude, oDefaultProject.latitude), zoom: Math.log2(iWorldSize / this.map.transform.tileSize) }); - }, - addMarkers: () => { - for(const asProject of Object.values(this.projects)) { - this.addMarker( - { - type: 'project', - subtype: 'project', - ...asProject, - opacityWhenCovered: 0.3 - }, - () => {this.hash.items = [asProject.codename];}, - (oEvent, oProject) => {this.openProjectPopup(oProject);} - ); - } } }); }, - async initMapBase({setCamera, addMarkers}) { - if(this.map) this.map.remove(); + async initMap({setCamera}) { + //Build map + if(!this.map) this.addMap(); + this.updateMapPadding(); + setCamera(); + + //Force wait for load event + await new Promise((resolve) => { + if(this.map.isStyleLoaded()) resolve(); + else this.map.once('load', resolve); + }); + + //Add content: Base Maps, Tracks, Markers + this.addMapContent(); + + await new Promise((resolve) => { + if(this.map.loaded() && this.map.areTilesLoaded()) resolve(); + else this.map.once('idle', resolve); + }); + }, + addMap() { this.map = new Map({ container: 'map', aroundCenter: true, @@ -275,43 +281,49 @@ export default { }, attributionControl: false }); - - this.updateMapPadding(); - setCamera(); this.map.addControl(new ScaleControl({unit: 'metric'}), 'bottom-right'); - - //Force wait for load event - await new Promise((resolve) => { - if(this.map.loaded()) resolve(); - else this.map.once('load', resolve); + }, + removeMap() { + this.removeMapContent(); + this.map?.remove(); + this.map = null; + }, + addMapContent() { + this.baseMaps.forEach(this.addBaseMap); + this.addTrack(); + this.markers.forEach(this.addMarker); + }, + removeMapContent() { + if(!this.map) return; + + this.closePopup(); + this.removeTrack(); + this.markers.forEach(this.removeMarker); + this.baseMaps.forEach(this.removeBaseMap); + }, + addBaseMap(asBaseMap) { + if(asBaseMap.default_map) this.baseMap = asBaseMap.codename; + this.map.addSource(asBaseMap.codename, { + type: 'raster', + tiles: [asBaseMap.pattern], + tileSize: asBaseMap.tile_size }); - - //Base maps (raster tiles) - this.baseMap = this.baseMaps.find((asBM) => asBM.default_map)?.codename ?? null; - for(const asBaseMap of this.baseMaps) { - this.map.addSource(asBaseMap.codename, { - type: 'raster', - tiles: [asBaseMap.pattern], - tileSize: asBaseMap.tile_size - }); - this.map.addLayer({ - id: asBaseMap.codename, - type: 'raster', - source: asBaseMap.codename, - 'layout': {'visibility': asBaseMap.codename == this.baseMap ? 'visible' : 'none'}, - minZoom: asBaseMap.min_zoom, - maxZoom: asBaseMap.max_zoom - }); - } - - addMarkers(); - await new Promise((resolve) => { - if(this.map.loaded() && this.map.areTilesLoaded()) resolve(); - else this.map.once('idle', resolve); + this.map.addLayer({ + id: asBaseMap.codename, + type: 'raster', + source: asBaseMap.codename, + 'layout': {'visibility': asBaseMap.default_map?'visible':'none'}, + minZoom: asBaseMap.min_zoom, + maxZoom: asBaseMap.max_zoom }); }, - addTrack(oTrack, bCenter=false) { - this.track = oTrack; + removeBaseMap(asBaseMap) { + if(this.map.getLayer(asBaseMap.codename)) this.map.removeLayer(asBaseMap.codename); + if(this.map.getSource(asBaseMap.codename)) this.map.removeSource(asBaseMap.codename); + }, + addTrack() { + if(!this.track) return; + this.track.features.forEach((oFeature, iFeatureId) => { oFeature.properties.track_id = iFeatureId; }); @@ -354,11 +366,71 @@ export default { } }); this.map.on('click', 'track-hitbox', this.openTrackPopup); - this.map.on('mouseenter', 'track-hitbox', () => {this.map.getCanvas().style.cursor = 'pointer';}); - this.map.on('mouseleave', 'track-hitbox', () => {this.map.getCanvas().style.cursor = '';}); + this.map.on('mouseenter', 'track-hitbox', this.onTrackHover); + this.map.on('mouseleave', 'track-hitbox', this.onTrackHover); + }, + removeTrack() { + //Over clickable track + if(this.map.getLayer('track-hitbox')) { + this.map.off('click', 'track-hitbox', this.openTrackPopup); + this.map.off('mouseenter', 'track-hitbox', this.onTrackHover); + this.map.off('mouseleave', 'track-hitbox', this.onTrackHover); + this.map.removeLayer('track-hitbox'); + } + + //Actual track + if(this.map.getLayer('track')) this.map.removeLayer('track'); + + //Track source + if(this.map.getSource('track')) this.map.removeSource('track'); + }, + addMarker(oMarker) { + const $Marker = document.createElement('div'); + oMarker.app = createApp(SpotIconStack, this.markerProps[oMarker.subtype]); + oMarker.app.mount($Marker); + + oMarker.marker = new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: oMarker.opacityWhenCovered ?? 0}) + .setLngLat([oMarker.longitude, oMarker.latitude]) + .addTo(this.map); + + const $MarkerElement = oMarker.marker.getElement(); + $MarkerElement.addEventListener('click', (oEvent) => {this.onMarkerClick(oEvent, oMarker);}); + $MarkerElement.addEventListener('mouseenter', (oEvent) => {this.onMarkerHover(oEvent, oMarker);}); + $MarkerElement.addEventListener('mouseleave', (oEvent) => {this.onMarkerHover(oEvent, oMarker);}); + }, + removeMarker(oMarker) { + if(oMarker.app) { + oMarker.app.unmount(); + delete oMarker.app; + } + if(oMarker.marker) { + oMarker.marker.remove(); + delete oMarker.marker; + } + }, + onTrackHover(oEvent) { + this.map.getCanvas().style.cursor = (oEvent.type == 'mouseenter')?'pointer':''; + }, + onMarkerClick(oEvent, oMarker) { + oEvent.preventDefault(); + oEvent.stopPropagation(); + switch (oMarker.type) { + case 'project': + this.hash.items = [oMarker.codename]; + break; + default: + this.openMarkerPopup(oMarker.id, oMarker.type); + } + }, + onMarkerHover(oEvent, oMarker) { + switch (oMarker.type) { + case 'project': + if(oEvent.type == 'mouseenter') this.openProjectPopup(oMarker); + else this.closePopup(); + break; + } }, openProjectPopup(oProject) { - this.closePopup(); this.openPopup({ lnglat: [oProject.longitude, oProject.latitude], options: oProject, @@ -366,7 +438,6 @@ export default { }); }, openMarkerPopup(iMarkerId, sMarkerType) { - this.closePopup(); let oMarker = this.markers.find((oCandidate) => oCandidate.id == iMarkerId && oCandidate.type == sMarkerType); this.openPopup({ lnglat: [oMarker.longitude, oMarker.latitude], @@ -375,13 +446,13 @@ export default { }); }, openTrackPopup(oEvent) { - this.closePopup(); this.openPopup({ lnglat: oEvent.lngLat, options: this.projects.getTrackInfo(oEvent.features[0], this.track, this.lang), }); }, openPopup({lnglat, options={}, offset=[0, 0]} = {}) { + this.closePopup(); const $Popup = document.createElement('div'); this.popup.element = new Popup({ anchor: 'bottom', @@ -412,32 +483,6 @@ export default { this.popup.element = null; } }, - addMarker(oMarker, fClickCallback=null, fHoverCallback=null) { - const $Marker = document.createElement('div'); - createApp(SpotIconStack, this.markerProps[oMarker.subtype]).mount($Marker); - - const $MarkerElement = new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: oMarker.opacityWhenCovered ?? 0}) - .setLngLat([oMarker.longitude, oMarker.latitude]) - .addTo(this.map) - .getElement(); - - if(fClickCallback) { - $MarkerElement.addEventListener('click', (oEvent) => { - oEvent.preventDefault(); - oEvent.stopPropagation(); - fClickCallback(oEvent, oMarker); - }); - } - - if(fHoverCallback) { - $MarkerElement.addEventListener('mouseenter', (oEvent) => { - fHoverCallback(oEvent, oMarker); - }); - $MarkerElement.addEventListener('mouseleave', () => { - this.closePopup(); - }); - } - }, getInitialMapBounds() { let oBounds = new LngLatBounds(); let oHashMarker; @@ -551,10 +596,7 @@ export default { //Add new Markers if(aoMarkers.length > 0) { this.markers.push(...aoMarkers); - aoMarkers.forEach(oMarker => this.addMarker( - oMarker, - () => this.openMarkerPopup(oMarker.id, oMarker.type) - )); + aoMarkers.forEach(this.addMarker); } //Message Last Update @@ -671,7 +713,7 @@ export default {

- +
- +
-
+
-
- -
+
{{ activeTimeInterval }}
diff --git a/src/components/projectPost.vue b/src/components/projectPost.vue index 52dffc3..4bd9825 100644 --- a/src/components/projectPost.vue +++ b/src/components/projectPost.vue @@ -62,7 +62,7 @@ return '#'+[this.hash.page, this.project.currProject.codename, this.options.type, this.options.id].join(this.consts.hash_sep); }, modeHisto() { - return (this.project.currProject.mode == this.consts.modes.histo); + return (this.project?.currProject?.mode == this.consts.modes.histo); }, relTime() { return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time; diff --git a/src/scripts/icons.js b/src/scripts/icons.js index 7bceb6e..8a79665 100644 --- a/src/scripts/icons.js +++ b/src/scripts/icons.js @@ -1,7 +1,6 @@ import { faArrowsRotate, faBars, - faCalendar, faCamera, faCarSide, faChartArea, @@ -120,7 +119,6 @@ const ICONS = { image: faImage, message: faLocationPin, time: faClock, - calendar: faCalendar, coords: faCompass, altitude: faMountain, 'drill-video': faCirclePlay, diff --git a/src/styles/_page.project.feed.scss b/src/styles/_page.project.feed.scss index 7a7a2ae..24be981 100644 --- a/src/styles/_page.project.feed.scss +++ b/src/styles/_page.project.feed.scss @@ -141,7 +141,6 @@ } } } - &.message { background: color.$message-bg; diff --git a/src/styles/_page.project.map.scss b/src/styles/_page.project.map.scss index 2742986..4560569 100644 --- a/src/styles/_page.project.map.scss +++ b/src/styles/_page.project.map.scss @@ -96,23 +96,25 @@ $thumbnail-max-size: 60px; } } - .message .medias-list { - display: flex; - flex-wrap: wrap; - gap: var.$elem-spacing; - align-items: center; - margin-top: var.$elem-spacing; + .message { + .medias-list { + display: flex; + flex-wrap: wrap; + gap: var.$elem-spacing; + align-items: center; + margin-top: var.$elem-spacing; - a.media-link { - flex: 0 0 auto; + a.media-link { + flex: 0 0 auto; - img { - max-width: $thumbnail-max-size; - max-height: calc($thumbnail-max-size * 2/3); - } + img { + max-width: $thumbnail-max-size; + max-height: calc($thumbnail-max-size * 2/3); + } - &.drill .drill-icon { - font-size: 1.5em; + &.drill .drill-icon { + font-size: 1.5em; + } } } } @@ -124,5 +126,13 @@ $thumbnail-max-size: 60px; } } } + + .project { + .year { + text-align: center; + font-size: 1.2em; + font-weight: bold; + } + } } } \ No newline at end of file