From 49f37465bd9794930168cad618923f35ee404c2b Mon Sep 17 00:00:00 2001 From: Franzz Date: Wed, 13 May 2026 23:28:36 +0200 Subject: [PATCH] Pick project from globe --- config/db/update_v23_to_v24.sql | 2 + lib/Converter.php | 5 +- lib/GeoJson.php | 15 ++- lib/Project.php | 8 +- lib/Spot.php | 9 +- src/components/project.vue | 161 ++++++++++++++++--------- src/scripts/projects.js | 1 - src/styles/_page.project.scss | 24 ++-- src/styles/_page.project.settings.scss | 4 + 9 files changed, 151 insertions(+), 78 deletions(-) create mode 100644 config/db/update_v23_to_v24.sql diff --git a/config/db/update_v23_to_v24.sql b/config/db/update_v23_to_v24.sql new file mode 100644 index 0000000..c54bab6 --- /dev/null +++ b/config/db/update_v23_to_v24.sql @@ -0,0 +1,2 @@ +ALTER TABLE projects ADD latitude DECIMAL(8,6) AFTER name; +ALTER TABLE projects ADD longitude DECIMAL(9,6) AFTER latitude; \ No newline at end of file diff --git a/lib/Converter.php b/lib/Converter.php index 13ec19d..b5fe7cb 100644 --- a/lib/Converter.php +++ b/lib/Converter.php @@ -29,7 +29,10 @@ class Converter extends PhpObject { $oGeoJson->sortOffTracks(); $oGeoJson->saveFile(); - return $oGpx->getLog().'
'.$oGeoJson->getLog(); + return [ + 'logs' => $oGpx->getLog().'
'.$oGeoJson->getLog(), + 'center' => $oGeoJson->getCenter() + ]; } public static function isGeoJsonValid($sCodeName) { diff --git a/lib/GeoJson.php b/lib/GeoJson.php index 4614d39..8c9ccac 100644 --- a/lib/GeoJson.php +++ b/lib/GeoJson.php @@ -103,7 +103,6 @@ class GeoJson extends Geo { if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)'); } - public function sortOffTracks() { $this->addNotice('Sorting off-tracks'); @@ -155,7 +154,19 @@ class GeoJson extends Geo { $this->asTracks = array_values($asTracks); } - private function parseOptions($sComment){ + public function getCenter() { + $asCoords = array(); + $asMainTracks = array_filter($this->asTracks, function ($astrack) {return $astrack['properties']['type'] == 'main';}); + foreach($asMainTracks as $asMainTrack) { + foreach($asMainTrack['geometry']['coordinates'] as $aiCoords) { + $asCoords[] = $aiCoords; + } + } + + return $asCoords[(int) floor(count($asCoords) / 2)]; + } + + private function parseOptions($sComment) { $sComment = strip_tags(html_entity_decode($sComment)); $asOptions = array(self::OPT_SIMPLE=>''); foreach(explode("\n", $sComment) as $sLine) { diff --git a/lib/Project.php b/lib/Project.php index 04754de..0ddad32 100644 --- a/lib/Project.php +++ b/lib/Project.php @@ -126,6 +126,8 @@ class Project extends PhpObject { Db::getId(self::PROJ_TABLE)." AS id", 'codename', 'name', + 'latitude', + 'longitude', 'active_from', 'active_to', "IF(NOW() BETWEEN active_from AND active_to, 1, IF(NOW() < active_from, 0, 2)) AS mode" @@ -147,12 +149,16 @@ class Project extends PhpObject { $asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getDistFilePath($sCodeName)); $asProject['codename'] = $sCodeName; $asProject['default'] = ($sCodeName == $sDefaultProjectCodeName); + //$asProject['center'] = [$asProject['latitude'], $asProject['longitude']]; } return $bSpecificProj?$asProject:$asProjects; } public function getGeoJson() { - if($this->sCodeName != '' && !Converter::isGeoJsonValid($this->sCodeName)) Converter::convertToGeoJson($this->sCodeName); + if($this->sCodeName != '' && !Converter::isGeoJsonValid($this->sCodeName)){ + $aiCenter = Converter::convertToGeoJson($this->sCodeName)['center']; + $this->oDb->updateRow(self::PROJ_TABLE, $this->iProjectId, ['latitude' => $aiCenter[1], 'longitude' => $aiCenter[0]]); + } return json_decode(file_get_contents(GeoJson::getDistFilePath($this->sCodeName)), true); } diff --git a/lib/Spot.php b/lib/Spot.php index f5294f8..f1afeed 100755 --- a/lib/Spot.php +++ b/lib/Spot.php @@ -173,12 +173,13 @@ class Spot extends Main return parent::getMainPage( array( - 'projects' => $this->oProject->getProjects(), - 'user' => $this->oUser->getUserInfo(), - 'consts' => array( + 'projects' => $this->oProject->getProjects(), + 'user' => $this->oUser->getUserInfo(), + 'consts' => array( 'modes' => Project::MODES, 'clearances' => User::CLEARANCES, 'default_timezone' => Settings::TIMEZONE, + 'default_maps' => $this->oMap->getProjectMaps(-1), 'chunk_size' => self::FEED_CHUNK_SIZE, 'hash_sep' => '-', 'title' => 'Spotty', @@ -823,7 +824,7 @@ class Spot extends Main } public function buildGeoJSON($sCodeName) { - return Converter::convertToGeoJson($sCodeName); + return Converter::convertToGeoJson($sCodeName)['logs']; } public static function decToDms($dValue, $sType) { diff --git a/src/components/project.vue b/src/components/project.vue index 2ff8776..53eb56e 100644 --- a/src/components/project.vue +++ b/src/components/project.vue @@ -29,12 +29,13 @@ export default { track: null, markers: [], markerProps: { + project: {mainClasses: 'project', iconMain: 'marker', iconSub: 'project'}, 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, + currProject: null, + modeHisto: null, posts: [], baseMaps: {}, baseMap: null, @@ -63,8 +64,8 @@ export default { } }, 'hash.items.0'(newProjectCodename, oldProjectCodename) { - if(newProjectCodename && newProjectCodename != oldProjectCodename) { - this.hash.items = [newProjectCodename]; + if(newProjectCodename != oldProjectCodename) { + this.hash.items = newProjectCodename?[newProjectCodename]:[]; this.toggleSettingsPanel(false, 'none'); this.init(); } @@ -82,9 +83,6 @@ export default { }; }, inject: ['api', 'lang', 'hash', 'projects', 'user', 'consts', 'isMobile'], - beforeMount() { - if(this.hash.items.length == 0) this.hash.items[0] = this.projects.getDefaultCodeName(); - }, mounted() { this.init(); }, @@ -93,35 +91,47 @@ export default { }, methods: { async init() { - this.initProject(); this.initLightbox(); - - await Promise.all([ - this.initFeed(), - this.initMap() - ]); - - //Direct link post action - if(this.hash.items.length == 3) await this.findPost(this.hash.items[1], this.hash.items[2]); - }, - quit() { - this.lightbox.end(); - this.$refs.feedSimpleBar.scrollElement.removeEventListener('scroll', this.onFeedScroll); - this.setFeedUpdateTimer(-1); - this.map.remove(); - }, - initProject() { - this.currProject = this.projects[this.hash.items[0]]; - 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.baseMap = null; - this.baseMaps = {}; this.hikes.colors = { 'main': this.getStyleProperty('--track-main'), 'off-track': this.getStyleProperty('--track-off-track'), 'hitchhiking': this.getStyleProperty('--track-hitchhiking') }; + + if(!this.hash.items[0] || !this.projects[this.hash.items[0]]) await this.initProjectOverview(); + else await this.initProject(); + }, + quit() { + this.lightbox.end(); + this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll); + this.setFeedUpdateTimer(-1); + this.map.remove(); + }, + async initProjectOverview() { + 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; + + await this.initMapOverview(); + }, + async initProject() { + this.setFeedUpdateTimer(-1); + this.currProject = this.projects[this.hash.items[0]]; + 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() + ]); + + //Direct link post action + if(this.hash.items.length == 3) await this.findPost(this.hash.items[1], this.hash.items[2]); }, initLightbox() { if(!this.lightbox) { @@ -149,7 +159,10 @@ export default { } }, async initFeed() { + await this.$nextTick(); + //Simplebar event + this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll); this.$refs.feedSimpleBar?.scrollElement.addEventListener('scroll', this.onFeedScroll); //Mobile Touchscreen Events @@ -162,12 +175,12 @@ export default { //Get first posts batch await this.getNextFeed(); - this.$refs.feedSimpleBar.scrollElement.scrollTop = 0; + if(this.$refs.feedSimpleBar) this.$refs.feedSimpleBar.scrollElement.scrollTop = 0; //Start auto-update if(!this.modeHisto) this.setFeedUpdateTimer(this.refreshRate); }, - async initMap() { + async initMapProject() { //Start async calls [ { @@ -181,7 +194,51 @@ export default { this.api.get('geojson', {id_project: this.currProject.id}) ]); - //Build Map + await this.initMapBase({ + setCamera: () => { + this.map.fitBounds(this.getInitialMapBounds(), { + padding: 20, + animate: false, + maxZoom: 15 + }); + }, + addMarkers: () => { + this.addTrack(this.track); + this.markers.forEach(oMarker => this.addMarker(oMarker)); + } + }); + }, + async initMapOverview() { + await this.initMapBase({ + setCamera: () => { + //Center on default project + const oDefaultProject = this.projects.getDefaultProject(); + + //Adapt zoom to see whole planet + const $Canvas = this.map.getCanvas(); + const iTargetRadius = Math.min($Canvas.clientWidth, $Canvas.clientHeight) / 2; + const iWorldSize = iTargetRadius * 2 * Math.PI * Math.cos(oDefaultProject.latitude * Math.PI / 180); + + this.map.jumpTo({ + 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({ + subtype: 'project', + longitude: asProject.longitude, + latitude: asProject.latitude, + opacityWhenCovered: 0.3 + }, () => { + this.hash.items = [asProject.codename]; + }); + } + } + }); + }, + async initMapBase({setCamera, addMarkers}) { if(this.map) this.map.remove(); this.map = new Map({ container: 'map', @@ -200,17 +257,11 @@ export default { }, attributionControl: false }); + this.updateMapPadding(); - this.map.fitBounds(this.getInitialMapBounds(), { - padding: 20, - animate: false, - maxZoom: 15 - }); + setCamera(); this.map.addControl(new ScaleControl({unit: 'metric'}), 'bottom-right'); - - //Get default basemap - this.baseMap = this.baseMaps.find((asBM) => asBM.default_map)?.codename ?? null; - + //Force wait for load event await new Promise((resolve) => { if(this.map.loaded()) resolve(); @@ -218,6 +269,7 @@ export default { }); //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', @@ -233,14 +285,8 @@ export default { maxZoom: asBaseMap.max_zoom }); } - - //Add track - this.addTrack(this.track); - //Add Markers - this.markers.forEach(oMarker => this.addMarker(oMarker)); - - //Force wait for idle event + addMarkers(); await new Promise((resolve) => { if(this.map.loaded() && this.map.areTilesLoaded()) resolve(); else this.map.once('idle', resolve); @@ -300,18 +346,19 @@ export default { options: this.projects.getTrackInfo(oEvent.features[0], this.track, this.lang), }); }, - addMarker(oMarker) { + addMarker(oMarker, fClickCallback=null) { const $Marker = document.createElement('div'); createApp(SpotIconStack, this.markerProps[oMarker.subtype]).mount($Marker); - new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: 0}) + new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: oMarker.opacityWhenCovered ?? 0}) .setLngLat([oMarker.longitude, oMarker.latitude]) .addTo(this.map) .getElement() .addEventListener('click', (oEvent) => { oEvent.preventDefault(); oEvent.stopPropagation(); - this.openMarkerPopup(oMarker.id, oMarker.type); + if(fClickCallback) fClickCallback(oEvent, oMarker); + else this.openMarkerPopup(oMarker.id, oMarker.type); }); }, openMarkerPopup(iMarkerId, sMarkerType) { @@ -574,7 +621,7 @@ export default {
-
+

{{ lang.get('feed.last_update')+' '+lastUpdate.relative_time }}

@@ -625,18 +672,18 @@ export default {
-
+
{{ lang.get('track.'+hikeType) }}
-
+
{{ currProject.name }}
- +
@@ -648,7 +695,7 @@ export default {
-
+
diff --git a/src/scripts/projects.js b/src/scripts/projects.js index 2179f0e..dd38bbb 100644 --- a/src/scripts/projects.js +++ b/src/scripts/projects.js @@ -13,7 +13,6 @@ export default class Projects { } getDefaultProject() { - const sCodeName = this.getDefaultCodeName(); return this[this.getDefaultCodeName()]; } diff --git a/src/styles/_page.project.scss b/src/styles/_page.project.scss index cd65c90..848637c 100644 --- a/src/styles/_page.project.scss +++ b/src/styles/_page.project.scss @@ -6,16 +6,16 @@ @use '@styles/page.project.feed' as feed; @use '@styles/page.project.settings' as settings; -#projects { - --space: #{color.$space}; - --horizon: #{color.$horizon}; - --track-main: #{color.$main-track}; - --track-off-track: #{color.$off-track}; - --track-hitchhiking: #{color.$hitchhiking}; - - overflow: hidden; - position: absolute; - top: 0; +#projects { + --space: #{color.$space}; + --horizon: #{color.$horizon}; + --track-main: #{color.$main-track}; + --track-off-track: #{color.$off-track}; + --track-hitchhiking: #{color.$hitchhiking}; + + overflow: hidden; + position: absolute; + top: 0; left: 0; width: 100%; height: 100%; @@ -56,7 +56,7 @@ filter: drop-shadow(0px 1px 1px color.$over-img-shadow); } - &.message { + &.message, &.project { .main { color: color.$message-flashy; } @@ -83,4 +83,4 @@ } } } -} +} diff --git a/src/styles/_page.project.settings.scss b/src/styles/_page.project.settings.scss index 5b701a4..175f13d 100644 --- a/src/styles/_page.project.settings.scss +++ b/src/styles/_page.project.settings.scss @@ -105,6 +105,10 @@ margin-left: var.$text-spacing; @extend .clickable; @include common.no-text-overflow(); + + &:hover { + color: color.$default-hover; + } } .download {