Compare commits

...

11 Commits

Author SHA1 Message Date
d9bc89b7f6 Update SNT gpx 2025-07-19 16:22:14 +02:00
760f38374f Add email trigger to manual positioning 2025-07-19 16:21:44 +02:00
b9a4bd6d2d Adapt manual message upload to vue 2025-05-12 19:48:49 +02:00
ea14a1ef3e Add manual message upload 2025-05-11 17:38:59 +02:00
457bab2c18 Convert upload page to vue 2025-05-03 20:37:15 +02:00
3571f93e41 Add track popups 2025-05-03 11:56:04 +02:00
e878b159bf Fix SNT track color 2025-05-02 21:27:39 +02:00
db70593852 Add function to rebuild GeoJSON 2025-05-02 21:26:45 +02:00
73b8e6b04f Add Build GeoJSON Catch 2025-05-02 21:19:11 +02:00
a49f73236b Update geo/snt.gpx 2025-05-02 21:18:50 +02:00
c0b7ad8000 Add SNT gpx 2025-05-02 21:18:29 +02:00
20 changed files with 69721 additions and 85 deletions

21
composer.lock generated
View File

@@ -12,7 +12,7 @@
"dist": { "dist": {
"type": "path", "type": "path",
"url": "../objects", "url": "../objects",
"reference": "a9f8601384a0078cf8531576768e2c5bad4bbf95" "reference": "bcae723140735b1432caaf3070ef4e29ecb73a76"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -27,16 +27,16 @@
}, },
{ {
"name": "phpmailer/phpmailer", "name": "phpmailer/phpmailer",
"version": "v6.8.1", "version": "v6.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git", "url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "e88da8d679acc3824ff231fdc553565b802ac016" "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e88da8d679acc3824ff231fdc553565b802ac016", "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"reference": "e88da8d679acc3824ff231fdc553565b802ac016", "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -56,6 +56,7 @@
"yoast/phpunit-polyfills": "^1.0.4" "yoast/phpunit-polyfills": "^1.0.4"
}, },
"suggest": { "suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing", "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@@ -95,7 +96,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP", "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": { "support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues", "issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.1" "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
}, },
"funding": [ "funding": [
{ {
@@ -103,7 +104,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-08-29T08:26:30+00:00" "time": "2025-04-24T15:19:31+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],
@@ -114,7 +115,7 @@
}, },
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": [], "platform": {},
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

View File

@@ -8,7 +8,7 @@ class Settings
const DB_NAME = 'spot'; const DB_NAME = 'spot';
const DB_ENC = 'utf8mb4'; const DB_ENC = 'utf8mb4';
const TEXT_ENC = 'UTF-8'; const TEXT_ENC = 'UTF-8';
const TIMEZONE = 'Europe/Paris'; const TIMEZONE = 'Europe/Zurich';
const MAIL_SERVER = ''; const MAIL_SERVER = '';
const MAIL_FROM = ''; const MAIL_FROM = '';
const MAIL_USER = ''; const MAIL_USER = '';

69304
geo/snt.gpx Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ upload_title = Picture & Video Uploads
upload_mode_archived= Project "$0" is archived. No upload allowed upload_mode_archived= Project "$0" is archived. No upload allowed
upload_success = $0 uploaded successfully upload_success = $0 uploaded successfully
upload_media_exist = Picture $0 already exists upload_media_exist = Picture $0 already exists
new_position = New Position
post_message = Message post_message = Message
post_name = Name post_name = Name

View File

@@ -24,6 +24,7 @@ upload_title = Cargar fotos y videos
upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
upload_success = $0 ha sido subido upload_success = $0 ha sido subido
upload_media_exist = La imagen $0 ya existe upload_media_exist = La imagen $0 ya existe
new_position = Nueva posición
post_message = Mensaje post_message = Mensaje
post_name = Nombre post_name = Nombre

View File

@@ -24,6 +24,7 @@ upload_title = Uploader photos & vidéos
upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
upload_success = $0 a été uploadé upload_success = $0 a été uploadé
upload_media_exist = l'image $0 existe déjà upload_media_exist = l'image $0 existe déjà
new_position = Nouvelle position
post_message = Message post_message = Message
post_name = Nom post_name = Nom

View File

@@ -33,12 +33,10 @@ class Converter extends PhpObject {
} }
public static function isGeoJsonValid($sCodeName) { public static function isGeoJsonValid($sCodeName) {
$bResult = false;
$sGpxFilePath = Gpx::getFilePath($sCodeName); $sGpxFilePath = Gpx::getFilePath($sCodeName);
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName); $sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
//No need to generate if gpx is missing //No need to generate if gpx is missing
if(!file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Gpx::getFilePath($sCodeName))) $bResult = true; return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);
return $bResult;
} }
} }

View File

@@ -168,6 +168,31 @@ class Feed extends PhpObject {
return $bNewMsg; return $bNewMsg;
} }
public function addManualPosition($sLat, $sLng, $iTimestamp) {
$sTimeZone = date_default_timezone_get();
$oDateTime = new \DateTime('@'.$iTimestamp);
$oDateTime->setTimezone(new \DateTimeZone($sTimeZone));
$asWeather = $this->getWeather(array($sLat, $sLng), $iTimestamp);
$asMsg = [
'ref_msg_id' => $iTimestamp.'/man',
'id_feed' => $this->getFeedId(),
'type' => 'OK',
'latitude' => $sLat,
'longitude' => $sLng,
'iso_time' => $oDateTime->format("Y-m-d\TH:i:sO"), //Incorrect ISO 8601 format, but compliant with Spot data
'site_time' => $oDateTime->format(Db::TIMESTAMP_FORMAT),
'timezone' => $sTimeZone,
'unix_time' => $iTimestamp,
'content' => '',
'battery_state' => '',
'posted_on' => date(Db::TIMESTAMP_FORMAT),
];
$iMessageId = $this->oDb->insertRow(self::MSG_TABLE, array_merge($asMsg, $asWeather));
return $iMessageId;
}
private function updateFeed() { private function updateFeed() {
$bNewMsg = false; $bNewMsg = false;
$asData = $this->retrieveFeed(); $asData = $this->retrieveFeed();

View File

@@ -223,34 +223,7 @@ class Spot extends Main
//Send Update Email //Send Update Email
if($bNewMsg) { if($bNewMsg) {
$oEmail = new Email($this->asContext['serv_name'], 'email_update'); $bSuccess = $this->sendEmail();
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
//Add Position
$asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE)))));
$oEmail->oTemplate->setTags($asLastMessage);
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
//Add latest news feed
$asNews = $this->getNextFeed(0, true);
$iPostCount = 0;
foreach($asNews as $asPost) {
if($asPost['type'] != 'message') {
$oEmail->oTemplate->newInstance('news');
$oEmail->oTemplate->setInstanceTags('news', array(
'local_server' => $this->asContext['serv_name'],
'project' => $this->oProject->getProjectCodeName(),
'type' => $asPost['type'],
'id' => $asPost['id_'.$asPost['type']])
);
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
$iPostCount++;
}
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
}
$bSuccess = $oEmail->send();
$sDesc = $bSuccess?'mail_sent':'mail_failure'; $sDesc = $bSuccess?'mail_sent':'mail_failure';
} }
else $sDesc = 'no_new_msg'; else $sDesc = 'no_new_msg';
@@ -258,6 +231,37 @@ class Spot extends Main
return self::getJsonResult($bSuccess, $sDesc); return self::getJsonResult($bSuccess, $sDesc);
} }
private function sendEmail() {
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
//Add Position
$asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE)))));
$oEmail->oTemplate->setTags($asLastMessage);
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
//Add latest news feed
$asNews = $this->getNextFeed(0, true);
$iPostCount = 0;
foreach($asNews as $asPost) {
if($asPost['type'] != 'message') {
$oEmail->oTemplate->newInstance('news');
$oEmail->oTemplate->setInstanceTags('news', array(
'local_server' => $this->asContext['serv_name'],
'project' => $this->oProject->getProjectCodeName(),
'type' => $asPost['type'],
'id' => $asPost['id_'.$asPost['type']])
);
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
$iPostCount++;
}
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
}
return $oEmail->send();
}
public function genCronFile() { public function genCronFile() {
//$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false); //$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false);
$sFileName = 'spot_cron.sh'; $sFileName = 'spot_cron.sh';
@@ -632,6 +636,19 @@ class Spot extends Main
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']); return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
} }
public function addPosition($sLat, $sLng, $iTimestamp) {
$oFeed = new Feed($this->oDb, $this->oProject->getFeedIds()[0]);
$bSuccess = ($oFeed->addManualPosition($sLat, $sLng, $iTimestamp) > 0);
if($bSuccess) {
$bSuccess = $this->sendEmail();
$sDesc = $bSuccess?'mail_sent':'mail_failure';
}
else $sDesc = 'error_commit_db';
return self::getJsonResult($bSuccess, $sDesc);
}
public function getAdminSettings($sType='') { public function getAdminSettings($sType='') {
$oFeed = new Feed($this->oDb); $oFeed = new Feed($this->oDb);
$asData = array( $asData = array(
@@ -774,6 +791,10 @@ class Spot extends Main
return self::getJsonResult($bSuccess, $sDesc, $asResult); return self::getJsonResult($bSuccess, $sDesc, $asResult);
} }
public function buildGeoJSON($sCodeName) {
return Converter::convertToGeoJson($sCodeName);
}
public static function decToDms($dValue, $sType) { public static function decToDms($dValue, $sType) {
if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude
else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude

View File

@@ -26,6 +26,9 @@ $oValue = $_REQUEST['value'] ?? '';
$iId = $_REQUEST['id'] ?? 0 ; $iId = $_REQUEST['id'] ?? 0 ;
$sType = $_REQUEST['type'] ?? ''; $sType = $_REQUEST['type'] ?? '';
$sEmail = $_REQUEST['email'] ?? ''; $sEmail = $_REQUEST['email'] ?? '';
$sLat = $_REQUEST['latitude'] ?? '';
$sLng = $_REQUEST['longitude'] ?? '';
$iTimestamp = $_REQUEST['timestamp'] ?? 0;
//Initiate class //Initiate class
$oSpot = new Spot(__FILE__, $sTimezone); $oSpot = new Spot(__FILE__, $sTimezone);
@@ -74,6 +77,9 @@ if($sAction!='')
case 'add_comment': case 'add_comment':
$sResult = $oSpot->addComment($iId, $sContent); $sResult = $oSpot->addComment($iId, $sContent);
break; break;
case 'add_position':
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
break;
case 'admin_get': case 'admin_get':
$sResult = $oSpot->getAdminSettings(); $sResult = $oSpot->getAdminSettings();
break; break;
@@ -92,6 +98,9 @@ if($sAction!='')
case 'sql': case 'sql':
$sResult = $oSpot->getDbBuildScript(); $sResult = $oSpot->getDbBuildScript();
break; break;
case 'build_geojson':
$sResult = $oSpot->buildGeoJSON($sName);
break;
default: default:
$sResult = Main::getJsonResult(false, Main::NOT_FOUND); $sResult = Main::getJsonResult(false, Main::NOT_FOUND);
} }

View File

@@ -1,6 +1,8 @@
# Spot Project # Spot Project
[Spot](https://www.findmespot.com) & GPX integration [Spot](https://www.findmespot.com) & GPX integration
## Dependencies ## Dependencies
* npm 18+ * npm 18+
* composer * composer
* php-mbstring * php-mbstring
@@ -11,13 +13,17 @@
* ffprobe & ffmpeg * ffprobe & ffmpeg
* STARTTLS Email Server (use Gmail if none available) * STARTTLS Email Server (use Gmail if none available)
* Optional: Geo Caching Server (WMTS Caching Service) * Optional: Geo Caching Server (WMTS Caching Service)
## PHP Configuration ## PHP Configuration
* max_execution_time = 300 * max_execution_time = 300
* memory_limit = 500M * memory_limit = 500M
* post_max_size = 4G * post_max_size = 4G
* upload_max_filesize = 4G * upload_max_filesize = 4G
* max_file_uploads = 50 * max_file_uploads = 50
## Getting started ## Getting started
1. Clone Git onto web server 1. Clone Git onto web server
2. composer install 2. composer install
3. npm install webpack 3. npm install webpack
@@ -27,9 +33,11 @@
7. Copy settings-sample.php to settings.php and populate 7. Copy settings-sample.php to settings.php and populate
8. Go to #admin and create a new project, feed & maps 8. Go to #admin and create a new project, feed & maps
9. Add a GPX file named <project_codename>.gpx to /geo/ 9. Add a GPX file named <project_codename>.gpx to /geo/
## To Do List ## To Do List
* Add mail frequency slider * Add mail frequency slider
* Use WMTS servers directly when not using Geo Caching Server * Use WMTS servers directly when not using Geo Caching Server
* Allow HEIF picture format * Allow HEIF picture format
* Fix .MOV playback on windows firefox * Fix .MOV playback on windows firefox
* Garmin InReach Integration * Garmin InReach Integration

View File

@@ -1,21 +1,27 @@
<script> <script>
import Project from './components/project.vue'; import Project from './components/project.vue';
import Admin from './components/admin.vue'; import Admin from './components/admin.vue';
import Upload from './components/upload.vue';
const aoRoutes = { const aoRoutes = {
'project': Project, 'project': Project,
'admin': Admin 'admin': Admin,
'upload': Upload
}; };
export default { export default {
data() { data() {
return { return {
hash: {} hash: {},
consts: this.spot.consts,
user: this.spot.vars('user')
}; };
}, },
provide() { provide() {
return { return {
projects: this.spot.vars('projects') projects: this.spot.vars('projects'),
consts: this.consts,
user: this.user
}; };
}, },
inject: ['spot'], inject: ['spot'],
@@ -25,12 +31,15 @@ export default {
return aoRoutes[this.hash.page]; return aoRoutes[this.hash.page];
} }
}, },
created() {
//User
this.user.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
},
mounted() { mounted() {
window.addEventListener('hashchange', () => {this.onHashChange();}); window.addEventListener('hashchange', () => {this.onHashChange();});
var oEvent = new Event('hashchange'); var oEvent = new Event('hashchange');
window.dispatchEvent(oEvent); window.dispatchEvent(oEvent);
} },
,
methods: { methods: {
_hash(hash, bReboot) { _hash(hash, bReboot) {
bReboot = bReboot || false; bReboot = bReboot || false;

View File

@@ -1,9 +1,11 @@
<script> <script>
import SpotIcon from './spotIcon.vue';
import SpotButton from './spotButton.vue'; import SpotButton from './spotButton.vue';
import AdminInput from './adminInput.vue'; import AdminInput from './adminInput.vue';
export default { export default {
components: { components: {
SpotIcon,
SpotButton, SpotButton,
AdminInput AdminInput
}, },
@@ -51,7 +53,6 @@ export default {
createElem(sType) { createElem(sType) {
this.spot.get2('admin_create', {type: sType}) this.spot.get2('admin_create', {type: sType})
.then((aoNewElemTypes) => { .then((aoNewElemTypes) => {
console.log(aoNewElemTypes);
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) { for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) { for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
oNewElem.type = sType; oNewElem.type = sType;
@@ -60,7 +61,7 @@ export default {
} }
} }
}) })
.catch((sMsg) => {console.log(sMsg);this.spot.onFeedback('error', sMsg, {'create':sType});}); .catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'create':sType});});
}, },
deleteElem(oElem) { deleteElem(oElem) {
const asInputs = { const asInputs = {
@@ -118,7 +119,7 @@ export default {
</script> </script>
<template> <template>
<div id="admin"> <div id="admin">
<a name="back" class="button" href="#project"><i class="fa fa-back push"></i>{{ l('nav_back') }}</a> <a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="l('nav_back')" /></a>
<h1>{{ l('projects') }}</h1> <h1>{{ l('projects') }}</h1>
<div id="project_section"> <div id="project_section">
<table> <table>

View File

@@ -1,7 +1,7 @@
<script> <script>
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
import { Map, NavigationControl, Marker, LngLatBounds, LngLat } from 'maplibre-gl'; import { Map, NavigationControl, Marker, LngLatBounds, LngLat, Popup } from 'maplibre-gl';
import { createApp, defineComponent, nextTick, ref, defineCustomElement, provide, inject } from 'vue';
import simplebar from 'simplebar-vue'; import simplebar from 'simplebar-vue';
import autosize from 'autosize'; import autosize from 'autosize';
@@ -14,12 +14,14 @@ import lightbox from '../scripts/lightbox.js';
import SpotIcon from './spotIcon.vue'; import SpotIcon from './spotIcon.vue';
import SpotButton from './spotButton.vue'; import SpotButton from './spotButton.vue';
import ProjectPost from './projectPost.vue'; import ProjectPost from './projectPost.vue';
import ProjectPopup from './projectPopup.vue';
export default { export default {
components: { components: {
SpotIcon, SpotIcon,
SpotButton, SpotButton,
ProjectPost, ProjectPost,
ProjectPopup,
simplebar simplebar
}, },
data() { data() {
@@ -36,7 +38,6 @@ export default {
posts: [], posts: [],
nlFeedbacks: [], nlFeedbacks: [],
nlLoading: false, nlLoading: false,
user: {name:'', email:''},
baseMaps: {}, baseMaps: {},
baseMap: null, baseMap: null,
messages: null, messages: null,
@@ -73,7 +74,7 @@ export default {
watch: { watch: {
baseMap(sNewBaseMap, sOldBaseMap) { baseMap(sNewBaseMap, sOldBaseMap) {
if(sOldBaseMap) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none'); if(sOldBaseMap) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible'); if(sNewBaseMap) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
}, },
projectCodename(sNewCodeName, sOldCodeName) { projectCodename(sNewCodeName, sOldCodeName) {
console.log('change in projectCodename: '+sNewCodeName); console.log('change in projectCodename: '+sNewCodeName);
@@ -84,11 +85,10 @@ export default {
}, },
provide() { provide() {
return { return {
user: this.user,
project: this.project project: this.project
}; };
}, },
inject: ['spot', 'projects'], inject: ['spot', 'projects', 'user'],
mounted() { mounted() {
this.spot.addPage('project', { this.spot.addPage('project', {
onResize: () => { onResize: () => {
@@ -110,7 +110,6 @@ export default {
this.initProject(); this.initProject();
if(bFirstLoad) this.initLightbox(); if(bFirstLoad) this.initLightbox();
this.initFeed(); this.initFeed();
if(bFirstLoad) this.initSettings();
this.initMap(); this.initMap();
}, },
initProject() { initProject() {
@@ -152,12 +151,10 @@ export default {
//Scroll to post //Scroll to post
if(this.$parent.hash.items.length == 3) this.findPost({type: this.$parent.hash.items[1], id: this.$parent.hash.items[2]}); if(this.$parent.hash.items.length == 3) this.findPost({type: this.$parent.hash.items[1], id: this.$parent.hash.items[2]});
}, },
initSettings() {
this.user = this.spot.vars('user');
},
async initMap() { async initMap() {
//Get Map Info //Get Map Info
const aoMarkers = await this.spot.get2('markers', {id_project: this.project.id}); const aoMarkers = await this.spot.get2('markers', {id_project: this.project.id});
this.baseMap = null;
this.baseMaps = aoMarkers.maps; this.baseMaps = aoMarkers.maps;
this.messages = aoMarkers.messages; this.messages = aoMarkers.messages;
@@ -193,7 +190,10 @@ export default {
}); });
this.map.once('load', async () => { this.map.once('load', async () => {
//Track //Default Basemap
this.baseMap = this.baseMaps.filter((asBM) => asBM.default_map)[0].codename;
//Get track
const oTrack = await this.spot.get2('geojson', {id_project: this.project.id}); const oTrack = await this.spot.get2('geojson', {id_project: this.project.id});
this.map.addSource('track', { this.map.addSource('track', {
'type': 'geojson', 'type': 'geojson',
@@ -224,9 +224,91 @@ export default {
}); });
//Markers //Markers
let aoMarkerSource = {type:'geojson', data:{type: 'FeatureCollection', features: []}};
for(const oMsg of this.messages) { for(const oMsg of this.messages) {
new Marker().setLngLat(new LngLat(oMsg.longitude, oMsg.latitude)).addTo(this.map); aoMarkerSource.data.features.push({
'type': 'Feature',
'properties': {
...oMsg,
...{'description': ''}
},
'geometry': {
'type': 'Point',
'coordinates': [oMsg.longitude, oMsg.latitude]
}
});
//Tooltip
/*
let $Tooltip = $($('<div>', {'class':'info-window'})
.append($('<h1>')
.addIcon('fa-message fa-lg', true)
.append($('<span>').text(this.spot.lang('post_message')+' '+this.spot.lang('counter', oMsg.displayed_id)))
.append($('<span>', {'class':'message-type'}).text('('+oMsg.type+')'))
)
.append($('<div>', {'class':'separator'}))
.append($('<p>', {'class':'coordinates'})
.addIcon('fa-coords fa-fw fa-lg', true)
.append(this.getGoogleMapsLink(oMsg))
)
.append($('<p>', {'class':'time'})
.addIcon('fa-time fa-fw fa-lg', true)
.append(oMsg.formatted_time+(this.project.mode==this.spot.consts.modes.blog?' ('+oMsg.relative_time+')':''))))[0];
const vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
//let vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
oPopup.setDOMContent(vTooltip);
new Marker({
element: $('<div style="width:'+this.markerSize.width+'px;height:'+this.markerSize.height+'px;"><span class="fa-stack"><i class="fa fa-message fa-stack-2x"></i><i class="fa fa-message-in fa-rotate-270 fa-stack-1x"></i></span></div>')[0],
anchor: 'bottom'
})
.setLngLat(new LngLat(oMsg.longitude, oMsg.latitude))
.setPopup(oPopup)
.addTo(this.map)
;
*/
} }
this.map.addSource('markers', aoMarkerSource);
const image = await this.map.loadImage('https://maplibre.org/maplibre-gl-js/docs/assets/custom_marker.png');
this.map.addImage('markerIcon', image.data);
this.map.addLayer({
'id': 'markers',
'type': 'symbol',
'source': 'markers',
'layout': {
//'icon-anchor': 'bottom',
'icon-image': 'markerIcon'
//'icon-overlap': 'always'
}
});
this.map.on("click", "markers", (e) => {
var oPopup = new Popup({
anchor: 'bottom',
offset: [0, this.markerSize.height * -1],
closeButton: false
})
.setHTML('<div id="popup"></div>')
.setLngLat(e.lngLat)
.addTo(this.map);
let rProp = ref(e.features[0].properties);
const vPopup = defineComponent({
extends: ProjectPopup,
setup: () => {
console.log(rProp.value);
provide('options', rProp.value);
provide('spot', this.spot);
provide('project', this.project);
return {'options': rProp.value, 'spot':this.spot, 'project':this.project};
}
});
nextTick(() => {
createApp(vPopup).mount("#popup");
});
});
//Centering map //Centering map
let bOpenFeedPanel = !this.mobile; let bOpenFeedPanel = !this.mobile;
@@ -242,7 +324,6 @@ export default {
} }
else { else {
//Fit to track //Fit to track
console.log('Fit to track');
for(const iFeatureId in oTrack.features) { for(const iFeatureId in oTrack.features) {
oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce( oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce(
(bounds, coord) => { (bounds, coord) => {
@@ -269,9 +350,6 @@ export default {
//Toggle only when map is ready, for the tilt effet //Toggle only when map is ready, for the tilt effet
this.toggleFeedPanel(bOpenFeedPanel); this.toggleFeedPanel(bOpenFeedPanel);
//Default Basemap
this.baseMap = this.baseMaps.filter((asBM)=>asBM.default_map)[0].codename;
}); });
this.map.on('idle', () => { this.map.on('idle', () => {
@@ -282,6 +360,14 @@ export default {
},
getGoogleMapsLink(asInfo) {
return $('<a>', {
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
title: this.spot.lang('see_on_google'),
target: '_blank',
rel: 'noreferrer noopener'
}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
}, },
async getNextFeed() { async getNextFeed() {
if(!this.feed.outOfData && !this.feed.loading) { if(!this.feed.outOfData && !this.feed.loading) {

View File

@@ -0,0 +1,50 @@
<script>
import { options } from 'lightbox2';
import projectMapLink from './projectMapLink.vue';
import spotIcon from './spotIcon.vue';
import projectRelTime from './projectRelTime.vue';
export default {
components: {
spotIcon,
projectMapLink,
projectRelTime
},
//props: {
// options: Object,
//},
data() {
return {
}
},
//inject: ['options', 'spot', 'project'],
mounted() {
}
}
</script>
<template>
<div class="info-window">
<h1>
<spotIcon :icon="'message'" :classes="'fa-lg'" :text="spot.lang('post_message')+' '+spot.lang('counter', options.displayed_id)" />
<span class="message-type">({{ options.type }})</span>
</h1>
<div class="separator"></div>
<p class="coordinates">
<spotIcon :icon="'coords'" :classes="'fa-fw fa-lg'" :margin="true" />
<projectMapLink :options="options" />
</p>
<p class="time">
<spotIcon :icon="'time'" :classes="'fa-fw fa-lg'" :text="options.formatted_time" />
<span v-if="project.mode==spot.consts.modes.blog"> ({{ options.relative_time }})</span>
</p>
<p class="timezone" v-if="options.day_offset != '0'">
<spotIcon :icon="'timezone'" :classes="'fa-fw fa-lg'" :margin="true" />
<projectRelTime :localTime="options.formatted_time_local" :offset="options.day_offset" />
</p>
<p class="weather" v-if="options.weather_icon && options.weather_icon!='unknown'" :title="options.weather_cond==''?'':spot.lang(options.weather_cond)">
<spotIcon :icon="options.weather_icon" :classes="'fa-fw fa-lg'" :text="options.weather_temp+'°C'" />
</p>
</div>
</template>

View File

@@ -10,7 +10,8 @@ export default {
<template> <template>
<span> <span>
{{ localTime.substr(-5) }} <span>{{ localTime.substring(-5) }}</span>
<sup v-if="offset != '0'" :title="offset+' '+spot.lang('unit_day')+' ('+localTime.substr(0, 5)+')'">{{ ' '+offset }}</sup> <sup v-if="offset != '0'" :title="offset+' '+spot.lang('unit_day')+' ('+localTime.substring(0, 5)+')'">{{ ' '+offset }}</sup>
<span>&nbsp;{{ spot.lang('local_time', ' ').trim() }}</span>
</span> </span>
</template> </template>

100
src/components/upload.vue Normal file
View File

@@ -0,0 +1,100 @@
<script>
import SpotIcon from './spotIcon.vue';
import SpotButton from './spotButton.vue';
import "blueimp-file-upload/js/vendor/jquery.ui.widget.js";
import "blueimp-file-upload/js/jquery.iframe-transport.js";
import "blueimp-file-upload/js/jquery.fileupload.js";
export default {
name: 'upload',
components: { SpotButton, SpotIcon },
inject: ['spot', 'projects', 'consts', 'user'],
data() {
return {
project: this.projects[this.spot.vars('default_project_codename')],
files: [],
logs: [],
progress: 0
};
},
mounted() {
this.spot.addPage('upload', {});
if(this.project.editable) {
$('#fileupload')
.fileupload({
dataType: 'json',
formData: {t: this.user.timezone},
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
done: (e, asData) => {
$.each(asData.result.files, (iKey, oFile) => {
let bError = ('error' in oFile);
//Feedback
this.logs.push(bError?oFile.error:(this.spot.lang('upload_success', [oFile.name])));
//Comments
oFile.content = '';
if(!bError) this.files.push(oFile);
});
},
progressall: (e, data) => {
this.progress = parseInt(data.loaded / data.total * 100, 10);
}
});
}
else this.logs = [this.spot.lang('upload_mode_archived', [this.project.name])];
},
methods: {
addComment(oFile) {
this.spot.get2('add_comment', {id: oFile.id, content: oFile.content})
.then((asData) => {this.logs.push(this.spot.lang('media_comment_update', asData.filename));})
.catch((sMsgId) => {this.logs.push(this.spot.lang(sMsgId));});
},
addPosition() {
if(navigator.geolocation) {
this.logs.push('Determining position...');
navigator.geolocation.getCurrentPosition(
(position) => {
this.logs.push('Sending position...');
this.spot.get2('add_position', {'latitude':position.coords.latitude, 'longitude':position.coords.longitude, 'timestamp':Math.round(position.timestamp / 1000)})
.then((asData) => {this.logs.push('Position sent');})
.catch((sMsgId) => {this.logs.push(self.lang(sMsgId));});
},
(error) => {
this.logs.push(error.message);
}
);
}
else this.logs.push('This browser does not support geolocation');
}
}
}
</script>
<template>
<div id="upload">
<a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="spot.lang('nav_back')" /></a>
<h1>{{ spot.lang('upload_title') }}</h1>
<h2>{{ this.project.name }}</h2>
<div class="section" v-if="project.editable">
<input id="fileupload" type="file" name="files[]" :data-url="this.spot.getActionLink('upload')" multiple />
</div>
<div class="section progress" v-if="progress > 0">
<div class="bar" :style="{width:progress+'%'}"></div>
</div>
<div class="section comment" v-for="file in files">
<img class="thumb" :src="file.thumbnail" />
<div class="form">
<input class="content" name="content" type="text" v-model="file.content" />
<input class="id" name="id" type="hidden" :value="file.id" />
<SpotButton :classes="'save'" :icon="'save'" :text="spot.lang('save')" @click="addComment(file)" />
</div>
</div>
<div class="section location">
<SpotButton :icon="'message'" :text="spot.lang('new_position')" @click="addPosition()" />
</div>
<div class="section logs" v-if="logs.length > 0">
<p class="log" v-for="log in logs">{{ log }}.</p>
</div>
</div>
</template>

View File

@@ -79,6 +79,9 @@ $fa-css-prefix: fa;
.#{$fa-css-prefix}-config:before { content: fa-content($fa-var-cogs); } .#{$fa-css-prefix}-config:before { content: fa-content($fa-var-cogs); }
.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-cloud-upload); } .#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-cloud-upload); }
/* Upload */
.#{$fa-css-prefix}-save:before { content: fa-content($fa-var-floppy-disk); }
/* Feed */ /* Feed */
.#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); } .#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); }
.#{$fa-css-prefix}-media:before { content: fa-content($fa-var-photo-video); } .#{$fa-css-prefix}-media:before { content: fa-content($fa-var-photo-video); }

View File

@@ -6,7 +6,7 @@
width: 100%; width: 100%;
/* Leaflet Popup */ /* Leaflet Popup */
.leaflet-popup-content { .maplibregl-popup-content {
h1 { h1 {
font-size: 1.4em; font-size: 1.4em;

View File

@@ -1,36 +1,53 @@
#upload { #upload {
padding: 1em; padding: 1em;
.bar { .section {
height: 18px; border-radius: 3px;
background: green; margin-top: 1rem;
padding: 0 1rem 1rem 1rem;
border-bottom: 1px solid #EEE;
}
.progress {
.bar {
height: 18px;
background: green;
}
} }
.comment { .comment {
margin-top: 1em;
.thumb { .thumb {
width: 30%; width: 30%;
max-width: 100px; max-width: 100px;
} }
form {
.form {
display: inline-block; display: inline-block;
width: calc(70% - 1em); width: calc(70% - 2rem);
min-width: calc(100% - 100px - 1em); min-width: calc(100% - 100px - 2rem);
margin-left: 1em; padding: 1rem;
vertical-align: top; vertical-align: top;
box-sizing: border-box;
.content { .content {
width: 100%; width: calc(100% - 2rem);
box-sizing: border-box; box-sizing: border-box;
padding: 0.5em; padding: 0.5em;
border: 1px solid #333; background: #EEE;
} }
.save { .save {
margin-top: 1em; margin-top: 1rem;
padding: 0.5em; padding: 0.5em;
} }
} }
} }
.logs {
padding: 1rem;
p.log {
margin: 0;
}
}
} }