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