Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8be679634 | ||
|
|
30a7c56fe5 | ||
|
|
df5ee60dd2 | ||
|
|
907743f6e0 | ||
|
|
cfaa730bdf | ||
|
|
bb3f825bc4 | ||
|
|
8d206ec1ba | ||
|
|
0d1bec144c | ||
|
|
809632db94 | ||
|
|
999ebc9a6d | ||
|
|
5ce7f0464d | ||
|
|
067c0696c1 | ||
|
|
25839e0942 | ||
|
|
b40e32d8b0 | ||
|
|
9b5cdf94cd | ||
|
|
9224be15eb | ||
|
|
750f0891cc | ||
|
|
1bf8a6174e | ||
|
|
9f9d3cefca | ||
|
|
2f04fdb9dc | ||
|
|
7d85c966d6 | ||
|
|
6fec4a5f65 | ||
|
|
36591a6074 | ||
|
|
3fcd1194a1 | ||
|
|
e7be663c85 | ||
|
|
4bd2636379 | ||
|
|
c9151d598a | ||
|
|
a8f3a0502e | ||
|
|
74fd8bd972 | ||
|
|
0e06b0e591 | ||
|
|
e384162c80 | ||
|
|
00474eb137 | ||
|
|
30615d3fae | ||
|
|
49e7d5db7d | ||
|
|
953035e0cc | ||
|
|
dbb784f863 | ||
|
|
fca737630b | ||
|
|
8a480aad35 | ||
|
|
9768c2e393 | ||
|
|
64699eac46 | ||
|
|
8b5ba6894e | ||
|
|
4ff3bb2c39 | ||
|
|
28ae822c96 | ||
|
|
95914ab471 | ||
|
|
ce9168be5f | ||
|
|
237c33b941 | ||
|
|
f084c30d2a | ||
|
|
a5f4ba2bb4 | ||
|
|
74d8d66e23 | ||
|
|
494545b39e | ||
|
|
b2cfdb5b07 | ||
|
|
8f99a8e9f5 | ||
|
|
8a1abbc240 | ||
|
|
b2de240c40 | ||
|
|
581581d86c | ||
|
|
cf49a17266 | ||
|
|
c7539e82ea | ||
|
|
13bb9f2399 | ||
|
|
7800885e0b | ||
|
|
f00e0b738f | ||
|
|
9119fcca74 | ||
|
|
10095b412f | ||
|
|
062dced9bc | ||
|
|
0300d8b577 | ||
|
|
33d5a927c1 | ||
|
|
6b7c6b72e6 | ||
|
|
60e5b795d5 | ||
|
|
4e3f4c70be | ||
|
|
15952451e8 | ||
|
|
c26251ec49 | ||
|
|
7e80fd4ad4 | ||
|
|
6db4dedf55 | ||
|
|
ed0eeaf776 | ||
|
|
df2c6da047 | ||
|
|
3622f70aa1 | ||
|
|
fb882d34f7 | ||
|
|
cfae1ddecd | ||
|
|
33a5143985 | ||
|
|
42298202d7 | ||
|
|
ca8a9ece18 | ||
|
|
81f070204c | ||
|
|
6981cc383d | ||
|
|
99a10723a5 | ||
|
|
8760db2aba | ||
|
|
8f34c31d43 | ||
|
|
8624433f37 | ||
|
|
2e93bfe7b7 | ||
|
|
b6cd49db09 | ||
|
|
b27274df89 | ||
|
|
209a1b6731 | ||
|
|
a1ef4d1cc8 | ||
|
|
104aaf02cc | ||
|
|
58958c8730 | ||
|
|
f037545591 | ||
|
|
de35c507ed | ||
|
|
2df24b16f7 | ||
|
|
17828fe9ce | ||
|
|
7585d475a7 | ||
|
|
1bcdef230c | ||
|
|
de8a3f07e5 | ||
|
|
17411c1f19 | ||
|
|
efd3a73c09 | ||
|
|
cc1d9d97db | ||
|
|
b6ec8c908e | ||
|
|
da3b16db46 | ||
|
|
b40d813634 | ||
|
|
f05cdbf351 | ||
|
|
b8a8d75084 | ||
|
|
d605acbd51 | ||
|
|
7d9a61cdd4 | ||
|
|
332f3df940 | ||
|
|
342d4dda8d | ||
|
|
341d61b29e | ||
|
|
6a8552ee47 | ||
|
|
9552814421 | ||
|
|
6e39b12a5f | ||
|
|
2aebcb00cc | ||
|
|
c86dce74c2 | ||
|
|
ebdad2e657 | ||
|
|
888e32520b | ||
|
|
a9adbb5045 | ||
|
|
06c1dcc6f4 | ||
|
|
33bc059658 | ||
|
|
52669bed48 | ||
|
|
dc570f883b | ||
|
|
7ba9243fa1 | ||
|
|
fe7fd52357 | ||
|
|
ffe7f3162f | ||
|
|
a33cda5f22 | ||
|
|
e66ec08ab4 | ||
|
|
5d79123e75 | ||
|
|
a0a3ba2d26 | ||
|
|
d6f2621368 | ||
|
|
47b693988a | ||
|
|
a5fa444997 | ||
|
|
1a07aac836 | ||
|
|
abab8e1f22 | ||
|
|
a9044de3f1 | ||
|
|
e34c2b690d | ||
|
|
a81f7a596b | ||
|
|
49ae92447b | ||
|
|
5bdccec6ce | ||
|
|
ef3dc986da | ||
|
|
c01fe7f3bd | ||
|
|
8f10febcf2 | ||
|
|
44ba1eae68 | ||
|
|
85e8c4a3d1 | ||
|
|
9b59ec502e | ||
|
|
90983754dc | ||
|
|
741e4efa0d | ||
|
|
fa18493439 | ||
|
|
79dbb5ad54 | ||
|
|
a52c1d5782 | ||
|
|
de2e17c430 | ||
|
|
7b4b965f15 | ||
|
|
37f1ccc8c1 | ||
|
|
05a2b1d581 | ||
|
|
f170855ab8 | ||
|
|
273bd48b9f | ||
|
|
502147cb0f | ||
|
|
1cf03b6d15 | ||
|
|
93d2161139 | ||
|
|
00f8a9b763 | ||
|
|
156c0a0414 | ||
|
|
6260b80c96 | ||
|
|
a364ad03e8 | ||
|
|
80a9364be6 | ||
|
|
9d9309c623 | ||
|
|
44651b3f3e | ||
|
|
8a530a13ed | ||
|
|
4f38c14413 | ||
|
|
1ed68c866f | ||
|
|
584accc72e | ||
|
|
9a78604ff2 | ||
|
|
f129e90005 | ||
|
|
5404a79947 | ||
|
|
83d5e4bcd6 | ||
|
|
5dcb04a1d7 | ||
|
|
acf81386ce | ||
|
|
7dbf5bef87 | ||
|
|
fb38feb51c | ||
|
|
2369a4302c | ||
|
|
6f040d61c5 | ||
|
|
faaa24012b | ||
|
|
68cb62a3fb | ||
|
|
56deda45fe | ||
|
|
43ad3c4193 | ||
|
|
00cf52ead7 | ||
|
|
20a23e9957 | ||
|
|
03aa057a11 | ||
|
|
7ec6d25220 | ||
|
|
34f152dead | ||
|
|
9bd8cd5342 | ||
|
|
d6939598d3 | ||
|
|
56948cb59f | ||
|
|
d250181a15 | ||
|
|
a7b552fe4a | ||
|
|
e2e5df5414 | ||
|
|
6a9c563df1 | ||
|
|
abad39eaa8 | ||
|
|
ea430cba72 | ||
|
|
28ff5c44d3 | ||
|
|
3bad9d4893 | ||
|
|
9d09171a73 | ||
|
|
add272ade6 | ||
|
|
7e729a7fdb | ||
|
|
0a955cd432 | ||
|
|
d0ecd047ca | ||
|
|
95ad05de26 | ||
|
|
7c0f3feed7 | ||
|
|
9c5dc5fa4d | ||
|
|
e80ae81e42 | ||
|
|
73f2e6c6e4 | ||
|
|
e1aac63ae0 | ||
|
|
9f7ada89f4 | ||
|
|
022e41ac7e | ||
|
|
6d7f561544 | ||
|
|
4d46662544 | ||
|
|
961b1581d1 | ||
|
|
55ede8bdeb | ||
|
|
e431d42ec7 | ||
|
|
932537d22d | ||
|
|
bac6817abd | ||
|
|
a8ea874125 | ||
|
|
d3dc6901a6 | ||
|
|
297660b70c | ||
|
|
966601f2ff | ||
|
|
97712b5122 | ||
|
|
09dfe7d24b | ||
|
|
755aa9e0ec | ||
|
|
6a1faceb6f | ||
|
|
eb82cc3c39 | ||
|
|
667a094f54 | ||
|
|
94ae30e1f3 | ||
|
|
b83ea24c18 | ||
|
|
832b98e516 | ||
|
|
e2e707595b | ||
|
|
1cd022463e | ||
|
|
cb5ca08891 | ||
|
|
226de87ff8 | ||
|
|
5511b28e47 | ||
|
|
5983cab494 | ||
|
|
d8a938af31 | ||
|
|
096c25a69e | ||
|
|
a9956a6a2e | ||
|
|
e4a8cc7dbb | ||
|
|
aafce4d207 | ||
|
|
8c50a5aea9 | ||
|
|
5063379f22 | ||
|
|
693b929519 | ||
|
|
dc3eadc00a | ||
|
|
3359467caa | ||
|
|
4b0f5714b3 | ||
|
|
a25dd83629 | ||
|
|
5a147d7c47 | ||
|
|
3938c5f4aa | ||
|
|
048bb6497f | ||
|
|
5bb13e54bd | ||
|
|
3a9b14c772 | ||
|
|
0717001a08 | ||
|
|
930cf74d34 | ||
|
|
8a2d2a535d | ||
|
|
5574188e8f | ||
|
|
13dbdea2fb | ||
|
|
dedb9a18ae | ||
|
|
0a6ff9dffd | ||
|
|
e4f89008cf | ||
|
|
7a503c0888 | ||
|
|
81b9e061a8 | ||
|
|
851c743fa6 | ||
|
|
f68a864b16 | ||
|
|
ba57642d24 | ||
|
|
6d34b8b415 | ||
|
|
0cf48c8dd0 | ||
|
|
9e9605fe2e | ||
|
|
eb6a94a490 | ||
|
|
82ebfc48aa | ||
|
|
1ba6489802 | ||
|
|
9bea0a318f | ||
|
|
29ffa3098a | ||
|
|
077c38fe58 | ||
|
|
299e4c8a54 | ||
|
|
34a7cf6b08 | ||
|
|
7f316624a7 | ||
|
|
1faa71eaaa | ||
|
|
c532573688 | ||
|
|
2ac8dead56 | ||
|
|
8a3c951b65 | ||
|
|
9499e2c7c9 | ||
|
|
26c2057677 | ||
|
|
c88c027c24 | ||
|
|
9dc302091b | ||
|
|
561a8ae215 | ||
|
|
ffc5d81a3d | ||
|
|
f4e787b9b7 | ||
|
|
19080d926a | ||
|
|
ebe286362b | ||
|
|
c9503bf3ef | ||
|
|
f1d8879624 | ||
|
|
3a636b040b | ||
|
|
46b3bc1ccc | ||
|
|
c4377d62d5 | ||
|
|
3aa349b338 | ||
|
|
a940b7ec2d | ||
|
|
e328ad860c | ||
|
|
441dcae09a | ||
|
|
efbb31c1d0 | ||
|
|
03edff8baa | ||
|
|
8eb110879d | ||
|
|
999094b23b | ||
|
|
15ca2841d3 | ||
|
|
28eaffe247 | ||
|
|
cc5491cd5f | ||
|
|
2dc35bbc12 | ||
|
|
2a9686429f | ||
|
|
a00d9a7a51 | ||
|
|
54426daca6 | ||
|
|
62b94624f0 | ||
|
|
3149a80fa5 | ||
|
|
0ea31c8ff2 | ||
|
|
571eb0d241 | ||
|
|
e138f2ca41 | ||
|
|
85bd290d22 | ||
|
|
e40e57cf7f | ||
|
|
591a2c5bad | ||
|
|
b92c6fb5ed | ||
|
|
d943094d87 | ||
|
|
9443a83501 | ||
|
|
4a570e3b9d | ||
|
|
a824912683 | ||
|
|
c0954c32d9 | ||
|
|
05638e8762 | ||
|
|
40a242ccb2 | ||
|
|
36a3bfc726 | ||
|
|
82be0d5dc7 | ||
|
|
fd07402940 | ||
|
|
2c146e6236 | ||
|
|
00cb4790b6 | ||
|
|
19b3d8373f | ||
|
|
59a9e5b690 | ||
|
|
5a0734a357 | ||
|
|
8ff7185ff3 | ||
|
|
32108b0141 | ||
|
|
8dcfb4822b | ||
|
|
73fa987490 | ||
|
|
fd9b4d787f | ||
|
|
4a9dfa22e2 | ||
|
|
0912e76ec4 | ||
|
|
52e39832c2 | ||
|
|
20a7b1f49f | ||
|
|
d6a8172e8d | ||
|
|
a9329012f7 | ||
|
|
945b0332d0 | ||
|
|
273561ce72 | ||
|
|
2424a7bed1 | ||
|
|
1895b5545b | ||
|
|
e7bf63c4df | ||
|
|
f1691b44a2 | ||
|
|
6ce4e440cd | ||
|
|
d74cbccd01 | ||
|
|
e4dbed2716 | ||
|
|
9a56c27df5 | ||
|
|
e6de9bc72a | ||
|
|
b7c12f981d | ||
|
|
8a78de3ad8 | ||
|
|
6034cd762c | ||
|
|
2b8caa71a4 | ||
|
|
02c9ea3249 | ||
|
|
e5d64168d5 | ||
|
|
3b953e1237 | ||
|
|
bf4a4e32fe | ||
|
|
cc63ee51a7 | ||
|
|
6235825509 | ||
|
|
72b41124d8 | ||
|
|
3ea624de09 | ||
|
|
07c3d81779 | ||
|
|
6e7c45ef36 | ||
|
|
b51b5c2792 | ||
|
|
672872e9a1 | ||
|
|
0702e5afeb | ||
|
|
4fdbfd57bb | ||
|
|
c3af807b51 | ||
|
|
af10c675d3 | ||
|
|
59e07a65ff | ||
|
|
1fb2d72732 | ||
|
|
e56e77a37e | ||
|
|
25405b27f1 | ||
|
|
853fed071e | ||
|
|
7246e877af | ||
|
|
ee21a4f592 | ||
|
|
b0ff0b9a53 | ||
|
|
c3bad2d742 | ||
|
|
4a250a1a68 | ||
|
|
5d47c10aa1 | ||
|
|
3cc93afaf1 | ||
|
|
63478c37da | ||
|
|
f11bd6a96a | ||
|
|
e3b8e3048c | ||
|
|
bb771f6a8c | ||
|
|
0f097fdf63 | ||
|
|
dcac0bfe98 | ||
|
|
369f6dab5d | ||
|
|
ddfd1eaf21 | ||
|
|
9c89e5e237 | ||
|
|
59faae245d | ||
|
|
ec550d0a15 | ||
|
|
5d3d2d54ee | ||
|
|
42d891b952 | ||
|
|
03217058e8 | ||
|
|
0363f4f272 |
No files matched your search
@@ -1,60 +0,0 @@
|
||||
name: Deploy Livetrail
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: livetrail
|
||||
|
||||
env:
|
||||
COMPOSER_NO_INTERACTION: "1"
|
||||
COMPOSER_HOME: .composer
|
||||
DEPLOY_PATH: /var/www/livetrail
|
||||
npm_config_cache: .npm-cache
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check runner tools
|
||||
run: |
|
||||
command -v composer
|
||||
command -v npm
|
||||
command -v rsync
|
||||
|
||||
- name: Check deploy path
|
||||
run: |
|
||||
test -d "$DEPLOY_PATH"
|
||||
test -w "$DEPLOY_PATH"
|
||||
|
||||
- name: Validate Composer configuration
|
||||
run: composer validate --no-check-publish
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --no-dev --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Prepare runtime missing mount points
|
||||
run: mkdir -p files resources/geo
|
||||
|
||||
- name: Build frontend
|
||||
run: npm run prod
|
||||
|
||||
- name: Deploy to production
|
||||
run: |
|
||||
rsync -azc --no-times --delete \
|
||||
--exclude "/.git/" \
|
||||
--exclude "/.gitea/" \
|
||||
--exclude "/.composer/" \
|
||||
--exclude "/.npm-cache/" \
|
||||
--exclude "/node_modules/" \
|
||||
--exclude "/config/settings.php" \
|
||||
--exclude "/log.html" \
|
||||
--exclude "/files/" \
|
||||
--exclude "/resources/geo/*.geojson" \
|
||||
./ "$DEPLOY_PATH/"
|
||||
@@ -1,19 +1,19 @@
|
||||
# App config files
|
||||
/config/settings.php
|
||||
/log.html
|
||||
|
||||
# Upload folders
|
||||
/files/
|
||||
/resources/geo/*.geojson
|
||||
|
||||
# Build folder
|
||||
/public/*
|
||||
!/public/index.php
|
||||
|
||||
# Dependencies files
|
||||
/vendor/
|
||||
/node_modules/
|
||||
/composer.dev.lock
|
||||
|
||||
# Lint caches
|
||||
/.php-cs-fixer.cache
|
||||
/build.xml
|
||||
/settings.php
|
||||
/.externalToolBuilders/
|
||||
/.sass-cache/
|
||||
/.buildpath
|
||||
/.project
|
||||
/style/.sass-cache/
|
||||
/.settings/
|
||||
/files/**/*.jpg
|
||||
/files/**/*.JPG
|
||||
/files/**/*.jpeg
|
||||
/files/**/*.JPEG
|
||||
/files/**/*.png
|
||||
/files/**/*.PNG
|
||||
/files/**/*.mov
|
||||
/files/**/*.MOV
|
||||
/geo/*.geojson
|
||||
/spot_cron.sh
|
||||
/vendor/*
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-CS-Fixer config codifying the style already in use under lib/ and
|
||||
* public/. Deliberately NOT based on @PSR2/@PSR12 - this codebase diverges
|
||||
* from those on purpose (tabs, no space before control-structure parens),
|
||||
* so pulling in a preset would rewrite most of the codebase to match a
|
||||
* style nobody uses here. Rules below were derived by sampling lib/*.php,
|
||||
* not from a style guide.
|
||||
*/
|
||||
|
||||
$finder = (new PhpCsFixer\Finder())
|
||||
->in([__DIR__.'/lib', __DIR__.'/public'])
|
||||
->append([__DIR__.'/config/settings-sample.php'])
|
||||
->name('*.php');
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setIndent("\t")
|
||||
->setLineEnding("\n")
|
||||
->setRules([
|
||||
//Strings: single quotes except where interpolation needs double
|
||||
'single_quote' => true,
|
||||
|
||||
//Arrays: short [] syntax, not long-form array()
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
|
||||
//Braces: same line as class/function signature (dominant in lib/,
|
||||
//not the PSR-2 next-line-for-class convention)
|
||||
'braces_position' => [
|
||||
'classes_opening_brace' => 'same_line',
|
||||
'functions_opening_brace' => 'same_line',
|
||||
'anonymous_functions_opening_brace' => 'same_line'
|
||||
],
|
||||
|
||||
//Concatenation: no padding around `.`
|
||||
'concat_space' => ['spacing' => 'none'],
|
||||
|
||||
//true/false/null: lowercase (100% consistent already)
|
||||
'constant_case' => ['case' => 'lower'],
|
||||
'lowercase_keywords' => true,
|
||||
|
||||
'visibility_required' => ['elements' => ['method', 'property', 'const']],
|
||||
|
||||
//Housekeeping - safe regardless of style
|
||||
'no_unused_imports' => true,
|
||||
'no_trailing_whitespace' => true,
|
||||
'no_trailing_whitespace_in_comment' => true,
|
||||
'single_blank_line_at_eof' => true,
|
||||
'no_empty_statement' => true,
|
||||
'trim_array_spaces' => true,
|
||||
'new_with_parentheses' => true
|
||||
])
|
||||
->setFinder($finder);
|
||||
@@ -1,169 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidelines for AI coding agents working on Livetrail. See `readme.md` for installation and dependencies.
|
||||
|
||||
## Project overview
|
||||
|
||||
Self-hosted web app that shares journeys on a map: SPOT GPS positions, GPX routes, posts, photos/videos and a chronological feed.
|
||||
|
||||
- **Backend:** PHP 8.5+, MariaDB/MySQL, namespace `Franzz\Livetrail` (PSR-4 → `lib/`).
|
||||
- **Frontend:** Vue 3 (Options API), MapLibre GL, SCSS, built with Vite.
|
||||
- **Shared library:** `franzz/objects` (`Franzz\Objects\*`: `Main`, `PhpObject`, `Db`, `Translator`, `ToolBox`, `Mask`…). It lives in a separate repo (`../objects`). Change generic behaviour there, not by duplicating it here.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory layout
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `public/` | Web root only. `index.php` is the single front controller. `assets/` is Vite output (generated, never edit). `files` and `geo` are symlinks created by the build. |
|
||||
| `lib/` | PHP classes, one class per file, file name = class name. |
|
||||
| `config/` | `settings.php` (git-ignored, copied from `settings-sample.php`) defines the global `Settings` class of constants. Web server samples. |
|
||||
| `resources/lang/*.json` | Translations (`en`, `es`, `fr`). |
|
||||
| `resources/masks/*.html` | Server-side HTML templates (main page, emails). |
|
||||
| `resources/geo/` | `<codename>.gpx` source routes; `.geojson` files are generated from them. |
|
||||
| `src/` | Frontend sources: `app.js` (entry), `App.vue` (hash router), `components/`, `scripts/` (plain JS classes), `styles/`, `images/`. |
|
||||
| `cli/cron.sh` | Hourly `a=update_project` call that polls the SPOT feeds. |
|
||||
|
||||
### Web root
|
||||
|
||||
- The web server serves `public/` as the document root.
|
||||
- PHP sources, configuration, Composer dependencies, uploaded files and GPX data stay outside `public/`.
|
||||
- Runtime data is exposed through symlinks only: `public/files -> ../files` and `public/geo -> ../resources/geo`. The build must never copy uploaded media or GPX data into `public/`.
|
||||
|
||||
### Request flow
|
||||
|
||||
1. `public/index.php` → `Controller::handle()`.
|
||||
2. `Controller` reads the action parameter `a` and validates request values with `setReqVal()`, manages the session and checks the CSRF token. Then it dispatches through a `match`:
|
||||
- `dispatch()` handles public actions.
|
||||
- `dispatchAdmin()` handles actions that require `User::CLEARANCE_ADMIN`.
|
||||
3. `Livetrail` (extends `Franzz\Objects\Main`) is the application facade. It owns `Project`, `Media`, `User` and `Map`, and returns JSON through `self::getJsonResult($bSuccess, $sLangId, $asData, $asLangParams)`.
|
||||
4. With no action, `getAppMainPage()` renders `resources/masks/index.html`. It injects the app config (projects, user, constants, CSRF token) and the Vite assets read from `public/.vite/manifest.json`.
|
||||
|
||||
### Adding a backend action
|
||||
|
||||
- Add the public method to `Livetrail` (or to the domain class it delegates to) and return `getJsonResult(...)`.
|
||||
- Add the action to the right `match` in `Controller`.
|
||||
- If the action changes state, add it to `Controller::MUTATING_ACTIONS` so it requires POST + CSRF. If it writes the session, also add it to `SESSION_WRITING_ACTIONS`.
|
||||
- Read new request parameters through `setReqVal()`. Use a validation such as `'positiveInt'` where it applies.
|
||||
- Results and errors carry a translation key (`desc_lang_id`), not literal text. Add that key to **all** files in `resources/lang/`.
|
||||
|
||||
### Database
|
||||
|
||||
- The schema is declared in code in `Livetrail::getSqlOptions()` (`tables`, `types`, `constraints`, `cascading_delete`). Update it there when adding columns or tables.
|
||||
- Table names are class constants, e.g. `Project::PROJ_TABLE`, `Feed::MSG_TABLE`, `Media::MEDIA_TABLE`. Never hard-code table names outside raw SQL strings.
|
||||
- Primary/foreign keys follow `id_<singular>` and are obtained with `Db::getId(TABLE)`.
|
||||
- Use the `Db` helpers (`selectRows`, `selectColumn`, `insertRow`, `updateRow`, `deleteRow`) rather than raw SQL wherever possible.
|
||||
- Timezones: follow the "Site Time" / "Local Time" convention documented at the top of `lib/Livetrail.php`. `unix_time` is UTC, `*_time`/`*_on` timestamps are Site Time, and the `timezone` column holds Local Time.
|
||||
|
||||
### Frontend
|
||||
|
||||
- `src/app.js` creates the shared singletons and `provide`s them: `appConfig`, `api`, `lang`, `projects`, `user`. Components get them through `inject`, never through globals or imports.
|
||||
- Routing is hash-based (`#page-item-item`, separator `consts.hash_sep`), handled in `App.vue`. Routes are declared in `aoRoutes`. Lazy-load non-default pages with `defineAsyncComponent`.
|
||||
- Talk to the server only through `api.get(sAction, params)` / `api.post(sAction, params)` (`src/scripts/api.js`). This adds the timezone and CSRF token and throws an `Error` carrying `desc_lang_*` on failure.
|
||||
- Use translations through `lang.get('section.key', params)`. Placeholders are `$0`, `$1`…
|
||||
- Use the import aliases `@components`, `@scripts`, `@styles`, `@images`.
|
||||
- No `v-html` (lint error).
|
||||
|
||||
## Naming conventions
|
||||
|
||||
### Hungarian-style prefixes (PHP and JS)
|
||||
|
||||
Variables, parameters and properties carry a type prefix:
|
||||
|
||||
| Prefix | Type | Example |
|
||||
|---|---|---|
|
||||
| `s` | string | `$sCodeName`, `sAction` |
|
||||
| `i` | integer / number | `$iProjectId`, `iDistance` |
|
||||
| `f` / `d` | float / decimal | `$fValue`, `$dValue` |
|
||||
| `b` | boolean | `$bSuccess`, `bSecure` |
|
||||
| `o` | object / instance | `$oDb`, `oFeed` |
|
||||
| `a` | generic array | `$aValues` |
|
||||
| `as` | array of strings | `$asProject`, `asParams` |
|
||||
| `ai`, `ao` | array of ints / objects | `$aiCenter`, `aoCoords` |
|
||||
| `j` | JSON string | `jNewHash` |
|
||||
|
||||
Exceptions: loop counters (`i`), short comparator args (`$a`, `$b`), Vue `data`/`props`/`computed` names (plain camelCase: `postMessage`, `anchorVisible`), and keys of JSON exchanged with the backend (snake_case).
|
||||
|
||||
### PHP
|
||||
|
||||
- Classes: PascalCase, one per file, extending `Franzz\Objects\PhpObject` (call `parent::__construct(__CLASS__)`). Report errors with `$this->addError()`.
|
||||
- Methods: camelCase with verb prefixes: `get*`, `set*`, `is*`/`has*`, `add*`, `create*`, `delete*`, `check*`.
|
||||
- Constants: `UPPER_SNAKE_CASE`, grouped under short comments (`//DB Tables`, `//Spot Mode`). Table constants end in `_TABLE`.
|
||||
- Controller actions, DB columns, array keys sent to the frontend and request parameters: `snake_case` (`id_project`, `ref_type`, `last_update`).
|
||||
- New code: add parameter/return types (`string $sCodeName`, `: int`) as in `Controller`. Don't mass-retype old code.
|
||||
|
||||
### JavaScript / Vue
|
||||
|
||||
- Component files: PascalCase, named after their parent area, for example:
|
||||
- `App*` for generic UI (`AppIcon`, `AppButton`)
|
||||
- `Project*` for the project page (`ProjectFeed`, `ProjectPost`)
|
||||
- `Admin*` for the admin page
|
||||
- Templates use camelCase component tags (`<appIcon>`, `<projectMapLink>`). Import names are mixed between camelCase and PascalCase, so follow the file you are editing.
|
||||
- Script modules in `src/scripts/`: lowerCamelCase file names, each exporting a class (`export default class Api`) or named functions (`common.js`).
|
||||
- Module-level constants: `UPPER_SNAKE_CASE`.
|
||||
- Components use the Options API (`data`, `inject`, `computed`, `methods`…), not `<script setup>`.
|
||||
|
||||
### Translations
|
||||
|
||||
- Keys are nested `section.key` in snake_case (`account.invalid_email`, `error.commit_db`, `unit.days`).
|
||||
- Every key must exist in `en.json`, `es.json` and `fr.json`.
|
||||
|
||||
### SCSS
|
||||
|
||||
- Partials are prefixed with `_` and named by scope with dots: `_page.project.panel.feed.scss`, `_color.scss`, `_var.scss`.
|
||||
- Register them in `livetrail.scss` with `@use`.
|
||||
- Use the shared variables (`var.$elem-spacing`, `var.$block-radius`, `color.$post`…) instead of literal values.
|
||||
- Use kebab-case for CSS ids/classes and SCSS variables (`#feed-panel`, `.post-item`, `$trans-quick`).
|
||||
|
||||
## Code style
|
||||
|
||||
Both linters codify existing style. Their configs explain each rule.
|
||||
|
||||
- **Tabs** for indentation, LF line endings.
|
||||
- **Single quotes** in PHP and JS.
|
||||
- **No space** before control-structure parentheses: `if(...)`, `foreach(...)`, `switch(...)`, `function(x)`.
|
||||
- PHP:
|
||||
- Opening brace on the same line for classes and functions.
|
||||
- Short array syntax `[]`.
|
||||
- No spaces around the `.` concatenation operator.
|
||||
- `else`/`elseif` on a new line after `}`.
|
||||
- JS:
|
||||
- Semicolons, except after the `export default {...}` of a Vue SFC.
|
||||
- `{a, b}` without padding for objects; `import { x } from 'y'` with padding.
|
||||
- Short one-statement `if` without braces is common: `if($x) return;`.
|
||||
- Comments: `//Capitalized comment` with no space after `//`. Keep them sparse and explain the *why*.
|
||||
|
||||
## Development workflow
|
||||
|
||||
### Commands
|
||||
|
||||
| Task | Command |
|
||||
|---|---|
|
||||
| Dev build (watch) | `npm run dev`: development assets into `public/assets/`, also runs ESLint (reports issues, never blocks the watcher) |
|
||||
| Production build | `npm run prod`: hashed assets into `public/assets/`, aborts on any ESLint error |
|
||||
| JS/Vue lint | `npm run lint` |
|
||||
| PHP lint / fix | `composer lint` (php-cs-fixer dry run) / `composer lint-fix` (applies it). Needs the dev dependencies below. |
|
||||
| Local dev with sibling `objects` repo | `COMPOSER=composer.dev.json composer update` |
|
||||
|
||||
### Working with the `objects` library
|
||||
|
||||
- `composer.dev.json` symlinks `vendor/franzz/objects` to `../objects` and autoloads that namespace from the local source path. It also installs the dev dependencies (php-cs-fixer).
|
||||
- Production uses `composer.json`, which installs `franzz/objects` from its Git repository.
|
||||
- Commit and publish `objects` changes **before** updating or deploying a Livetrail version that relies on them.
|
||||
|
||||
### Before committing
|
||||
|
||||
- Run `composer lint` and `npm run lint` manually and fix any issues.
|
||||
- There is no automated test suite. Verify changes by running the app locally.
|
||||
|
||||
## Rules for agents
|
||||
|
||||
- Never edit generated or runtime content:
|
||||
- `public/assets/`, `public/.vite/`, `vendor/`, `node_modules/`
|
||||
- `files/`, `resources/geo/*.geojson`
|
||||
- Never read, open, search or print `config/settings.php`: it contains credentials and API tokens. To learn which settings exist, read `config/settings-sample.php` instead. It declares the same `Settings` constants with empty values.
|
||||
- Don't copy media or GPX data into `public/`. They are exposed through symlinks only.
|
||||
- Don't commit secrets. New settings go into `config/settings-sample.php` as empty constants.
|
||||
- Pushing to `master` deploys to production (`.gitea/workflows/deploy.yml`). Never push without explicit approval.
|
||||
- Keep changes in the style of the surrounding code. Don't reformat unrelated lines.
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")/../public" || exit 1 #Execute from public folder
|
||||
php -f index.php a=update_project > /dev/null
|
||||
|
||||
#Crontab job: 0 * * * * /path/to/livetrail/cli/cron.sh > /dev/null
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "franzz/livetrail",
|
||||
"description": "LiveTrail",
|
||||
"type": "project",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../objects",
|
||||
"options": {
|
||||
"symlink": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.5",
|
||||
"franzz/objects": "dev-vue",
|
||||
"phpmailer/phpmailer": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.75"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Franzz\\Livetrail\\": "lib/",
|
||||
"Franzz\\Objects\\": "../objects/inc/"
|
||||
},
|
||||
"files": [
|
||||
"config/settings.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "php-cs-fixer fix --dry-run --diff",
|
||||
"lint-fix": "php-cs-fixer fix"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,23 @@
|
||||
{
|
||||
"name": "franzz/livetrail",
|
||||
"description": "LiveTrail",
|
||||
"name": "franzz/spot",
|
||||
"description": "Spotty",
|
||||
"type": "project",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"repositories": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://git.lutran.fr/franzz/objects"
|
||||
"type": "path",
|
||||
"url": "../objects"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.5",
|
||||
"franzz/objects": "dev-vue",
|
||||
"phpmailer/phpmailer": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.75"
|
||||
"franzz/objects": "dev-composer",
|
||||
"phpmailer/phpmailer": "^6.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Franzz\\Livetrail\\": "lib/"
|
||||
"Franzz\\Spot\\": "inc/"
|
||||
},
|
||||
"files": [
|
||||
"config/settings.php"
|
||||
"settings.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "php-cs-fixer fix --dry-run --diff",
|
||||
"lint-fix": "php-cs-fixer fix"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName livetrail.localhost
|
||||
DocumentRoot /var/www/html/
|
||||
|
||||
DirectoryIndex index.php
|
||||
|
||||
# Serve http://livetrail.localhost/ from the public web root.
|
||||
Alias /livetrail /var/www/html/livetrail/public
|
||||
|
||||
<Directory /var/www/html/livetrail/public>
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
|
||||
AddType application/geo+json .geojson
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.geojson$">
|
||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_brotli.c>
|
||||
AddOutputFilterByType BROTLI_COMPRESS \
|
||||
text/html \
|
||||
text/plain \
|
||||
text/css \
|
||||
text/javascript \
|
||||
text/xml \
|
||||
application/javascript \
|
||||
application/json \
|
||||
application/geo+json \
|
||||
application/manifest+json \
|
||||
application/xml \
|
||||
image/svg+xml
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE \
|
||||
text/html \
|
||||
text/plain \
|
||||
text/css \
|
||||
text/javascript \
|
||||
text/xml \
|
||||
application/javascript \
|
||||
application/json \
|
||||
application/geo+json \
|
||||
application/manifest+json \
|
||||
application/xml \
|
||||
image/svg+xml
|
||||
</IfModule>
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
||||
# Geo (WMTS cache service). Told apart from Live Trail by host name;
|
||||
# *.localhost resolves to 127.0.0.1 without touching /etc/hosts.
|
||||
# Needs: a2enmod rewrite headers proxy_fcgi setenvif
|
||||
<VirtualHost *:80>
|
||||
ServerName geo.localhost
|
||||
|
||||
# The project root also holds vendor/, config/settings.php, lib/ and .git,
|
||||
# so only index.php and favicon.ico are reachable; everything else is
|
||||
# rewritten to the front controller below.
|
||||
DocumentRoot /var/www/html/geo
|
||||
|
||||
<Directory /var/www/html/geo>
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# CORS: Live Trail (browser) calls Geo cross-origin. "always" so the headers
|
||||
# are also sent on 403/404/5xx, otherwise the browser reports a CORS error
|
||||
# instead of the real status.
|
||||
<IfModule mod_headers.c>
|
||||
Header always set Access-Control-Allow-Origin "http://livetrail.localhost"
|
||||
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
||||
Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
|
||||
Header always set Access-Control-Max-Age "86400"
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine on
|
||||
|
||||
# Preflight is answered here, without hitting PHP
|
||||
RewriteCond %{REQUEST_METHOD} =OPTIONS
|
||||
RewriteRule ^ - [R=204,END]
|
||||
|
||||
# Every request (?a=tile&id=...&x=..&y=..&z=..) goes through the front controller
|
||||
RewriteCond %{REQUEST_URI} !=/favicon.ico
|
||||
RewriteCond %{REQUEST_URI} !=/index.php
|
||||
RewriteRule ^ /index.php [END,QSA]
|
||||
</VirtualHost>
|
||||
@@ -1,102 +0,0 @@
|
||||
# Live Trail (front controller in public/) and Geo (WMTS cache service).
|
||||
# Both share port 80 and are told apart by host name; *.localhost resolves to
|
||||
# 127.0.0.1 in browsers and curl without touching /etc/hosts.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name livetrail.localhost;
|
||||
|
||||
root /var/www/html/livetrail/public;
|
||||
index index.php;
|
||||
|
||||
# Matches post_max_size / upload_max_filesize in php.ini
|
||||
client_max_body_size 4G;
|
||||
|
||||
# Compression (text/html is always compressed once gzip is on). Already
|
||||
# compressed formats (images, woff/woff2, video, zip) are left out.
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/json
|
||||
application/geo+json
|
||||
application/manifest+json
|
||||
application/xml
|
||||
application/gpx+xml
|
||||
application/xhtml+xml
|
||||
image/svg+xml
|
||||
image/x-icon;
|
||||
|
||||
# Stock mime.types lacks these two, so they would be served as
|
||||
# application/octet-stream and skipped by gzip_types.
|
||||
include mime.types;
|
||||
types {
|
||||
application/geo+json geojson;
|
||||
application/gpx+xml gpx;
|
||||
}
|
||||
|
||||
# Serve pre-compressed file.ext.gz when it exists next to file.ext
|
||||
# (no-op otherwise; needs ngx_http_gzip_static_module).
|
||||
gzip_static on;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||
|
||||
# max_execution_time = 300
|
||||
fastcgi_read_timeout 300;
|
||||
}
|
||||
|
||||
# Never serve dotfiles (.git, .env, .vite manifest dir...)
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name geo.localhost;
|
||||
|
||||
# The project root also holds vendor/, config/settings.php, lib/ and .git,
|
||||
# so only index.php and favicon.ico are reachable; everything else is 404.
|
||||
root /var/www/html/geo;
|
||||
|
||||
location = /favicon.ico {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# CORS: Live Trail (browser) calls Geo cross-origin. "always" so the headers
|
||||
# are also sent on 403/404/5xx, otherwise the browser reports a CORS error
|
||||
# instead of the real status.
|
||||
add_header Access-Control-Allow-Origin "http://livetrail.localhost" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
|
||||
add_header Access-Control-Max-Age 86400 always;
|
||||
|
||||
location / {
|
||||
# Preflight is answered here, without hitting PHP
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Every request (?a=tile&id=...&x=..&y=..&z=..) goes through the front controller
|
||||
include fastcgi.conf;
|
||||
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
fastcgi_param SCRIPT_NAME /index.php;
|
||||
fastcgi_read_timeout 300;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
class Settings {
|
||||
public const DB_SERVER = 'localhost';
|
||||
public const DB_LOGIN = '';
|
||||
public const DB_PASS = '';
|
||||
public const DB_NAME = 'livetrail';
|
||||
public const DB_ENC = 'utf8mb4';
|
||||
public const TEXT_ENC = 'UTF-8';
|
||||
public const TIMEZONE = 'Europe/Zurich';
|
||||
public const MAIL_SERVER = '';
|
||||
public const MAIL_FROM = '';
|
||||
public const MAIL_USER = '';
|
||||
public const MAIL_PASS = '';
|
||||
public const WEATHER_TOKEN = ''; //visualcrossing.com
|
||||
public const TIMEZONE_USER = ''; //geonames.org
|
||||
public const DEBUG = true;
|
||||
public const LOG_FOLDER = __DIR__;
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// ESLint flat config codifying the style already in use across src/.
|
||||
// Goal: catch real mistakes and enforce the existing conventions, not impose
|
||||
// an external style guide. Rules were derived by sampling src/scripts/*.js
|
||||
// and src/components/*.vue, not from a template.
|
||||
import js from '@eslint/js';
|
||||
import vue from 'eslint-plugin-vue';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...vue.configs['flat/essential'],
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
//Indentation: tabs everywhere, one indent per level
|
||||
'indent': 'off', //too many false positives on Vue template attr wrapping; keep manual
|
||||
'no-tabs': 'off',
|
||||
|
||||
//Strings: single quotes (project uses single-quoted strings exclusively)
|
||||
'quotes': ['warn', 'single', {avoidEscape: true, allowTemplateLiterals: true}],
|
||||
|
||||
//Semicolons: required almost everywhere. The one consistent
|
||||
//exception is the top-level `export default {...}` in Vue SFCs,
|
||||
//which never gets a trailing semicolon - ESLint's `semi` rule
|
||||
//can't carve that one spot out, so this is 'warn' rather than
|
||||
//'error' to avoid flagging every component's SFC boilerplate
|
||||
//as a hard failure.
|
||||
'semi': ['warn', 'always'],
|
||||
|
||||
//Equality: codebase mixes == and === deliberately (e.g. loose checks
|
||||
//against numeric strings from the API) - don't force either
|
||||
'eqeqeq': 'off',
|
||||
|
||||
//Function parens: no space before the parameter list - `function(x)`, not `function (x)`
|
||||
'space-before-function-paren': ['warn', 'never'],
|
||||
|
||||
//Control structures: no space before the parenthesis -
|
||||
//`if(x)`, `for(...)`, `switch(x)`, not `if (x)` etc.
|
||||
//(100% consistent across src/ - checked via grep before writing this)
|
||||
'keyword-spacing': ['warn', {
|
||||
after: true,
|
||||
overrides: {
|
||||
if: {after: false},
|
||||
for: {after: false},
|
||||
while: {after: false},
|
||||
switch: {after: false},
|
||||
catch: {after: false}
|
||||
}
|
||||
}],
|
||||
'space-before-blocks': ['warn', 'always'],
|
||||
|
||||
//Object-curly-spacing is deliberately NOT configured: the codebase
|
||||
//consistently uses `{a, b}` (no padding) for object literals and
|
||||
//destructuring, but `import { x } from 'y'` (padded) for imports -
|
||||
//and this rule can't apply different spacing to those two node
|
||||
//kinds, so enforcing either would misfire on the other.
|
||||
'object-curly-spacing': 'off',
|
||||
'array-bracket-spacing': ['warn', 'never'],
|
||||
|
||||
//Prefix convention (Hungarian-ish: sString, iInt, aArray, oObject, bBool)
|
||||
//is a project-wide naming discipline, not something ESLint can check;
|
||||
//left undocumented here on purpose.
|
||||
|
||||
//Real-bug catchers - keep these strict regardless of style
|
||||
//ignoreRestSiblings covers the `const {prev, ...rest} = obj` idiom
|
||||
//(destructuring a key out just to exclude it from the rest), used
|
||||
//in App.vue - that `prev` binding is intentionally unused.
|
||||
'no-unused-vars': ['warn', {argsIgnorePattern: '^_', ignoreRestSiblings: true}],
|
||||
'no-undef': 'error',
|
||||
'no-var': 'warn', //codebase is ES module/class based; var only appears in one legacy fallback block
|
||||
'prefer-const': 'off', //not consistently followed, don't force churn
|
||||
|
||||
//Vue-specific: components are registered and used with camelCase
|
||||
//tags in templates (<appIcon>, <projectMapLink>). ESLint's
|
||||
//component-name-in-template-casing rule only supports PascalCase
|
||||
//or kebab-case, neither of which matches, so it's left off here
|
||||
//rather than forced into a casing the codebase doesn't use.
|
||||
'vue/component-name-in-template-casing': 'off',
|
||||
'vue/multi-word-component-names': 'off', //AppIcon, Admin, Project etc. mix single/multi-word by design
|
||||
'vue/attribute-hyphenation': 'off', //existing templates mix camelCase and kebab-case attrs; not consistent enough to enforce yet
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-v-html': 'error' //codebase currently has zero v-html usage - keep it that way
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['public/**', 'vendor/**', 'node_modules/**']
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE TABLE `maps` (
|
||||
`id_map` int(10) UNSIGNED auto_increment,
|
||||
`codename` VARCHAR(100),
|
||||
`geo_name` VARCHAR(100),
|
||||
`min_zoom` TINYINT UNSIGNED,
|
||||
`max_zoom` TINYINT UNSIGNED,
|
||||
`attribution` VARCHAR(100),
|
||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id_map`));
|
||||
|
||||
CREATE TABLE `mappings` (
|
||||
`id_mapping` int(10) UNSIGNED auto_increment,
|
||||
`id_map` int(10) UNSIGNED,
|
||||
`id_project` int(10) UNSIGNED,
|
||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id_mapping`));
|
||||
|
||||
ALTER TABLE mappings ADD INDEX(`id_map`);
|
||||
ALTER TABLE mappings ADD FOREIGN KEY (`id_map`) REFERENCES maps(`id_map`);
|
||||
ALTER TABLE mappings ADD INDEX(`id_project`);
|
||||
ALTER TABLE mappings ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
||||
|
||||
INSERT INTO maps(codename, geo_name, min_zoom, max_zoom, attribution) VALUES
|
||||
/*1*/('satellite', 'mapbox.satellite-streets', 0, 19, ''),
|
||||
/*2*/('otm', 'opentopomap', 2, 19, ''),
|
||||
/*3*/('ign_france', 'ign.fr', 2, 19, ''),
|
||||
/*4*/('ign_spain', 'ign.es', 1, 20, ''),
|
||||
/*5*/('linz', 'linz', 0, 17, 'Sourced from LINZ. CC BY 4.0'),
|
||||
/*6*/('usgs', 'usgs', 1, 16, ''),
|
||||
/*7*/('natgeo', 'natgeo.pct', 5, 14, '');
|
||||
|
||||
INSERT INTO mappings(id_map, id_project) VALUES
|
||||
(1, NULL),
|
||||
(2, NULL),
|
||||
(3, 2),
|
||||
(4, 2),
|
||||
(5, 1),
|
||||
(6, 3);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD gravatar LONGTEXT AFTER email;
|
||||
@@ -0,0 +1,50 @@
|
||||
ALTER TABLE medias ADD timezone CHAR(64) AFTER posted_on;
|
||||
ALTER TABLE messages ADD timezone CHAR(64) AFTER site_time;
|
||||
ALTER TABLE posts ADD timezone CHAR(64) AFTER site_time;
|
||||
|
||||
UPDATE messages
|
||||
SET iso_time = DATE_FORMAT(CONVERT_TZ(LEFT(iso_time, 19),'+00:00','+02:00'), '%Y-%m-%dT%T+0200'),
|
||||
led = led
|
||||
WHERE id_feed = 2;
|
||||
|
||||
UPDATE messages
|
||||
INNER JOIN feeds ON feeds.id_feed = messages.id_feed
|
||||
INNER JOIN projects ON projects.id_project = feeds.id_project
|
||||
SET messages.timezone = projects.timezone,
|
||||
messages.led = messages.led;
|
||||
|
||||
UPDATE posts
|
||||
SET timezone = 'Europe/Paris',
|
||||
led = led;
|
||||
|
||||
UPDATE posts
|
||||
INNER JOIN projects ON projects.id_project = posts.id_project
|
||||
SET posts.id_user = 1,
|
||||
posts.timezone = projects.timezone,
|
||||
posts.led = posts.led
|
||||
WHERE posts.name IN ('francois', 'françois','Francois', 'François', 'franzz');
|
||||
|
||||
UPDATE posts
|
||||
SET timezone = 'Pacific/Auckland',
|
||||
led = led
|
||||
WHERE name = 'nz';
|
||||
|
||||
UPDATE posts
|
||||
SET timezone = 'Atlantic/Madeira',
|
||||
led = led
|
||||
WHERE id_post IN (141, 142);
|
||||
|
||||
UPDATE medias
|
||||
INNER JOIN projects ON projects.id_project = medias.id_project
|
||||
SET medias.timezone = projects.timezone,
|
||||
medias.led = medias.led;
|
||||
|
||||
UPDATE medias
|
||||
SET timezone = 'Atlantic/Madeira',
|
||||
taken_on = posted_on,
|
||||
led = led
|
||||
WHERE id_media IN (64, 65);
|
||||
|
||||
ALTER TABLE projects DROP COLUMN timezone;
|
||||
|
||||
UPDATE maps SET attribution = 'OpenTopoMap (CC-BY-SA)' WHERE id_map = 2;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users ADD clearance TINYINT(1) DEFAULT 0 AFTER active;
|
||||
UPDATE users SET clearance = 9 WHERE email = 'francois.lutran@gmail.com';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE messages ADD posted_on TIMESTAMP DEFAULT 0 AFTER battery_state;
|
||||
UPDATE messages SET posted_on = led, led = led;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE messages ADD weather_icon VARCHAR(30) AFTER posted_on;
|
||||
ALTER TABLE messages ADD weather_cond VARCHAR(30) AFTER weather_icon;
|
||||
ALTER TABLE messages ADD weather_temp DECIMAL(3,1) AFTER weather_cond;
|
||||
@@ -0,0 +1,4 @@
|
||||
INSERT INTO maps (codename, geo_name, min_zoom, max_zoom, attribution) VALUES ('outdoors', 'mapbox.outdoors', 0, 19, '');
|
||||
ALTER TABLE maps ADD COLUMN tile_size SMALLINT UNSIGNED DEFAULT 256 AFTER geo_name;
|
||||
UPDATE maps SET tile_size = 512 WHERE geo_name = 'mapbox.outdoors';
|
||||
UPDATE maps SET tile_size = 512 WHERE geo_name = 'mapbox.satellite-streets';
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE maps ADD pattern VARCHAR(200) NOT NULL AFTER geo_name;
|
||||
UPDATE maps SET pattern = CONCAT('http://localhost/geo/?a=tile&id=', geo_name, '&z={z}&x={x}&y={y}') WHERE geo_name <> '';
|
||||
|
||||
ALTER TABLE maps ADD token VARCHAR(4096) AFTER pattern;
|
||||
UPDATE maps SET token = '';
|
||||
|
||||
ALTER TABLE maps DROP geo_name;
|
||||
|
||||
INSERT INTO maps (codename, pattern, token, tile_size, min_zoom, max_zoom, attribution)
|
||||
VALUES ('static', 'http://localhost/geo/?a=tile&id=static&z=13&x={x}&y={y}', '', 400, 13, 13, '');
|
||||
|
||||
INSERT INTO maps (codename, pattern, token, tile_size, min_zoom, max_zoom, attribution)
|
||||
VALUES ('static_marker', 'http://localhost/geo/?a=tile&id=static.marker&z=13&x={x}&y={y}&marker=http://localhost/spot/images/footprint_mapbox.png', '', 400, 13, 13, '');
|
||||
|
||||
UPDATE maps SET max_zoom = 17 WHERE codename = 'otm';
|
||||
@@ -0,0 +1,233 @@
|
||||
ALTER TABLE medias ADD width INT AFTER timezone;
|
||||
ALTER TABLE medias ADD height INT AFTER width;
|
||||
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (1).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (10).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (11).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (12).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (13).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (14).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (15).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (16).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (17).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (18).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (19).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (2).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (20).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (21).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (22).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (23).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (24).jpg';
|
||||
UPDATE medias SET width = 640, height = 1136 WHERE filename = 'image (25).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (26).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (27).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (28).jpg';
|
||||
UPDATE medias SET width = 960, height = 1280 WHERE filename = 'image (29).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (3).jpg';
|
||||
UPDATE medias SET width = 960, height = 1280 WHERE filename = 'image (30).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (31).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (32).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (33).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (34).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (35).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (36).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (37).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (38).jpg';
|
||||
UPDATE medias SET width = 2048, height = 1365 WHERE filename = 'image (39).jpg';
|
||||
UPDATE medias SET width = 1280, height = 960 WHERE filename = 'image (4).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (40).jpg';
|
||||
UPDATE medias SET width = 8192, height = 1856 WHERE filename = 'image (41).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (42).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (43).jpg';
|
||||
UPDATE medias SET width = 8192, height = 1856 WHERE filename = 'image (44).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (45).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (46).jpg';
|
||||
UPDATE medias SET width = 640, height = 1038 WHERE filename = 'image (47).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (48).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (49).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (5).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (50).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (51).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (52).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (53).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (54).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (55).jpg';
|
||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (56).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (57).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (58).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (59).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (6).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (60).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (61).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (62).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (63).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (7).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (8).jpg';
|
||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (9).jpg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'DSC01187[1].JPG';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'DSC01477.JPG';
|
||||
UPDATE medias SET width = 2726, height = 4089 WHERE filename = 'DSC03114.jpg';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_6011.MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = '156E315D-88AD-492C-BE97-9854FED48FF7.MOV';
|
||||
UPDATE medias SET width = 2576, height = 1932 WHERE filename = '4D06AAF9-A244-4AEF-A1B0-A3C439673840.jpeg';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '06361CBA-F514-4789-A498-47D681881DDF.MOV';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'B0A15DCC-B5BB-4BE3-ADBA-CBCB51C178F6.jpeg';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = 'F827F48B-5CC4-4DA7-A80B-FFED47F6EC60.MOV';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'EC407D2D-0ECF-46C7-BC05-73F48D152182.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '7961DA2D-288D-4B0B-B4A6-400673E23BA6.jpeg';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'B43B0160-DAB4-433A-9064-918C40744D27.MOV';
|
||||
UPDATE medias SET width = 1536, height = 2134 WHERE filename = 'C0D5CCF6-FE72-424C-A040-96D8C34FBE9E.jpeg';
|
||||
UPDATE medias SET width = 2154, height = 1850 WHERE filename = '0940EF97-4C27-4304-A663-654F0DE5FAB2.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '7597DEC0-1BD3-4B93-926B-E4D8A4B28E61.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '3A1F777D-58F6-40BA-8AAE-1EA236581BA3.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'C6AC86F9-C819-4866-AA71-A31375E4CCC4.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'B936E022-4D70-4C9E-9EAE-308FB6F91816.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'C777A1C7-7C3A-4ADC-8A5C-CEE8FB047B79.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'B76D8452-FCC4-4FCE-8686-2A4B8C7832FF.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '313F8490-E885-42F5-BB59-ACFB12250F0F.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F0C1EFDB-0A4F-4FC0-A46D-A396D8724054.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'E9339EEF-A21C-48A6-B6CC-23C3A512DA29.jpeg';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '5150FFEF-A715-4F5A-8562-1502BE778B6A.MOV';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'C8CD8B78-3EC9-4700-9A46-478556496239.jpeg';
|
||||
UPDATE medias SET width = 1536, height = 2048 WHERE filename = '3D83F7D3-B746-440F-8DD3-6DADF230AC28.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9121A78-0B21-42E2-8392-A60BCE240EFE.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '5A3755D0-81CB-43B1-9750-AB34F413D526.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F7C6FFFF-BF61-42BF-949B-72CD2CAAEFE2.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B0557309-579D-4C1A-863F-CF4898C42E77.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '2C75F8E9-C3E0-4FD5-AF2E-CE339545D6BC.jpeg';
|
||||
UPDATE medias SET width = 3891, height = 2917 WHERE filename = 'C6E18BC2-B407-4F2C-82F9-87244A9AD311.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '0A4B5812-D5CD-4CB0-989B-C95D15526F1A.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '1548CCA0-C8C8-4DC6-BD96-ABAA4589E825.jpeg';
|
||||
UPDATE medias SET width = 4685, height = 3116 WHERE filename = '7F80807F-D40F-4714-81AE-3CD7CD3FE8CD.jpeg';
|
||||
UPDATE medias SET width = 5283, height = 3519 WHERE filename = '32F5B460-958D-4A13-92BD-371BBC0DA769.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F99EEC87-C5CC-49AD-BCBF-8A14ADADD262.jpeg';
|
||||
UPDATE medias SET width = 8192, height = 1856 WHERE filename = 'C243E61F-2A31-4179-97B4-AD54C02E88D5.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '85807A81-91CB-4B10-861B-1B3C5A60F066.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'DE99B389-738F-48CB-BEF1-D448DEB16E7E.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '6BBD7291-BDFC-460F-A620-9D2F68AF637F.jpeg';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '7DB6662E-353A-4AC5-9331-D9122D956FE7.MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = '974841E5-AB13-4067-AD5F-3EDEFEAE28E4.MOV';
|
||||
UPDATE medias SET width = 1536, height = 2304 WHERE filename = '09F007C7-53BA-4214-B3AF-DB0E0CDD1994.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '6B92D63D-EDA5-4787-8AC7-4A0F65DC071F.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'BAF3BB55-E012-40CD-8D8B-27C4398C1D00.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '8FA2F4F0-F220-4E98-9BC0-ACB607FC2F8E.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'C260B1AD-BA4C-4835-B132-A529A9319D4D.jpeg';
|
||||
UPDATE medias SET width = 1536, height = 2304 WHERE filename = '92F8E35B-5FF8-414D-A627-46FF45E6CCCA.jpeg';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '33AA17D3-CFD6-426A-B2D5-CD639F53C081.MOV';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '0015713D-18A2-45A7-B26F-3954AC0979D3.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '223A23C5-71BB-4419-8765-CE51F3E69480.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '9ED663DD-0257-4211-A993-C86CDAC9066B.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '1ECF5050-8821-441C-8DF7-E93B24941F8A.jpeg';
|
||||
UPDATE medias SET width = 1536, height = 2304 WHERE filename = '68211D98-D6E7-4D7C-BB82-7E5E30884C7D.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '61747CFA-859E-4859-852F-3AE2650C7578.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F17CD761-DFDC-4E5F-97A7-C16035A8D8EB.jpeg';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = '7EE0B25A-0BC6-4BF1-9CDF-9E4547CFEAF4.MOV';
|
||||
UPDATE medias SET width = 3503, height = 2625 WHERE filename = '273092DD-F6CE-4C74-8878-A4D94A9C78F9.jpeg';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '4853B2CB-5BC4-45FE-AD02-0980953DB59A.MOV';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '789210A7-293D-4119-87EE-1B7CA0ACBE0A.jpeg';
|
||||
UPDATE medias SET width = 2665, height = 1996 WHERE filename = '52D57195-22A2-423B-AA35-05D48B889950.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F5644C0F-5244-4574-BBE1-4059794F87E1.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '7BB3D57A-D5BE-45D5-8CAC-C3172A89C67A.jpeg';
|
||||
UPDATE medias SET width = 2687, height = 2013 WHERE filename = '8345C480-1274-45DB-A990-40913DD219A7.jpeg';
|
||||
UPDATE medias SET width = 4128, height = 3096 WHERE filename = '28567B3E-A067-41AE-AE69-37DD3C98F820.jpeg';
|
||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO.jpg';
|
||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO (1).jpg';
|
||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO (2).jpg';
|
||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO (3).jpg';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled.png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (1).png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (2).png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (3).png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (4).png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (5).png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (6).png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (7).png';
|
||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (8).png';
|
||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg.png';
|
||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (1).png';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'iPhone 6S (1).jpeg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2591.JPG';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'CF0432E7-5221-433D-A681-DBABE131EE99.jpeg';
|
||||
UPDATE medias SET width = 694, height = 628 WHERE filename = 'Capture.PNG';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2232 (1).jpg';
|
||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (9).jpg';
|
||||
UPDATE medias SET width = 1154, height = 770 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO (3).jpg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2232 (2).jpg';
|
||||
UPDATE medias SET width = 2320, height = 3088 WHERE filename = '9C0965E1-6A40-4691-8127-35B2F1D6BB58.jpeg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_3077.jpg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'AF7A1DBC-E1CC-4FEE-8411-AC25BF69C0B5.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'F7FC1246-6304-4662-A5F2-230A29728EBC.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'CA5BA9A6-2A53-430A-A0BF-B8D0C0CDC983.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '12E87114-6B4B-4051-945C-9042FBBF5E3E.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'F97B5058-C7D7-4B25-B727-914A8897CE58.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'D1E626E6-6F49-4F62-A3D5-B19CBC906F7B.jpeg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_1228_DxO.jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_1268_DxO.jpg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2232.jpg';
|
||||
UPDATE medias SET width = 2475, height = 2475 WHERE filename = 'IMG_2232_DxO (1).jpg';
|
||||
UPDATE medias SET width = 2475, height = 2475 WHERE filename = 'IMG_2232_DxO.jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2317.jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2574 (1).jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2574 (2).jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2574.jpg';
|
||||
UPDATE medias SET width = 1344, height = 672 WHERE filename = 'PHOTO-2019-12-24-23-40-26.jpg';
|
||||
UPDATE medias SET width = 1512, height = 2688 WHERE filename = 'PXL_20210405_083835976.jpg';
|
||||
UPDATE medias SET width = 3840, height = 2160 WHERE filename = 'PXL_20210405_091516836.jpg';
|
||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO (1).jpg';
|
||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO (2).jpg';
|
||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO.jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (1).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (2).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (3).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (4).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (5).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (6).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (7).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (8).jpg';
|
||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945).jpg';
|
||||
UPDATE medias SET width = 1500, height = 2000 WHERE filename = 'Photo 2015-01-19 19-59-34 (P1040055).jpg';
|
||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'TEST.jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'image (64).jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'image (65).jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'image.jpg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '083932A9-4248-4208-91CC-34DCDC374E7A.jpeg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = '102ED557-69B0-41F2-9193-DF52FEEFD35C.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '45289F93-5D82-40DE-A0B1-1871317C56C1.jpeg';
|
||||
UPDATE medias SET width = 1200, height = 1600 WHERE filename = '5F97CB55-46C7-4BB7-9335-BBDFF77F1310.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '634716D8-D7E7-4128-B0D2-EEB4437411F3.jpeg';
|
||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = '6C111386-591F-450D-9794-E812FB6FF036.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '6D718B33-020D-460A-9194-D637A2590ABC.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '866196A9-5428-4E6C-8C4A-03DB284A9448.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'A68E6383-095C-4CB2-876B-4EC59DB24419.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'APPLE (1).jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'APPLE.jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E (1).jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E (2).jpeg';
|
||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E (3).jpeg';
|
||||
UPDATE medias SET width = 1152, height = 1536 WHERE filename = 'BA37E256-C0B2-4DE6-B0E0-659EB2C3411B.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'D4009C41-7B4C-42D5-9FB4-9CEC7CC1B4B0.jpeg';
|
||||
UPDATE medias SET width = 1200, height = 1600 WHERE filename = 'DDFADE5F-2785-4168-9EAB-D63818566929.jpeg';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'iPhone 6S.jpeg';
|
||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (2).png';
|
||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (3).png';
|
||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (4).png';
|
||||
UPDATE medias SET width = 200, height = 120 WHERE filename = 'ffprobeall.png';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = 'temp_60a6954ea732b.png';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = 'temp_60a695501d507.png';
|
||||
UPDATE medias SET width = 1080, height = 1920 WHERE filename = 'temp_60a695509009d.png';
|
||||
UPDATE medias SET width = 1080, height = 1920 WHERE filename = 'temp_60a695520a9a0.png';
|
||||
UPDATE medias SET width = 1080, height = 1920 WHERE filename = 'temp_60a695535d689.png';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '7DB6662E-353A-4AC5-9331-D9122D956FE7 (1).MOV';
|
||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '7DB6662E-353A-4AC5-9331-D9122D956FE7 (2).MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2584 (1).MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2584.MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (1).MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (2).MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (3).MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (4).MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (5).MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585.MOV';
|
||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'iPhone 6S.MOV';
|
||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '1620E5B9-C65D-4252-A495-18D5CAD63C6E.jpeg';
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE `projects` (
|
||||
`id_project` int(10) UNSIGNED auto_increment,
|
||||
`codename` VARCHAR(100),
|
||||
`name` VARCHAR(100),
|
||||
`active_from` DATETIME,
|
||||
`active_to` DATETIME,
|
||||
`geofile` VARCHAR(50),
|
||||
`timezone` VARCHAR(100),
|
||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id_project`));
|
||||
|
||||
INSERT INTO projects (name, codename, active_from, active_to, geofile, timezone) VALUES ('Te Araroa', 'te_araroa', '2015-12-29 00:00:00', '2016-03-05 23:59:59', 'te_araroa.geojson', 'Pacific/Auckland');
|
||||
INSERT INTO projects (name, codename, active_from, active_to, geofile, timezone) VALUES ('HRP', 'hrp', '2019-06-01 00:00:00', '2019-09-10 23:59:59', 'hrp.geojson', 'Europe/Paris');
|
||||
|
||||
ALTER TABLE feeds ADD COLUMN id_project int(10) UNSIGNED AFTER id_spot;
|
||||
ALTER TABLE feeds ADD last_update DATETIME AFTER status;
|
||||
ALTER TABLE feeds ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
||||
UPDATE feeds SET last_update = led;
|
||||
UPDATE feeds SET id_project = 1;
|
||||
|
||||
ALTER TABLE posts ADD COLUMN id_project int(10) UNSIGNED AFTER id_post;
|
||||
ALTER TABLE posts ADD timestamp DATETIME AFTER content;
|
||||
ALTER TABLE posts ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
||||
UPDATE posts SET timestamp = led;
|
||||
UPDATE posts SET id_project = 1;
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE `pictures` (
|
||||
`id_picture` int(10) UNSIGNED auto_increment,
|
||||
`id_project` int(10) UNSIGNED,
|
||||
`filename` VARCHAR(100) NOT NULL,
|
||||
`taken_on` DATETIME,
|
||||
`timestamp` DATETIME,
|
||||
`rotate` SMALLINT,
|
||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id_picture`),
|
||||
UNIQUE KEY `uni_file_name` (`filename`)
|
||||
);
|
||||
|
||||
ALTER TABLE pictures ADD INDEX(`id_project`);
|
||||
ALTER TABLE pictures ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE messages ADD iso_time VARCHAR(24) AFTER longitude;
|
||||
ALTER TABLE messages CHANGE COLUMN timestamp site_time TIMESTAMP DEFAULT 0;
|
||||
ALTER TABLE messages CHANGE COLUMN unix_timestamp unix_time INT;
|
||||
UPDATE messages SET iso_time = CONCAT(REPLACE(CONVERT_TZ(FROM_UNIXTIME(unix_time), @@session.time_zone, 'Pacific/Auckland'), ' ', 'T'), '+1200') WHERE id_feed = 1;
|
||||
|
||||
ALTER TABLE feeds MODIFY last_update TIMESTAMP DEFAULT 0;
|
||||
|
||||
ALTER TABLE projects MODIFY active_from TIMESTAMP DEFAULT 0;
|
||||
ALTER TABLE projects MODIFY active_to TIMESTAMP DEFAULT 0;
|
||||
|
||||
ALTER TABLE posts CHANGE COLUMN timestamp site_time TIMESTAMP DEFAULT 0;
|
||||
|
||||
ALTER TABLE pictures CHANGE COLUMN timestamp posted_on TIMESTAMP DEFAULT 0;
|
||||
UPDATE pictures INNER JOIN projects USING(id_project) SET taken_on = CONVERT_TZ(taken_on, projects.timezone, @@session.time_zone) where id_project = 1;
|
||||
ALTER TABLE pictures MODIFY taken_on TIMESTAMP DEFAULT 0;
|
||||
@@ -0,0 +1 @@
|
||||
UPDATE projects SET geofile = REPLACE(geofile, '.geojson', '');
|
||||
@@ -0,0 +1,4 @@
|
||||
RENAME TABLE pictures TO medias;
|
||||
ALTER TABLE medias CHANGE COLUMN id_picture id_media INT(10) UNSIGNED NOT NULL auto_increment;
|
||||
ALTER TABLE medias ADD COLUMN type VARCHAR(20) AFTER filename;
|
||||
UPDATE medias SET type = 'image';
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Remove NO_ZERO_DATE mode, checks mode with: SELECT @@SQL_MODE, @@GLOBAL.SQL_MODE; and: SET @@SQL_MODE = REPLACE(@@SQL_MODE, 'NO_ZERO_DATE', ''); */
|
||||
ALTER TABLE medias ADD comment LONGTEXT AFTER rotate;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE projects DROP COLUMN geofile;
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE `users` (
|
||||
`id_user` int(10) UNSIGNED auto_increment,
|
||||
`name` VARCHAR(100),
|
||||
`email` VARCHAR(320),
|
||||
`language` VARCHAR(2),
|
||||
`active` BOOLEAN,
|
||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id_user`),
|
||||
UNIQUE KEY `uni_email` (`email`)
|
||||
);
|
||||
|
||||
ALTER TABLE posts ADD COLUMN id_user int(10) UNSIGNED AFTER id_project;
|
||||
ALTER TABLE posts ADD INDEX(`id_user`);
|
||||
ALTER TABLE posts ADD FOREIGN KEY (`id_user`) REFERENCES users(`id_user`);
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE users ADD COLUMN timezone char(64) AFTER language;
|
||||
ALTER TABLE users MODIFY COLUMN email VARCHAR(320) NOT NULL;
|
||||
UPDATE users SET timezone = 'Europe/Paris';
|
||||
|
||||
ALTER TABLE projects MODIFY COLUMN timezone char(64);
|
||||
@@ -0,0 +1,265 @@
|
||||
// ==UserScript==
|
||||
// @name GaiaGps Track Colors Mass Updater
|
||||
// @description Add missing feature: Mass update of GaiaGps Track Colors
|
||||
// @version 1.1
|
||||
// @grant none
|
||||
// @match https://www.gaiagps.com/datasummary/tracks/
|
||||
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.0/jquery.min.js
|
||||
// @namespace https://greasyfork.org/users/583371
|
||||
// ==/UserScript==
|
||||
|
||||
console.log('computes');
|
||||
|
||||
function init() {
|
||||
$Selector = $('<div class="MuiPaper-root MuiMenu-paper MuiPopover-paper MuiPaper-elevation8 MuiPaper-rounded" style="opacity: 1; transform: none; min-width: 40px; transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, transform 67ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; top: 440px; left: 114px; transform-origin: 0px 0px 0px;" tabindex="-1"><ul class="MuiList-root MuiMenu-list MuiList-padding" role="listbox" tabindex="-1" style="display: flex; flex-wrap: wrap; width: 217px; flex-grow: 1; justify-content: center;"><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#FFEF00" data-value="#FFEF00"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(255, 239, 0); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#A4A4A4" data-value="#A4A4A4"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(164, 164, 164); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#000000" data-value="#000000"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(0, 0, 0); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#BAF200" data-value="#BAF200"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(186, 242, 0); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#FFC900" data-value="#FFC900"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(255, 201, 0); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#7AD915" data-value="#7AD915"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(122, 217, 21); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#784D3E" data-value="#784D3E"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(120, 77, 62); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#F42410" data-value="#F42410"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(244, 36, 16); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#E834EC" data-value="#E834EC"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(232, 52, 236); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#A726BC" data-value="#A726BC"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(167, 38, 188); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="#5726C2" data-value="#5726C2"><div style="position: relative; height: 24px; width: 24px; background-color: rgb(87, 38, 194); border-radius: 4px;"></div></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root Mui-selected MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button Mui-selected" tabindex="0" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8pxLine truncated
|
||||
|
||||
$Selector.css('z-index', '10000').find('li').click(setColor);
|
||||
//Background
|
||||
$Background = $('<div>', {'aria-hidden':'true', 'style':'position: fixed; inset: 0px; background-color: transparent;'}).click(function(){toggleSelector(false);});
|
||||
|
||||
//Add Selector & background to DOM
|
||||
$Selector.add($Background).hide().appendTo('body');
|
||||
|
||||
//Extra style
|
||||
$('body').append($('<style>').text('@keyframes rainbow { 0%, 100% {background-color: red;} 17% {background-color: orange;} 33% {background-color: yellow;} 50% {background-color: green;} 67% {background-color: blue;} 83% {background-color: purple;} }'));
|
||||
|
||||
//Button for mass update
|
||||
$MassUpdate = $('<button>', {'type':'button', 'class':'ga-button ga-button-flat ga-toolbar-button', 'disabled':'disabled'})
|
||||
.data('track-id', 'mass')
|
||||
.click(function(){toggleSelector(true, $(this));})
|
||||
.append($('<i>', {'class':'material-icons ibmid'}).text('color_lens'))
|
||||
.append(' ')
|
||||
.append($('<span>', {'class':'ibmid ga-toolbar-button-text'}).text('Change Color'));
|
||||
|
||||
$('#DatasummaryController .ga-toolbar .ga-toolbar-left .ga-button').eq(1).after($MassUpdate);
|
||||
|
||||
//Event on checkboxes to enable mass icon button
|
||||
$('.ngGrid .col0 input[type="checkbox"]').change(function(){
|
||||
$MassUpdate.prop('disabled', ($('.ngGrid .col0 input[type="checkbox"]:checked').length == 0));
|
||||
});
|
||||
|
||||
//DOM Elements
|
||||
$Grid = $('.ngGrid');
|
||||
$Head = $Grid.find('.ngTopPanel');
|
||||
$Body = $Grid.find('.ngViewport');
|
||||
|
||||
iColorColLeft = $Grid.find('.colt0').width();
|
||||
iCol1Width = $Grid.find('.ngHeaderCell.ng-scope.col1.colt1').width();
|
||||
iCol1Left = parseInt($Grid.find('.ngHeaderCell.ng-scope.col1.colt1').css('left'));
|
||||
|
||||
//Insert column header
|
||||
$Head.find('.ngHeaderContainer .ngHeaderCell.col0').after(
|
||||
$('<div>', {'class':'ngHeaderCell ng-scope col0-1 colt0-1', 'style':'width:'+iColorColWidth+'px; left:'+iColorColLeft+'px;'})
|
||||
.append($('<div>', {'class':'ngVerticalBar ngVerticalBarVisible', 'style':'height: '+iColorColHeight+'px;'}).text(' '))
|
||||
.append($('<div>')
|
||||
.append($('<div>', {'class':'ngHeaderSortColumn ngSorted', 'style':'cursor: pointer;'})
|
||||
.append($('<div>', {'class':'ngHeaderText ng-binding', 'style':'width:'+iColorColWidth+'px;'}).text('Color'))
|
||||
)
|
||||
)
|
||||
);
|
||||
$Head.find('.ngHeaderCell.colt1').width(iCol1Width - iColorColWidth);
|
||||
$Head.find('.col1').width(iCol1Width - iColorColWidth).css('left', (iCol1Left + iColorColWidth)+'px');
|
||||
|
||||
//Update Colors
|
||||
update();
|
||||
|
||||
//Event on DOM change: First track of the page
|
||||
oObserver = new MutationObserver((oMutations) => {update();});
|
||||
oObserver.observe($('.ngGrid .ngCanvas .ngCell.col1 .ngCellText.col1 .ng-binding')[0], {attributes: true});
|
||||
}
|
||||
|
||||
function wait(fIsFinished, fAction) {
|
||||
console.log('waiting for page to load');
|
||||
if(typeof oWaitTimer != "undefined") clearTimeout(oWaitTimer);
|
||||
if(fIsFinished()) fAction();
|
||||
else { oWaitTimer = setTimeout(function(){wait(fIsFinished, fAction);}, 100); }
|
||||
}
|
||||
|
||||
function update(bWaited) {
|
||||
bWaited = bWaited || false;
|
||||
|
||||
if(!bWaited) {
|
||||
if(typeof oTimer != "undefined") clearTimeout(oTimer);
|
||||
oTimer = setTimeout((() => {update(true);}), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
oObserver.disconnect();
|
||||
oObserver.observe($('.ngGrid .ngCanvas .ngCell.col1 .ngCellText.col1 .ng-binding')[0], {attributes: true});
|
||||
|
||||
console.log('updating');
|
||||
|
||||
$Body.find('.ngRow').each(function(key, elem){
|
||||
var $Row = $(elem);
|
||||
var sTrackId = $Row.find('.col1 a.ng-binding').attr('href').split('/')[3];
|
||||
|
||||
setTrackInfo(sTrackId, function(oData){refreshRow(oData.row, oData.track.id);}, {row:$Row});
|
||||
});
|
||||
}
|
||||
|
||||
function setTrackInfo(sTrackId, fOnDone, oVars) {
|
||||
oVars = oVars || {};
|
||||
if(!asTracks[sTrackId].post) {
|
||||
$.get({url:'https://www.gaiagps.com/api/objects/track/'+sTrackId+'/', id:sTrackId})
|
||||
.done(function(asTrack) {
|
||||
asTracks[this.id].post = asTrack;
|
||||
oVars.track = asTracks[this.id];
|
||||
fOnDone(oVars);
|
||||
})
|
||||
.fail(function(){
|
||||
setTrackInfo(sTrackId, fOnDone, oVars);
|
||||
});
|
||||
}
|
||||
else {
|
||||
oVars.track = asTracks[sTrackId];
|
||||
fOnDone(oVars);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshRow($Row, sTrackId) {
|
||||
//Color
|
||||
var sColor = asTracks[sTrackId].post.features[0].properties.hexcolor;
|
||||
if(!sColor) sColor = 'rgb(0,0,0)';
|
||||
|
||||
//Add Color
|
||||
if(!$Row.find('.ngCell.col0-1.colt0-1').length) {
|
||||
//Move Col1
|
||||
$Row.find('.colt1').width(iCol1Width - iColorColWidth);
|
||||
$Row.find('.col1').width(iCol1Width - iColorColWidth).css('left', (iCol1Left + iColorColWidth)+'px');
|
||||
|
||||
$Row.find('.ngCell.col0.colt0').after(
|
||||
$('<div>', {'class':'ngCell col0-1 colt0-1', 'style':'cursor: pointer; width:'+iColorColWidth+'px; left:'+iColorColLeft+'px;'})
|
||||
.data('color', sColor)
|
||||
.data('track-id', sTrackId)
|
||||
.append($('<div>', {'class':'ngVerticalBar ngVerticalBarVisible', 'style':'height: '+iColorColHeight+'px;'}))
|
||||
.append($('<div>')
|
||||
.append($('<div>', {'style':'ngSelectionCell'})
|
||||
.append($('<div>', {'class':'track-color', 'style':'position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); height: 24px; width: 24px; background-color: '+sColor+'; border-radius: 4px;'}))
|
||||
)
|
||||
)
|
||||
//Display Selector
|
||||
.click(function(){
|
||||
toggleSelector(null, $(this));
|
||||
})
|
||||
);
|
||||
}
|
||||
//Update Color
|
||||
else {
|
||||
$Row.find('.ngCell.col0-1.colt0-1')
|
||||
.data('color', sColor)
|
||||
.data('track-id', sTrackId)
|
||||
.find('.track-color')
|
||||
.css('background', sColor);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelector(bShow, $Track){
|
||||
bShow = (bShow == null)?(!$Selector.is(':visible')):bShow;
|
||||
$Selector.add($Background).toggle(bShow);
|
||||
if(bShow) {
|
||||
$Selector
|
||||
.data('track', $Track)
|
||||
.css({top: ($Track.offset().top + iColorColHeight)+'px', left: ($Track.offset().left)+'px'})
|
||||
.find('li[data-value="'+$Track.data('color')+'"]').addClass('Mui-selected');
|
||||
}
|
||||
else {
|
||||
$Selector
|
||||
.find('li.Mui-selected').removeClass('Mui-selected');
|
||||
}
|
||||
return bShow;
|
||||
}
|
||||
|
||||
function getTrackDOm(sTrackId) {
|
||||
var $Link = $('a[href="/datasummary/track/'+sTrackId+'/"]');
|
||||
return ($Link.length == 0)?null:$Link.parents('.ngRow').find('.ngCell.col0-1');
|
||||
}
|
||||
|
||||
function setColor(){
|
||||
var sColor = $(this).attr('data-value');
|
||||
|
||||
//List of tracks to change (selected checkboxes or just the one)
|
||||
a$Tracks = [];
|
||||
if($Selector.data('track').data('track-id') == 'mass') {
|
||||
if($('[ng-show="selectAllFromAllPages"]').is(':visible')) {
|
||||
for(i in asTracks) {
|
||||
var $Row = getTrackDOm(asTracks[i].id);
|
||||
if(!$Row) {
|
||||
$Row = $('<div>')
|
||||
.data('track-id', asTracks[i].id)
|
||||
.append($('<div>', {'class':'track-color'}));
|
||||
}
|
||||
|
||||
a$Tracks.push($Row);
|
||||
}
|
||||
}
|
||||
else { a$Tracks = $('.ngGrid .ngViewport .ngRow .ngCell.col0 .ngSelectionCheckbox:checked').parents('.ngRow').find('.ngCell.col0-1').toArray(); }
|
||||
}
|
||||
else { a$Tracks.push($Selector.data('track')); }
|
||||
|
||||
console.log('nb of tracks to modify: '+a$Tracks.length);
|
||||
|
||||
setTrackColor(sColor, 0);
|
||||
}
|
||||
|
||||
function setTrackColor(sColor, iIndex) {
|
||||
var $Track = $(a$Tracks[iIndex]);
|
||||
|
||||
//Ajax Loader
|
||||
$Track.find('.track-color').css('animation', 'rainbow 1s infinite');
|
||||
|
||||
var sTrackId = $Track.data('track-id');
|
||||
|
||||
setTrackInfo(
|
||||
sTrackId,
|
||||
function(oData){
|
||||
asTracks[oData.track.id].post.features[0].properties.color = oData.color;
|
||||
asTracks[oData.track.id].post.features[0].properties.hexcolor = oData.color;
|
||||
|
||||
var asPost = {
|
||||
type: asTracks[oData.track.id].post.features[0].type,
|
||||
properties: asTracks[oData.track.id].post.features[0].properties,
|
||||
geometry: asTracks[oData.track.id].post.features[0].geometry,
|
||||
};
|
||||
asPost.properties.localId = oData.track.id;
|
||||
|
||||
$.ajax({
|
||||
url: 'https://www.gaiagps.com/api/objects/track/'+oData.track.id+'/',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(asPost),
|
||||
id: oData.track.id
|
||||
})
|
||||
.done(function(){
|
||||
setTrackInfo(
|
||||
this.id,
|
||||
function(oData) {
|
||||
iIndex++;
|
||||
$('[ng-show="selectAllFromAllPages"]:visible').text(iIndex+'/'+a$Tracks.length);
|
||||
var $Track = getTrackDOm(oData.track.id);
|
||||
if($Track) {
|
||||
$Track.find('.track-color').css({'background': oData.color, 'animation':'none'});
|
||||
toggleSelector(false, $Track);
|
||||
}
|
||||
if(iIndex < a$Tracks.length) setTrackColor(oData.color, iIndex);
|
||||
},
|
||||
{color:oData.color, force_update:true}
|
||||
);
|
||||
})
|
||||
.fail(()=>{
|
||||
var $Track = getTrackDOm(oData.track.id);
|
||||
if($Track) $Track.find('.track-color').css({'animation':'none'});
|
||||
setTrackColor(oData.color, iIndex);
|
||||
});
|
||||
},
|
||||
{color:sColor}
|
||||
);
|
||||
}
|
||||
|
||||
var iColorColWidth = 50, iColorColHeight = 59;
|
||||
|
||||
//Tracks
|
||||
asTracks = {};
|
||||
$.ajax({url:'https://www.gaiagps.com/api/objects/track/?routepoints=false&show_archived=false&show_filed=true'}).done((asData) => {
|
||||
for(var i in asData) { asTracks[asData[i].id] = asData[i]; }
|
||||
wait(function(){return ($('.ngViewport .ngRow').length > 0);}, init);
|
||||
});
|
||||
@@ -0,0 +1,782 @@
|
||||
// ==UserScript==
|
||||
// @name GaiaGps Uploader
|
||||
// @namespace https://greasyfork.org/users/583371
|
||||
// @description Allow the user to upload multiple files at once and more than 1000 waypoints
|
||||
// @grant none
|
||||
// @version 3.1.2
|
||||
// @author Franzz
|
||||
// @license GNU GPLv3
|
||||
// @match https://www.gaiagps.com/map/*
|
||||
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
|
||||
// ==/UserScript==
|
||||
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
//Ctrl+Alt+Shift+I to open network tab on addon
|
||||
|
||||
/* GPXParser - v3.0.8 - https://github.com/Luuka/GPXParser.js/blob/master/src/GPXParser.js */
|
||||
/* Personnal modifications have been applied, care on upgrade */
|
||||
|
||||
/**
|
||||
* GPX file parser
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
let gpxParser = function () {
|
||||
this.xmlSource = "";
|
||||
this.metadata = {};
|
||||
this.waypoints = [];
|
||||
this.tracks = [];
|
||||
this.routes = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a gpx formatted string to a GPXParser Object
|
||||
*
|
||||
* @param {string} gpxstring - A GPX formatted String
|
||||
*
|
||||
* @return {gpxParser} A GPXParser object
|
||||
*/
|
||||
gpxParser.prototype.parse = function (gpxstring) {
|
||||
let keepThis = this;
|
||||
|
||||
let domParser = new window.DOMParser();
|
||||
this.xmlSource = domParser.parseFromString(gpxstring, 'text/xml');
|
||||
|
||||
let metadata = this.xmlSource.querySelector('metadata');
|
||||
if(metadata != null){
|
||||
this.metadata.name = this.getElementValue(metadata, "name");
|
||||
this.metadata.desc = this.getElementValue(metadata, "desc");
|
||||
this.metadata.time = this.getElementValue(metadata, "time");
|
||||
|
||||
let author = {};
|
||||
let authorElem = metadata.querySelector('author');
|
||||
if(authorElem != null){
|
||||
author.name = this.getElementValue(authorElem, "name");
|
||||
author.email = {};
|
||||
let emailElem = authorElem.querySelector('email');
|
||||
if(emailElem != null){
|
||||
author.email.id = emailElem.getAttribute("id");
|
||||
author.email.domain = emailElem.getAttribute("domain");
|
||||
}
|
||||
|
||||
let link = {};
|
||||
let linkElem = authorElem.querySelector('link');
|
||||
if(linkElem != null){
|
||||
link.href = linkElem.getAttribute('href');
|
||||
link.text = this.getElementValue(linkElem, "text");
|
||||
link.type = this.getElementValue(linkElem, "type");
|
||||
}
|
||||
author.link = link;
|
||||
}
|
||||
this.metadata.author = author;
|
||||
|
||||
let link = {};
|
||||
let linkElem = this.queryDirectSelector(metadata, 'link');
|
||||
if(linkElem != null){
|
||||
link.href = linkElem.getAttribute('href');
|
||||
link.text = this.getElementValue(linkElem, "text");
|
||||
link.type = this.getElementValue(linkElem, "type");
|
||||
this.metadata.link = link;
|
||||
}
|
||||
}
|
||||
|
||||
var wpts = [].slice.call(this.xmlSource.querySelectorAll('wpt'));
|
||||
for (let idx in wpts){
|
||||
var wpt = wpts[idx];
|
||||
let pt = {};
|
||||
pt.name = keepThis.getElementValue(wpt, "name");
|
||||
pt.lat = parseFloat(wpt.getAttribute("lat"));
|
||||
pt.lon = parseFloat(wpt.getAttribute("lon"));
|
||||
//let floatValue = parseFloat(keepThis.getElementValue(wpt, "ele"));
|
||||
//pt.ele = isNaN(floatValue) ? null : floatValue;
|
||||
pt.ele = parseFloat(keepThis.getElementValue(wpt, "ele")) || null;
|
||||
pt.cmt = keepThis.getElementValue(wpt, "cmt");
|
||||
pt.desc = keepThis.getElementValue(wpt, "desc");
|
||||
pt.sym = keepThis.getElementValue(wpt, "sym");
|
||||
|
||||
//let time = keepThis.getElementValue(wpt, "time");
|
||||
//pt.time = time == null ? null : new Date(time);
|
||||
pt.time = (keepThis.getElementValue(wpt, "time") || keepThis.metadata.time) || null;
|
||||
|
||||
keepThis.waypoints.push(pt);
|
||||
}
|
||||
|
||||
var rtes = [].slice.call(this.xmlSource.querySelectorAll('rte'));
|
||||
for (let idx in rtes){
|
||||
let rte = rtes[idx];
|
||||
let route = {};
|
||||
route.name = keepThis.getElementValue(rte, "name");
|
||||
route.cmt = keepThis.getElementValue(rte, "cmt");
|
||||
route.desc = keepThis.getElementValue(rte, "desc");
|
||||
route.src = keepThis.getElementValue(rte, "src");
|
||||
route.number= keepThis.getElementValue(rte, "number");
|
||||
|
||||
let type = keepThis.queryDirectSelector(rte, "type");
|
||||
route.type = type != null ? type.innerHTML : null;
|
||||
|
||||
let link = {};
|
||||
let linkElem= rte.querySelector('link');
|
||||
if(linkElem != null){
|
||||
link.href = linkElem.getAttribute('href');
|
||||
link.text = keepThis.getElementValue(linkElem, "text");
|
||||
link.type = keepThis.getElementValue(linkElem, "type");
|
||||
}
|
||||
route.link = link;
|
||||
|
||||
let routepoints = [];
|
||||
var rtepts = [].slice.call(rte.querySelectorAll('rtept'));
|
||||
|
||||
for (let idxIn in rtepts){
|
||||
let rtept = rtepts[idxIn];
|
||||
let pt = {};
|
||||
pt.lat = parseFloat(rtept.getAttribute("lat"));
|
||||
pt.lon = parseFloat(rtept.getAttribute("lon"));
|
||||
//let floatValue = parseFloat(keepThis.getElementValue(rtept, "ele"));
|
||||
//pt.ele = isNaN(floatValue) ? null : floatValue;
|
||||
pt.ele = parseFloat(keepThis.getElementValue(rtept, "ele")) || null;
|
||||
|
||||
//let time = keepThis.getElementValue(rtept, "time");
|
||||
//pt.time = time == null ? null : new Date(time);
|
||||
pt.time = (keepThis.getElementValue(rtept, "time") || keepThis.metadata.time) || null;
|
||||
|
||||
routepoints.push(pt);
|
||||
}
|
||||
|
||||
//route.distance = keepThis.calculDistance(routepoints);
|
||||
//route.elevation = keepThis.calcElevation(routepoints);
|
||||
//route.slopes = keepThis.calculSlope(routepoints, route.distance.cumul);
|
||||
route.points = routepoints;
|
||||
|
||||
keepThis.routes.push(route);
|
||||
}
|
||||
|
||||
var trks = [].slice.call(this.xmlSource.querySelectorAll('trk'));
|
||||
for (let idx in trks){
|
||||
let trk = trks[idx];
|
||||
let track = {};
|
||||
|
||||
track.name = keepThis.getElementValue(trk, "name");
|
||||
track.cmt = keepThis.getElementValue(trk, "cmt");
|
||||
track.desc = keepThis.getElementValue(trk, "desc");
|
||||
track.src = keepThis.getElementValue(trk, "src");
|
||||
track.number= keepThis.getElementValue(trk, "number");
|
||||
track.color = keepThis.getElementValue(trk, "DisplayColor");
|
||||
track.time = keepThis.metadata.time;
|
||||
|
||||
let type = keepThis.queryDirectSelector(trk, "type");
|
||||
track.type = type != null ? type.innerHTML : null;
|
||||
|
||||
let link = {};
|
||||
let linkElem= trk.querySelector('link');
|
||||
if(linkElem != null){
|
||||
link.href = linkElem.getAttribute('href');
|
||||
link.text = keepThis.getElementValue(linkElem, "text");
|
||||
link.type = keepThis.getElementValue(linkElem, "type");
|
||||
}
|
||||
track.link = link;
|
||||
|
||||
let trackpoints = [];
|
||||
let trkpts = [].slice.call(trk.querySelectorAll('trkpt'));
|
||||
for (let idxIn in trkpts){
|
||||
var trkpt = trkpts[idxIn];
|
||||
let pt = {};
|
||||
pt.lat = parseFloat(trkpt.getAttribute("lat"));
|
||||
pt.lon = parseFloat(trkpt.getAttribute("lon"));
|
||||
//let floatValue = parseFloat(keepThis.getElementValue(trkpt, "ele"));
|
||||
//pt.ele = isNaN(floatValue) ? null : floatValue;
|
||||
pt.ele = parseFloat(keepThis.getElementValue(trkpt, "ele")) || null;
|
||||
|
||||
//let time = keepThis.getElementValue(trkpt, "time");
|
||||
//pt.time = time == null ? null : new Date(time);
|
||||
pt.time = (keepThis.getElementValue(trkpt, "time") || keepThis.metadata.time) || null;
|
||||
|
||||
trackpoints.push(pt);
|
||||
}
|
||||
//track.distance = keepThis.calculDistance(trackpoints);
|
||||
//track.elevation = keepThis.calcElevation(trackpoints);
|
||||
//track.slopes = keepThis.calculSlope(trackpoints, track.distance.cumul);
|
||||
track.points = trackpoints;
|
||||
|
||||
keepThis.tracks.push(track);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get value from a XML DOM element
|
||||
*
|
||||
* @param {Element} parent - Parent DOM Element
|
||||
* @param {string} needle - Name of the searched element
|
||||
*
|
||||
* @return {} The element value
|
||||
*/
|
||||
gpxParser.prototype.getElementValue = function(parent, needle){
|
||||
let elem = parent.querySelector(needle);
|
||||
if(elem != null){
|
||||
//Get value (in case of CDATA)
|
||||
let sValue = (elem.innerHTML != undefined && elem.innerHTML.substring(0, 8) != '<![CDATA') ? elem.innerHTML : elem.childNodes[0].data;
|
||||
|
||||
//If decoded HTML, re-encode
|
||||
if(sValue.substr(0, 4)== '<') sValue = $('<div>').html(sValue).text();
|
||||
|
||||
//Strip HTML tags & trim value
|
||||
return $('<div>').html(sValue).text().trim();
|
||||
}
|
||||
return elem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search the value of a direct child XML DOM element
|
||||
*
|
||||
* @param {Element} parent - Parent DOM Element
|
||||
* @param {string} needle - Name of the searched element
|
||||
*
|
||||
* @return {} The element value
|
||||
*/
|
||||
gpxParser.prototype.queryDirectSelector = function(parent, needle) {
|
||||
|
||||
let elements = parent.querySelectorAll(needle);
|
||||
let finalElem = elements[0];
|
||||
|
||||
if(elements.length > 1) {
|
||||
let directChilds = parent.childNodes;
|
||||
|
||||
for(idx in directChilds) {
|
||||
elem = directChilds[idx];
|
||||
if(elem.tagName === needle) {
|
||||
finalElem = elem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalElem;
|
||||
};
|
||||
|
||||
|
||||
class Gaia {
|
||||
static get URL() { return 'https://www.gaiagps.com'; }
|
||||
static get API() { return Gaia.URL+'/api/objects'; }
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.asFiles = [];
|
||||
this.aoWaypoints = [];
|
||||
this.aoTracks = [];
|
||||
this.asFolders = {};
|
||||
this.progress = {current:0, total:0};
|
||||
}
|
||||
|
||||
setLayout() {
|
||||
this.reset();
|
||||
|
||||
/* FIXME: adapts on GaiaGPS upgrade */
|
||||
let $InputButton = $('a[href="https://help.gaiagps.com/hc/en-us/articles/360052763513"]').parent().find('button');
|
||||
|
||||
//If the button is found (=displayed)
|
||||
if($InputButton.length > 0) {
|
||||
this.$InputBox = $InputButton.parent();
|
||||
|
||||
//Add Feedback box
|
||||
this.$Feedback = ($('#ggu-feedback').length > 0)?$('#ggu-feedback'):($('<div>', {
|
||||
'id':'ggu-feedback',
|
||||
'style':'width:calc(100% + 64px); height:400px; margin:1em 0 0 -32px; display:inline-block; text-align:left; overflow:auto;'
|
||||
}).insertAfter($InputButton));
|
||||
|
||||
/*
|
||||
//Add Folder Name Input next to button
|
||||
this.$InputName = ($('#ggu-inputname').length > 0)?$('#ggu-inputname'):($('<input>', {
|
||||
'type':'text',
|
||||
'id': 'ggu-inputname',
|
||||
'name':'ggu-inputname',
|
||||
'placeholder':'Folder Name',
|
||||
'style': 'border-width:2px; border-color:rgba(0, 0, 0, 0.1); border-radius:4px; font-family:Inter,Helvetica Neue,Helvetica,Arial; font-size:15px; padding:8px 12px; margin-bottom:12px;'
|
||||
}).val('PCT').prependTo(this.$InputBox));
|
||||
*/
|
||||
|
||||
//Reset File Input DOM Element
|
||||
let $InputFile = $('input[type=file]');
|
||||
this.$InputFile = $InputFile.clone()
|
||||
.insertAfter($InputFile)
|
||||
.attr('multiple', 'multiple')
|
||||
.attr('name', 'files[]')
|
||||
.change(() => { this.readInputFiles(); });
|
||||
$InputFile.remove();
|
||||
|
||||
//Reset button
|
||||
this.$InputButton = $InputButton.clone()
|
||||
.insertAfter($InputButton)
|
||||
.click(() => {this.$InputFile.click();});
|
||||
$InputButton.remove();
|
||||
|
||||
//Clear all upload notifications
|
||||
this.resetNotif();
|
||||
}
|
||||
}
|
||||
|
||||
feedback(sType, sMsg) {
|
||||
let sColor = 'black';
|
||||
let sIcon = '';
|
||||
switch(sType) {
|
||||
case 'error': sColor = 'red'; sIcon = '\u274C'; break;
|
||||
case 'warning': sColor = 'orange'; sIcon = '\u26A0'; break;
|
||||
case 'info': sColor = '#2D5E38'; sIcon = '\u2713'; break;
|
||||
}
|
||||
|
||||
var sFormattedMsg = sIcon+' '+sMsg+(sMsg.slice(-1)=='.'?'':'.');
|
||||
console.log(sFormattedMsg);
|
||||
|
||||
this.$Feedback.append($('<p>', {'style': 'color: '+sColor+';'}).text(sFormattedMsg));
|
||||
this.$Feedback.scrollTop(this.$Feedback.prop("scrollHeight"));
|
||||
}
|
||||
|
||||
incProgress() {
|
||||
if(!this.progress.current) {
|
||||
this.progress.$Done = $('<div>', {'style':'overflow:hidden; background:#2D5E38; color: white; text-align:right;'});
|
||||
this.progress.$Left = $('<div>', {'style':'overflow:hidden; background:white; color: #2D5E38; text-align:left;'});
|
||||
this.progress.$Box = $('<div>', {'id':'ggu-progress', 'style':'margin-top:1em;'})
|
||||
.append($('<div>', {'style':'display:flex; width:calc(100% + 64px); margin-left:-32px; border-radius:3px; border: 1px solid #2D5E38; box-sizing: border-box;'})
|
||||
.append(this.progress.$Done)
|
||||
.append(this.progress.$Left)
|
||||
);
|
||||
this.$Feedback.before(this.progress.$Box);
|
||||
}
|
||||
|
||||
this.progress.current++;
|
||||
|
||||
if(this.progress.current < this.progress.total) {
|
||||
let iRatio = Math.round(this.progress.current / this.progress.total * 100);
|
||||
let sTextDone = '', sTextLeft = '';
|
||||
if(iRatio < 50) {
|
||||
sTextDone = '';
|
||||
sTextLeft = ' '+iRatio+'% ('+this.progress.current+' / '+this.progress.total+')';
|
||||
}
|
||||
else {
|
||||
sTextDone = '('+this.progress.current+' / '+this.progress.total+') '+iRatio+'% ';
|
||||
sTextLeft = '';
|
||||
}
|
||||
this.progress.$Done.css('flex', iRatio+' 1 0%').html(sTextDone);
|
||||
this.progress.$Left.css('flex', (100 - iRatio)+' 1 0%').html(sTextLeft);
|
||||
}
|
||||
else {
|
||||
this.progress.$Box.remove();
|
||||
this.progress = {current:0, total:0};
|
||||
}
|
||||
}
|
||||
|
||||
//Marking all upload notifications as read
|
||||
resetNotif() {
|
||||
this.feedback('info', 'Marking all upload notifications as read');
|
||||
$.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => {
|
||||
for(var i in asNotifs) {
|
||||
if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) {
|
||||
$.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Parse files from input (multiple)
|
||||
readInputFiles() {
|
||||
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
|
||||
this.feedback('error', 'File APIs are not fully supported in this browser');
|
||||
return;
|
||||
}
|
||||
else this.feedback('info', 'Parsing input files');
|
||||
|
||||
//Get Folder Name (text input)
|
||||
let aoReaders = [];
|
||||
let iCount = 0;
|
||||
this.asFiles = this.$InputFile.prop('files');
|
||||
for(var i=0 ; i < this.asFiles.length ; i++) {
|
||||
aoReaders[i] = new FileReader();
|
||||
aoReaders[i].onload = ((oFileReader) => {
|
||||
return (asResult) => {
|
||||
iCount++;
|
||||
this.feedback('info', 'Reading file "'+oFileReader.name+'" ('+iCount+'/'+this.asFiles.length+')');
|
||||
this.parseFile(oFileReader.name, asResult.target.result);
|
||||
this.asFolders[oFileReader.name] = {};
|
||||
|
||||
if(iCount == this.asFiles.length) {
|
||||
this.progress.total = this.aoTracks.length + this.aoWaypoints.length + this.asFiles.length; //extra action per file: Create folder
|
||||
this.createFolders();
|
||||
}
|
||||
};
|
||||
})(this.asFiles[i]);
|
||||
aoReaders[i].readAsText(this.asFiles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
//Parse GPX files to consolidate tracks & waypoints
|
||||
parseFile(sFileName, oContent) {
|
||||
this.feedback('info', 'Parsing file "'+sFileName+'"');
|
||||
|
||||
var oGPX = new gpxParser();
|
||||
oGPX.parse(oContent);
|
||||
|
||||
//Waypoints
|
||||
for(var w in oGPX.waypoints) {
|
||||
oGPX.waypoints[w].filename = sFileName;
|
||||
var sWaypointName = oGPX.waypoints[w].name;
|
||||
/* if(sWaypointName.indexOf('Milestone - ') != -1 && sWaypointName.substr(-2) != '00') { //1 milestone every 100 miles
|
||||
this.feedback('info', 'Ignoring milestone waypoint "'+sWaypointName+'"');
|
||||
}
|
||||
else */this.aoWaypoints.push(oGPX.waypoints[w]);
|
||||
}
|
||||
|
||||
//Tracks
|
||||
for(var t in oGPX.tracks) {
|
||||
oGPX.tracks[t].filename = sFileName;
|
||||
this.aoTracks.push(oGPX.tracks[t]);
|
||||
}
|
||||
}
|
||||
|
||||
//Delete existing folder with same name & recreating it
|
||||
createFolders() {
|
||||
let iCount = 0;
|
||||
$.each(this.asFolders, (sFileName, asFolder) => {
|
||||
|
||||
//Folder Name
|
||||
let sFolderName = sFileName.replace(/\.[^\.]+$/, '');
|
||||
|
||||
this.feedback('info', 'Looking for existing folder "'+sFolderName+'"...');
|
||||
$.get(Gaia.API+'/folder/?search='+sFolderName).done((asFolders) => {
|
||||
if(asFolders != '' && !$.isEmptyObject(asFolders)) {
|
||||
for(var f in asFolders) {
|
||||
this.feedback('info', 'Deleting "'+asFolders[f].title+'" folder');
|
||||
$.ajax({
|
||||
url: Gaia.API+'/folder/'+asFolders[f].id+'/',
|
||||
type: 'DELETE'
|
||||
});
|
||||
}
|
||||
}
|
||||
else this.feedback('info', 'No folder named "'+sFolderName+'" found');
|
||||
|
||||
this.feedback('info', 'Creating folder "'+sFolderName+'"');
|
||||
let sTime = (new Date()).toISOString();
|
||||
$.post({
|
||||
url: Gaia.API+'/folder/',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
title: sFolderName,
|
||||
imported: true
|
||||
})
|
||||
}).done((asFolder) => {
|
||||
this.feedback('info', 'Folder "'+asFolder.properties.name+'" created');
|
||||
this.asFolders[sFileName] = asFolder;
|
||||
this.incProgress();
|
||||
|
||||
iCount++;
|
||||
if(iCount == Object.keys(this.asFolders).length) this.uploadTrack();
|
||||
}).fail(() => {
|
||||
this.feedback('error', 'Folder "'+sFolderName+'" could not be created');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//Build & Upload Track File
|
||||
uploadTrack(iIndex) {
|
||||
iIndex = iIndex || 0;
|
||||
|
||||
if(iIndex == 0) this.feedback('info', 'Uploading tracks...');
|
||||
|
||||
let aoTrack = this.aoTracks[iIndex];
|
||||
this.feedback('info', 'Uploading track "'+aoTrack.name+'"');
|
||||
|
||||
let sColor = '#4ABD32';
|
||||
switch(aoTrack.color) {
|
||||
|
||||
//Personal Colors
|
||||
case 'DarkBlue': sColor = '#2D3FC7'; break;
|
||||
case 'Magenta': sColor = '#B60DC3'; break;
|
||||
|
||||
//Garmin Colors
|
||||
case 'Black': sColor = '#000000'; break;
|
||||
case 'DarkRed': sColor = '#F90553'; break;
|
||||
case 'DarkGreen': sColor = '#009B89'; break;
|
||||
case 'DarkYellow': sColor = '#DCEE0E'; break;
|
||||
//case 'DarkBlue': sColor = '#5E23CA'; break;
|
||||
case 'DarkMagenta': sColor = '#B60DC3'; break;
|
||||
case 'DarkCyan': sColor = '#00ACF8'; break;
|
||||
case 'LightGray': sColor = '#A4A4A4'; break;
|
||||
case 'DarkGray': sColor = '#577B8E'; break;
|
||||
case 'Red': sColor = '#F90553'; break;
|
||||
case 'Green': sColor = '#36C03B'; break;
|
||||
case 'Yellow': sColor = '#FFF011'; break;
|
||||
case 'Blue': sColor = '#2D3FC7'; break;
|
||||
//case 'Magenta': sColor = '#B60DC3'; break;
|
||||
case 'Cyan': sColor = '#00C3DD'; break;
|
||||
case 'White': sColor = '#FFFFFF'; break;
|
||||
case 'Transparent': sColor = '#784D3E'; break;
|
||||
}
|
||||
|
||||
//Add track points
|
||||
let aoCoords = [];
|
||||
for(var p in aoTrack.points) {
|
||||
let pt = aoTrack.points[p];
|
||||
aoCoords.push([pt.lon, pt.lat, pt.ele, 0]);
|
||||
}
|
||||
|
||||
//Convert to geojson
|
||||
let sPostedData = JSON.stringify({
|
||||
geometry: {
|
||||
coordinates: [aoCoords],
|
||||
type: 'MultiLineString'
|
||||
},
|
||||
properties: {
|
||||
archived: false,
|
||||
color: sColor,
|
||||
//distance: 168405.62350073704,
|
||||
filename: aoTrack.filename,
|
||||
hexcolor: sColor,
|
||||
isLatestImport: true,
|
||||
isLocallyCreated: true,
|
||||
isPublicTrack: false,
|
||||
isValid: true,
|
||||
localId: 'track'+(iIndex + 1),
|
||||
notes: aoTrack.desc,
|
||||
parent_folder_id: this.asFolders[aoTrack.filename].id,
|
||||
routing_mode: null,
|
||||
time_created: aoTrack.time || (new Date()).toISOString(),
|
||||
title: aoTrack.name,
|
||||
type: 'track',
|
||||
writable: true
|
||||
},
|
||||
type: 'Feature'
|
||||
});
|
||||
|
||||
var self = this;
|
||||
$.post({
|
||||
url: Gaia.API+'/track/',
|
||||
contentType: 'application/json',
|
||||
data: sPostedData,
|
||||
trackName: aoTrack.name
|
||||
}).done(function(asTrack) {
|
||||
self.aoTracks[iIndex] = asTrack;
|
||||
self.confirmTrack(iIndex, asTrack, this.data);
|
||||
}).fail(function() {
|
||||
self.feedback('error', 'Track "'+this.trackName+'" upload failed (stage 1). Retrying...');
|
||||
self.uploadTrack(iIndex);
|
||||
});
|
||||
}
|
||||
|
||||
confirmTrack(iIndex, asTrack, sPostedData) {
|
||||
iIndex = iIndex || 0;
|
||||
|
||||
var self = this;
|
||||
$.ajax({
|
||||
url: Gaia.API+'/track/'+asTrack.features[0].id+'/',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: sPostedData,
|
||||
trackName: asTrack.features[0].properties.title
|
||||
}).done(function() {
|
||||
self.feedback('info', 'Track "'+this.trackName+'" uploaded');
|
||||
self.incProgress();
|
||||
iIndex++;
|
||||
if(iIndex < self.aoTracks.length) self.uploadTrack(iIndex);
|
||||
else {
|
||||
self.feedback('info', 'All tracks uploaded');
|
||||
self.uploadWayPoints();
|
||||
}
|
||||
}).fail(function() {
|
||||
self.feedback('error', 'Track "'+this.trackName+'" upload failed (stage 2). Retrying...');
|
||||
self.confirmTrack(iIndex, asTrack, sPostedData);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
//Wait for file to be processed by Gaia
|
||||
checkNotif() {
|
||||
this.feedback('info', 'Waiting for Gaia to process consolidated track');
|
||||
$.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => {
|
||||
for(var i in asNotifs) {
|
||||
if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) {
|
||||
this.feedback('info', 'Notification '+asNotifs[i].id+' found. Marking as read');
|
||||
var $Notif = $('<span>').html(asNotifs[i].html);
|
||||
this.sFolderId = $Notif.find('a').attr('href').split('/')[3];
|
||||
$.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/');
|
||||
}
|
||||
}
|
||||
|
||||
if(this.sFolderId != '') {
|
||||
this.setTracksColor();
|
||||
this.uploadWayPoints();
|
||||
}
|
||||
else setTimeout((() => {this.checkNotif();}), 1000);
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
uploadWayPoints(iIndex) {
|
||||
iIndex = iIndex || 0;
|
||||
|
||||
//Upload waypoints
|
||||
var sWaypointName = this.aoWaypoints[iIndex].name;
|
||||
var aoWaypoint = this.aoWaypoints[iIndex];
|
||||
|
||||
this.feedback('info', 'Uploading waypoint '+(iIndex + 1)+'/'+this.aoWaypoints.length+' ('+aoWaypoint.name+')');
|
||||
var asPost = {
|
||||
geometry: {
|
||||
coordinates: [
|
||||
aoWaypoint.lon,
|
||||
aoWaypoint.lat,
|
||||
aoWaypoint.ele
|
||||
],
|
||||
type: 'Point'
|
||||
},
|
||||
properties: {
|
||||
archived: false,
|
||||
filename: aoWaypoint.filename,
|
||||
icon: Gaia.getIconName(aoWaypoint.sym),
|
||||
isLatestImport: true,
|
||||
isLocallyCreated: true,
|
||||
isValid: true,
|
||||
localId: iIndex+'',
|
||||
notes: aoWaypoint.desc,
|
||||
parent_folder_id: this.asFolders[aoWaypoint.filename].id,
|
||||
time_created: aoWaypoint.time || (new Date()).toISOString(),
|
||||
title: aoWaypoint.name,
|
||||
type: 'waypoint',
|
||||
writable: true
|
||||
},
|
||||
type: 'Feature'
|
||||
};
|
||||
|
||||
let sData = JSON.stringify(asPost);
|
||||
var self = this;
|
||||
$.post({
|
||||
url: Gaia.API+'/waypoint/',
|
||||
contentType: 'application/json',
|
||||
data: sData
|
||||
}).done(function(asWaypoint) {
|
||||
self.aoWaypoints[iIndex] = asWaypoint;
|
||||
self.confirmWayPoint(iIndex, asWaypoint, this.data);
|
||||
}).fail(function(){
|
||||
self.feedback('error', 'Failed to upload waypoint #'+(iIndex + 1)+' "'+sWaypointName+'" (Stage 1). Trying again...');
|
||||
self.uploadWayPoints(iIndex);
|
||||
});
|
||||
}
|
||||
|
||||
confirmWayPoint(iIndex, asWaypoint, sPostedData) {
|
||||
$.ajax({
|
||||
url: Gaia.API+'/waypoint/'+asWaypoint.properties.id+'/',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: sPostedData
|
||||
}).done(() => {
|
||||
iIndex++;
|
||||
this.incProgress();
|
||||
if(iIndex < this.aoWaypoints.length) this.uploadWayPoints(iIndex);
|
||||
//else this.assignElementsToFolders();
|
||||
else this.feedback('info', 'Done');
|
||||
}).fail(() => {
|
||||
this.feedback('error', 'Failed to upload waypoint #'+(iIndex + 1)+' "'+asWaypoint.properties.title+'" (Stage 2). Trying again...');
|
||||
this.confirmWayPoint(iIndex, asWaypoint, sPostedData);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
assignElementsToFolders(iIndex) {
|
||||
iIndex = iIndex || 0;
|
||||
let asFolders = Object.keys(this.asFolders).map(key => this.asFolders[key]);
|
||||
let asFolder = asFolders[iIndex];
|
||||
|
||||
this.feedback('info', 'Assigning elements of folder "'+asFolder.properties.name+'"');
|
||||
|
||||
//Folder metadata
|
||||
let asData = {
|
||||
cover_photo_id: asFolder.properties.cover_photo_id,
|
||||
id: asFolder.id,
|
||||
name: asFolder.properties.name,
|
||||
notes: asFolder.properties.notes,
|
||||
time_created: asFolder.properties.time_created,
|
||||
updated_date: asFolder.properties.updated_date
|
||||
}
|
||||
|
||||
//Assign waypoints to folder
|
||||
asData.waypoints = [];
|
||||
for(var w in this.aoWaypoints) {
|
||||
if(this.aoWaypoints[w].parent_folder_id = asFolder.id) asData.waypoints.push(this.aoWaypoints[w].properties.id);
|
||||
}
|
||||
|
||||
//Assign tracks to folder
|
||||
asData.tracks = [];
|
||||
for(var t in this.aoTracks) {
|
||||
if(this.aoTracks[t].parent_folder_id = asFolder.id) asData.tracks.push(this.aoTracks[t].features[0].id);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: Gaia.API+'/folder/'+asFolder.id+'/',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(asData)
|
||||
}).done(() => {
|
||||
iIndex++;
|
||||
this.incProgress();
|
||||
this.feedback('info', 'Tracks & waypoints assigned to folder "'+asFolder.properties.name+'"');
|
||||
if(iIndex < asFolders.length) this.assignElementsToFolders(iIndex);
|
||||
else this.feedback('info', 'Done');
|
||||
}).fail(() => {
|
||||
this.feedback('warning', 'Failed to assign waypoints & tracks to folder "'+asFolder.properties.name+'". Trying again...');
|
||||
this.assignElementsToFolders(iIndex);
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
static getIconName(sGarminName) {
|
||||
var asMapping = {
|
||||
'Bridge': 'bridge',
|
||||
'Campground': 'campsite-24',
|
||||
'Car': 'car-24',
|
||||
'Cemetery': 'cemetery-24',
|
||||
'Church': 'ghost-town',
|
||||
'City (Capitol)': 'city-24',
|
||||
'Convenience Store': 'market',
|
||||
'Drinking Water': 'potable-water',
|
||||
'Flag, Blue': 'blue-pin-down',
|
||||
'Flag, Green': 'green-pin',
|
||||
'Flag, Red': 'red-pin-down',
|
||||
'Forest': 'forest',
|
||||
'Ground Transportation': 'car-24',
|
||||
'Lodging': 'lodging-24',
|
||||
'Park': 'park-24',
|
||||
'Pharmacy': 'hospital-24',
|
||||
'Picnic Area': 'picnic',
|
||||
'Post Office': 'resupply',
|
||||
'Powerline': 'petroglyph',
|
||||
'Residence': 'building-24',
|
||||
'Restaurant': 'restaurant-24',
|
||||
'Restroom': 'toilets-24',
|
||||
'Shopping Center': 'market',
|
||||
'Ski Resort': 'skiing-24',
|
||||
'Summit': 'peak',
|
||||
'Toll Booth': 'ranger-station',
|
||||
'Trail Head': 'known-route',
|
||||
'Truck': 'car-24',
|
||||
'Water Source': 'water-24'
|
||||
};
|
||||
return (sGarminName in asMapping)?asMapping[sGarminName]:'red-pin-down';
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Loading GaiaGps Uploader '+GM_info.script.version);
|
||||
|
||||
let oGaia = new Gaia();
|
||||
|
||||
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
|
||||
|
||||
let observer = new MutationObserver((mutations, observer) => {
|
||||
|
||||
/* FIXME: adapts on GaiaGPS upgrade */
|
||||
let $Import = $('div[aria-label="Import Data"]');
|
||||
if($Import.length > 0) {
|
||||
observer.disconnect();
|
||||
$Import.parent('li').on('click', () => { setTimeout(() => { oGaia.setLayout(); }, 500)});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document, { subtree: true, attributes: true});
|
||||
@@ -0,0 +1,228 @@
|
||||
// ==UserScript==
|
||||
// @name GaiaGps Waypoints Mass Updater
|
||||
// @description Add missing feature: Mass update of GaiaGps Waypoints
|
||||
// @version 1.2
|
||||
// @grant none
|
||||
// @match https://www.gaiagps.com/datasummary/waypoints/
|
||||
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.0/jquery.min.js
|
||||
// @namespace https://greasyfork.org/users/583371
|
||||
// ==/UserScript==
|
||||
|
||||
console.log('computes');
|
||||
|
||||
function init() {
|
||||
|
||||
//Event on checkboxes to enable mass icon button
|
||||
$('.ngGrid .col0 input[type="checkbox"]').change(function(){
|
||||
$MassUpdate.prop('disabled', ($('.ngGrid .col0 input[type="checkbox"]:checked').length == 0));
|
||||
});
|
||||
|
||||
//DOM Elements
|
||||
$Grid = $('.ngGrid');
|
||||
$Head = $Grid.find('.ngTopPanel');
|
||||
$Body = $Grid.find('.ngViewport');
|
||||
|
||||
iIconColLeft = $Grid.find('.colt0').width();
|
||||
iCol1Width = $Grid.find('.ngHeaderCell.ng-scope.col1.colt1').width();
|
||||
iCol1Left = parseInt($Grid.find('.ngHeaderCell.ng-scope.col1.colt1').css('left'));
|
||||
|
||||
//Insert column header
|
||||
$Head.find('.ngHeaderContainer .ngHeaderCell.col0').after(
|
||||
$('<div>', {'class':'ngHeaderCell ng-scope col0-1 colt0-1', 'style':'width:'+iIconColWidth+'px; left:'+iIconColLeft+'px;'})
|
||||
.append($('<div>', {'class':'ngVerticalBar ngVerticalBarVisible', 'style':'height: '+iIconColHeight+'px;'}).text(' '))
|
||||
.append($('<div>')
|
||||
.append($('<div>', {'class':'ngHeaderSortColumn ngSorted', 'style':'cursor: pointer;'})
|
||||
.append($('<div>', {'class':'ngHeaderText ng-binding', 'style':'width:'+iIconColWidth+'px;'}).text('Icon'))
|
||||
)
|
||||
)
|
||||
);
|
||||
$Head.find('.ngHeaderCell.colt1').width(iCol1Width - iIconColWidth);
|
||||
$Head.find('.col1').width(iCol1Width - iIconColWidth).css('left', (iCol1Left + iIconColWidth)+'px');
|
||||
|
||||
//Update Icons
|
||||
update();
|
||||
|
||||
//Event on DOM change: First waypoint of the page
|
||||
oObserver = new MutationObserver((oMutations) => {update();});
|
||||
oObserver.observe($('.ngGrid .ngCanvas .ngCell.col1 .ngCellText.col1 .ng-binding')[0], {attributes: true});
|
||||
|
||||
//Event on buttons
|
||||
//$('.ga-toolbar-right .ga-toggleicons').click(() => {update();});
|
||||
}
|
||||
|
||||
function update(bWaited) {
|
||||
bWaited = bWaited || false;
|
||||
|
||||
if(!bWaited) {
|
||||
if(typeof oTimer != "undefined") clearTimeout(oTimer);
|
||||
oTimer = setTimeout((() => {update(true);}), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
oObserver.disconnect();
|
||||
oObserver.observe($('.ngGrid .ngCanvas .ngCell.col1 .ngCellText.col1 .ng-binding')[0], {attributes: true});
|
||||
|
||||
console.log('updating');
|
||||
|
||||
$Body.find('.ngRow').each(function(key, elem){
|
||||
var $Row = $(elem);
|
||||
var sWaypointId = $Row.find('.col1 a.ng-binding').attr('href').split('/')[3];
|
||||
|
||||
//Icon
|
||||
var sIcon = asWaypoints[sWaypointId].icon;
|
||||
if(sIcon=='') sIcon = 'red-pin-down.png';
|
||||
|
||||
//Add Icon
|
||||
if(!$Row.find('.ngCell.col0-1.colt0-1').length) {
|
||||
|
||||
//Move Col1
|
||||
$Row.find('.colt1').width(iCol1Width - iIconColWidth);
|
||||
$Row.find('.col1').width(iCol1Width - iIconColWidth).css('left', (iCol1Left + iIconColWidth)+'px');
|
||||
|
||||
$Row.find('.ngCell.col0.colt0').after(
|
||||
$('<div>', {'class':'ngCell col0-1 colt0-1', 'style':'cursor: pointer; width:'+iIconColWidth+'px; left:'+iIconColLeft+'px;'})
|
||||
.data('icon', sIcon)
|
||||
.data('waypoint-id', sWaypointId)
|
||||
.append($('<div>', {'class':'ngVerticalBar ngVerticalBarVisible', 'style':'height: '+iIconColHeight+'px;'}))
|
||||
.append($('<div>')
|
||||
.append($('<div>', {'style':'ngSelectionCell', 'style':'margin: 10px 0;text-align: center;'})
|
||||
.append($('<img>', {'src':asIcons[sIcon].src, style:'width: 20px;', 'class':'waypoint-icon'}))
|
||||
)
|
||||
)
|
||||
//Display Selector
|
||||
.click(function(){
|
||||
toggleSelector(null, $(this));
|
||||
})
|
||||
);
|
||||
}
|
||||
//Update Icon
|
||||
else {
|
||||
$Row.find('.ngCell.col0-1.colt0-1')
|
||||
.data('icon', sIcon)
|
||||
.data('waypoint-id', sWaypointId)
|
||||
.find('img.waypoint-icon')
|
||||
.attr('src', asIcons[sIcon].src);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSelector(bShow, $Waypoint){
|
||||
bShow = bShow || !$Selector.is(':visible');
|
||||
$Selector.add($Background).toggle(bShow);
|
||||
if(bShow) {
|
||||
$Selector
|
||||
.data('waypoint', $Waypoint)
|
||||
.css({top: ($Waypoint.offset().top + iIconColHeight)+'px', left: ($Waypoint.offset().left)+'px'})
|
||||
.find('li[data-value="'+$Waypoint.data('icon')+'"]').addClass('Mui-selected');
|
||||
}
|
||||
else {
|
||||
$Selector
|
||||
.find('li.Mui-selected').removeClass('Mui-selected');
|
||||
}
|
||||
return bShow;
|
||||
}
|
||||
|
||||
function setIcon(){
|
||||
var sIcon = $(this).attr('data-value');
|
||||
|
||||
var a$Waypoints = [];
|
||||
if($Selector.data('waypoint').data('waypoint-id') == 'mass') a$Waypoints = $('.ngGrid .ngViewport .ngRow .ngCell.col0 .ngSelectionCheckbox:checked').parents('.ngRow').find('.ngCell.col0-1').toArray();
|
||||
else a$Waypoints.push($Selector.data('waypoint'));
|
||||
|
||||
$.each(a$Waypoints, function(iKey, oWaypoint){
|
||||
var $Waypoint = $(oWaypoint);
|
||||
$Waypoint.find('img').attr('src', sLoader);
|
||||
var sWaypointId = $Waypoint.data('waypoint-id');
|
||||
|
||||
$.ajax({'url':'https://www.gaiagps.com/api/objects/waypoint/'+sWaypointId+'/'}).done(function(asWaypoint){
|
||||
$.ajax({
|
||||
url: 'https://elevation.gaiagps.com/geojson/',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(asWaypoint.geometry)
|
||||
})
|
||||
.done(function(asGeo){
|
||||
asWaypoint.geometry = asGeo;
|
||||
asWaypoint.properties.icon = sIcon;
|
||||
$.ajax({
|
||||
url: 'https://www.gaiagps.com/api/objects/waypoint/'+sWaypointId+'/',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(asWaypoint)
|
||||
})
|
||||
.done(function(asResult){
|
||||
$.ajax({url:'https://www.gaiagps.com/api/objects/waypoint/'+sWaypointId+'/'}).done(function(asWaypoint){
|
||||
$Waypoint.find('img').attr('src', asIcons[asWaypoint.properties.icon].src);
|
||||
toggleSelector(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var iIconColWidth = 50,
|
||||
iIconColHeight = 40;
|
||||
|
||||
sLoader = ' data:image/gif;base64,R0lGODlhFAAUAPeoAP////v7+/39/f7+/vn5+fr6+vX19fz8/Pf39+/v7/j4+PLy8vPz8/Dw8Ozs7PT09Obm5ufn5/Hx8czMzOTk5O7u7vb29t/f3+rq6t3d3ejo6LOzs4CAgNzc3GZmZu3t7ZmZmc/Pz+vr6+Dg4OHh4U1NTRoaGjMzM3Z2dunp6QAAAMbGxtnZ2a6urra2tt7e3s3NzeLi4sLCwtHR0dfX162traOjo9bW1tjY2GVlZbi4uJSUlKenp4ODg2BgYNvb27u7u4iIiGNjY87OztDQ0Hl5edTU1MTExL6+vlFRUWhoaMDAwL29vZqamuXl5by8vLCwsJCQkMvLy05OTsjIyJycnKSkpFRUVIqKirW1tZ+fn6qqqrKyssrKyh8fH56ennBwcLS0tNXV1aurq4yMjL+/v1dXV5ubm5eXl3R0dJWVlT8/P6mpqR0dHVNTU2trawMDA09PTzg4OAYGBl1dXbm5uWpqasXFxY6OjoaGhpiYmKGhocHBwY2NjZaWljU1NVBQUIWFhXt7e319fW5ubkhISFtbWzk5OaCgoFpaWnd3d3p6etLS0nV1dSwsLG9vb5GRkdra2omJiUVFRYGBgW1tbTw8PLGxsX9/f7q6umxsbKioqGFhYdPT08nJyXFxca+vrw8PDwUFBePj47e3t1VVVZOTk4+Pj////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAACoACwAAAAAFAAUAAAIhQABCBxIsKDBgwgTEvRDh4DCgQUM2QCwBg6GhwKNmCAEoAEJjBWcAOjwAONATnIcmCS4p4iAlTAPrtgRICaAHiU02DRAwabABykUDqhZkIydBAkrQChQEIgepggRJBhwEEEfPgQPxJCgkFGJRQAizBCwYMKLh0e4dtlgAMADohgDMPD5MCAAIfkEBQAAqAAsAgACABAAEQAACH0AAQgcCGAPoQIEExaoVAOAmzYOKABikxAAixODACygACCECRQEG2gAMMJAwgsHCDZKUqGiyxpYBLicSZPgEBsBag5U40GEToEWRv4E+mEoAC09FgxdsSWnUQJWhhAcIEFBxR9vTAFIMGIAAQgJXBJ5AKDDBKsFZNI8gMBlQAAh+QQFAACoACwCAAIAEAARAAAIeQBRCRyIaswggghRBcCkA1WOPxUSDsxQAg8qBhEkologAhUFBBI/4BAYxI4EjaiSqAiBqk4TlKiY+GAAMyGNDQdqCuTBIYFOVAoc/BSo4ORPLmce/JwBZKjAAE9+EARANSGEUy1QIWiAiioAiRlAVoCQsyrKAQEkBgQAIfkEBQAAqAAsAgACABEAEQAACIAAAQgcCMAFngAEEwIIAEkGgCJxGkQQwkUhgBgevgAwgAGAkROBEhpIAECDgoQxBCToMLBKDwYWBQoxYUTgnTEDYgI4AuaBTp0XZAj4CQBEEgY6QCwgegXOCwIkiRIgIbCAT6IDV2RBgFVghgkHug4UcCOF2AY6YIgFgAHhWqwBAQAh+QQFAACoACwCAAIAEQAQAAAIhQBRCRyIaskXgggFHrAyARUZJQswKHqSEFUEDi1QWfiAikWJHQgRLEDlgABCCKgkjBgI5YyBigLTnGAhkAhFmKi6cHiJE2aEGT0F2hDyoMsGnjhztCERgEFQVAUoCAygoGcDMQNZwCiA84qKhqg0ZBCAs4UPpwMHSEDwdCoEjm1RERgQNCAAIfkEBQAAqAAsAgACABEAEAAACIkAAQgcCEBKiwMEEwIQ4OIGAC2UDHzAckchAAcgkABQ0ADABQ82EhIwACBBgYQpBCygMPBIFgUWBeYpkUFgBxgDYgKYEQWBTp0JRuT8WSONgQ4TYP5E8cfJAZ8/ARSIIFBAgJ8LWAx0EAFhzBwmZgh8UGGoRRdgHkRtoClT1IFLVCR6O9DTB4IBAQAh+QQFAACoACwCAAIAEQARAAAIhgABCBwIQAwSAQQTAhBAhQQALk0QNPgSQiEACRsqEmAAgAKHDQkDKADAIEDCDwMeaBhIA0YBiwLReIghUEOGATAB0EA0MidMiTh96siDoAKEAz4BBImjYYDJpAEwJB3IIMPUgShO3LgKAAkHA0kXCOKTtIUHAitMvEk6RcULACESJG2AA2ZAACH5BAUAAKgALAIAAgARABEAAAiHAAEIHAhgRAgBBBMCGBBJBIAVYQgwuMRCIQADEy4ACGABQAoQMhIKOAAAAUKCEgZYcDDQQYSTFjdxiCDwQQKLA19AIYCzp8+EMtAo+DlQjxKHRDd+SHoxBlMAQUr8YLoiCgKCDB4xwSqJisUlKswAcIGiAIwTgnC2wADAhwkSAIxI+LmgA86AACH5BAUAAKgALAMAAgAQABEAAAh7AAEIHGggwYCBCBMCcABBAAEcDhQiHHAAwIMJIyRKVDCgAAONCqVskAASoQgiAUqqBHmjToGVAIA0WQBTwMcPP0oqwCBwzRwKILdwgACgzxULIGlAITCwwaQoCC3wuKGwhihHAFbsCMDCQxWJO4D2KKEBwIUHJQ0AlRgQACH5BAUAAKgALAIAAgARABEAAAh4AAEIHEiwoEGDBxxYOHiwAIQGDA8GABAAQUSDHSYYuEhwwQUBHA+28ECAIwkpAaaoeMFxQhgDDXCEFLAQQIILFwlUGOjGS4SIQECIEAgih0WGF8oU6FiqCkEFLjIczBLKEoAhNgKM4FCDoZafajwMpVAzogUNDAMCACH5BAUAAKgALAIAAwARABAAAAh8AAEIHEiwoMGDBzG8QChQQICBh+ZEYFgBQgGBWMwcYEggwQCGDF2guAhSRAcBPkyQAAkgAwwFCzqwBDCAgEAJMUAGWDAwh5wUDCdsaCDQCgoFDCMQeTjwgQceBAkcGXWwjJcpAGhsOAABBBOEWzAA4MEhAYAUSEEqcIAwIAAh+QQFAACoACwCAAMAEQAQAAAIfAABCCRQCITAgwgThlBBJ6FDAA5IAMDB4GHCEl5SWHToR8mBjSBDIglUIKSBCgM+nXASsgKEAwxGhAQw4COABRBAHkBwsEgSBxszTDAgEFQQAhsTXBCA0ECRDQgDdMLwkMoJIQAuyBDgYMMQi6QqANABYgGABCVBEkhgMSAAOw==';
|
||||
|
||||
asIcons = {};
|
||||
$Icons = $('<ul class="MuiList-root MuiMenu-list MuiList-padding" role="listbox" tabindex="-1" style="display: flex; flex-wrap: wrap; width: 480px; height: 376px; flex-grow: 1;"><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="black pin" data-value="black-pin.png"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAEBklEQVRo3r2ZQWgUVxjHv12iQVyiCK4JiK2KARFPNqIX2fYm1IsHQZD2pmTJoUYEL8tCCt4kHnITaUUDoijkIOSg5KJQ8dCgTUsOira0LnoKm4KJyfw8vPlmdndms/vezOx/j2/e9/+9975535u3Ig5iP+NMM8cideosMsc04+yX7EWRCV7RTq+YoJideYEqdTqpTpVCFvanqdGtapxO1zxHFS+Mv+rNUqbEMAUKDFOizCyrDU/gUSWXlv0W7jUMzhtlAIn5DTBKrRHiHlvSGX1gv+JV2Bprrr+tVFjxGhCSzwLVcOzHNjTX37HGeagmTz0/2Et2d2UvCLt5GeZCknSkoJlf87q3NwjBLNQSvJQ6/StdTn7zQgS54LoMFHXbqVjbC0Il3Jrcdkd+1umPy/xDTLJAnToLTHIo9o0IlmHCDcDf80cjoTczxXrT1rfOFJsjz40GNcKt4vm73kDE/kns/vskgjAQ7o72lZJx03M2Mq6ptiVgKvLsrDaN2wNMm57lyNqvtwVYj+RCWZum7QHmTM9SS8jJDcvgZMvTJW2YswdYND2HW0IubAiw0PL0sDYs2gP4e0ChJWS9w2mk+elC0GAP8Mn07E8E0B8UUnuAj6ZnMdESFLXhoz3Aa9PzQKIkPKANr+0BnpueJxK9hie06bk9wG3T80KijeiCNt2xB6jET6rNVtywYBV7gDOm5+OYOtdtMRIe6wNn7AH2mp7/e32x1b5zORb6WNZitNelHv5r+o44HUcEYUTH/5/becA/jl9yBrikAPfdAMY0uVwBgmQdcwPws2DN2+5kv521JBnQeCg76wRwNsmBzAe4aiLMOAHMKMBVd4AjugiD1va7wgU4Iu7Sb6zL1gCXky+AiAgXTZS/rAH+VICLyQB2smriHLeyPx7cZbBTkomHJtINK4AbCvBQkorv/cOWV+javkBdE/BUcoA+3vuL2TXATzr+9/RJcunJ4B82dWW/ib/dTwGxADtYNvF+7ArgB7VfZoekI67rmTfX0T7HHwpwXdISe/js51RHgFNq/5k9kp70iPq0I8BTBbgtaYrDGve7De2/DY+JhyVd8cjEfbYhwDO1fyRpi2809sm29ifD8Y9I+uKBiT3f5l3I8bvaP5AsxEHWTPxzsQDnwo+Eg5KN+MU4vI18tAv9vFWAXyUr8RUrxuNKBOBKcBPA15KduKa1cVfLASyof9ckS7GND8bnZhPATR3/B7ZJtuK8XsQfDeyPEvyhc16yFnnmjdcL8ghCnhc6/nnykr3Cy79y82UklKQ34q7xW/KGGGJJp/+u9EoMseR/M32a8a/zWGJIeif9cm7QmPRS5PUWzddvPUm/JoR9vAns37BPei8GucU73nGLQfcoXwDnhWdKwxgN/gAAAABJRU5ErkJggg==" width="24"></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="blue pin" data-value="blue-pin-down.png"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAExElEQVR4AeWaA7AsRxfH+/ti27adFGPbSbniFGLbKMW2nTzbtt8+2/bep4v5Zf/Fvqf2re5uz+zmV3Wqpma6z7/7tHvXhQI4Cngc+BXoA0wF0jI9652+KY3SuloA2Bd4HUhRPCnllY9qrPjOwCtAmpaTli/5rJbK3wwsofwske8kV/x/wCtARBbqm6Kox7zlPNp/Ele2G8Fpvw1k3697IdOz3umb0igt2YmkIa2kVX4H4C+ysHTD5uiRfpPY/5te7PR594JMaZVHecmOtHZIUsvbyrO5sSl6fdh09vm6pypVkimvfMhX9iAkoCcAr2Rr9QtbDVMlymLyJZ8YpJ2ECS/CI7ViHcf81E8FL6vJp3wbpH1znEvdEtvytvLlDoI0sqwOO8fd9TVOTbevjElDWrEOBWBfu8nRZKUChjBpGdJBd4zAG7brFzrbn/3HID4eN4dJK9Ok6xtketY7fSt0dcg2FF53oTB7e63ZeQu9+5c9+CI1D7vH8dA3pVHavP6kaUiFO9WZHZ42Lvkq32fBSgpEafMGQZp2xxjkFAk8joe2rvlaS61aJMqT16+0DY+HCMCveGj/nm/Mq6GKRXnyzQnSNvwaIgB98NAhJlchNbmViPLm9C1tQ58QAZiKh05yuQqpGb5ElDenb2kbpoYIQBoPHWdzFVLLXIkob07f0jakQwRgEx57fNkjtgBI27A5RACW43H4931iGwLSNiwPEYCZeJyqOSCmSVDahpkhAjAMj8vbDo9tGZS2YViIAPyMx0P9Jsa2EZK24ZcQAXgpSzcNthXOM7xeChGA2/HoPX+FChP8MCSTtuH2EAE4Ao/1DY3Rrl94BQ5wHJZJsy6jTXOOcCEAFuJx3j9DVaigJk3DIhcKew3+3KCpwQMgTcPfIQPwoJm0ggdAmoYHXSjsPNAYRdGB3/YOVnlpSTPs+M9zLXZnj/HBAiAtQ8qFBngbj46zlwYLgLQMb8cRgLPsMDjih74Vr7w0snT/s1wcAOPxeH5w5VcDaRhSLi6Ax/CYsqqu4gGYvCqN4bE4A7APUI/HRa0r9/OYfBukvY+LE6A1Ht9Pml+xAMi3obWLG+BaPNL1jZG5JyyLyad805zrkhCArYHFeDwzaErZA/D0wCkYFkvbJQF7R7AgvZHd7JG2BSZf8zM+DS+5pADsCdThcV+vVNkCcG/Gl6FOmi5JAB/kv9EtzSZmfBk+cEkDOBRowOPWzqNbXHn5MDRIyyURe2E6eNHqFgdAPgw/u6QCnILhavvjaRF2VSavRRouyQCd8BiyuPReoLyGTi7pAGdjuLHjqKIrrzxZOMdVA0ArPMYvX1d0AMZl8hhauWoBOAFoxOOenoXfGCmtoUk+XTUBfI/H3HUb8v6ULlMapTX84KoN4DBgMx4vDZmWNwBKY5CPw101ArxrT4q5rs30zZ745MNVK8BuwDI8fpy8YIsB0DeD8u7mqhngfpoTnd9qKLbyeqdveCivq3aA/wNj8Ri1dA07e5XXs94ZlOf/rhYALsTD/sFSzxblcbUE8Aceazc3REf+2BeZnvFQWldrAAeo3ubXpE0yPJRGaV0tAjxIfh50tYaZEIexZYYqjatlgCOBWRj0Tt/cfwFgf+BHYK5Mz3rnYuBfL+mM2UqtTGQAAAAASUVORK5CYII=" width="24"></li><li class="MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button" tabindex="-1" role="option" aria-disabled="false" style="padding-left: 8px; padding-right: 8px;" title="brown pin" data-value="brown-pin.png"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAE2klEQVR4AeWaA5AkyRvF84+zbdu2bdu2zbm1jmvbtm3btr07Vv/u3jE7o4fbk1U994vIiIqqzO/l9xKFbuML4BTgY6ANMAxYACSr6FjndE11VNeUBoDDgfLALIqO2pRXjERMfF+gDJDMrqMYZRQzUZJ/GFhP/Fmv2GFO/D9AGSBCDHKysyILp4ykV/2yNPnmWaq/dTsVnrhQRcc6p2uojuoSm4g0pBW25PcCOhKD5G2bIj3rlaHikxeR9MDphSqqqzZqS2w6SjNMI98R3BHPjAxuU53yj1+gpIpV1FYxFCu2CSGYCUCZWKPe4LPHlERcimLlMRvKhGHDi2Cxftl8fnj5OnU8rkUxFdtB2g8Heatb7468m3y8TYgxE9arL0FPfa1TZ9qXTJFGjD2hTBBPeMlYaLNSB30UaTkke31iBCq4U7+wu33Nd+9mTI9mbFi5iIy0FBUd65yuFfruEGMplDe+cJ/tdc8uqNNlHzmH8X1akZubSx7omuqoboHxpOkwy9tbnfuEV9BDjhJaMmMshUR1CzRBmtImmlN8GPAxFnp0LWi0NKpFRG0KjCtth499GNAGCz2/F7TmNbWLiNoUuCdI26GNDwOGYaGXmPw6qc2tmKhtvrGl7TDMhwELsNCbXH6d1A5fTNQ239jSdljgw4BkLPQ6m18ndZsrJmqbb2xpLine truncated
|
||||
$Icons.find('li').each((iKey, sElem)=>{
|
||||
var $Elem = $(sElem);
|
||||
asIcons[$Elem.attr('data-value')] = {'title': $Elem.attr('title'), 'src':$Elem.find('img').attr('src')};
|
||||
});
|
||||
|
||||
//Background
|
||||
$Background = $('<div>', {'aria-hidden':'true', 'style':'position: fixed; inset: 0px; background-color: transparent;'}).click(function(){toggleSelector(false);});
|
||||
|
||||
//Build Icon Selector
|
||||
$Selector = $('<div>', {
|
||||
'class':'MuiPaper-root MuiMenu-paper MuiPopover-paper MuiPaper-elevation8 MuiPaper-rounded',
|
||||
'style':'z-index: 1300; opacity: 1; transform: none; min-width: 52px; transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, transform 67ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transform-origin: 0px 0px 0px;'
|
||||
})
|
||||
.append($('<ul>', {
|
||||
'class':'MuiList-root MuiMenu-list MuiList-padding',
|
||||
'role':'listbox',
|
||||
'tabindex':'-1',
|
||||
'style':'display: flex; flex-wrap: wrap; width: 480px; flex-grow: 1;'
|
||||
}));
|
||||
|
||||
$.each(asIcons, function(sIcon, asAttr) {
|
||||
$Selector.find('ul').append(
|
||||
$('<li>', {
|
||||
'class': 'MuiButtonBase-root MuiListItem-root MuiMenuItem-root MuiMenuItem-gutters MuiListItem-gutters MuiListItem-button',
|
||||
'tabindex': '-1',
|
||||
'role': 'option',
|
||||
'style': 'padding-left: 8px; padding-right: 8px;',
|
||||
'title': asAttr.title,
|
||||
'data-value': sIcon
|
||||
})
|
||||
.click(setIcon)
|
||||
.append(
|
||||
$('<img>', {'src': asAttr.src, 'width': '24'})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
//Add Selector & background to DOM
|
||||
$Selector.add($Background).hide().appendTo('body');
|
||||
|
||||
//waypoints
|
||||
asWaypoints = {};
|
||||
$.ajax({url:'https://www.gaiagps.com/api/objects/waypoint/?routepoints=false&show_archived=true&show_filed=true'}).done((asData) => {
|
||||
for(var i in asData) {
|
||||
asWaypoints[asData[i].id] = asData[i];
|
||||
}
|
||||
setTimeout(init, 2000);
|
||||
});
|
||||
|
||||
//Icon Button for mass update
|
||||
$MassUpdate = $('<button>', {'type':'button', 'class':'ga-button ga-button-flat ga-toolbar-button', 'disabled':'disabled'})
|
||||
.data('waypoint-id', 'mass')
|
||||
.click(function(){toggleSelector(true, $(this));})
|
||||
.append($('<i>', {'class':'material-icons ibmid'}).text(''))
|
||||
.append(' ')
|
||||
.append($('<span>', {'class':'ibmid ga-toolbar-button-text'}).text('Icon'));
|
||||
|
||||
$('#DatasummaryController .ga-toolbar .ga-toolbar-left .ga-button').eq(1).after($MassUpdate);
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/images/icons/mstile-150x150.png?v=GvmqYyKwbb"/>
|
||||
<TileColor>#00a300</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M5041 5784 c366 -384 572 -817 630 -1323 17 -156 7 -495 -20 -641
|
||||
-78 -415 -250 -717 -1091 -1915 -108 -154 -338 -482 -510 -730 -172 -247 -328
|
||||
-469 -346 -492 -21 -25 -56 -52 -91 -68 -48 -23 -66 -27 -127 -23 -46 2 -86
|
||||
11 -113 24 l-42 21 36 -44 c47 -58 122 -116 191 -150 50 -25 68 -28 152 -28
|
||||
88 0 100 2 163 34 80 40 167 121 254 236 102 137 967 1375 1158 1660 429 639
|
||||
578 959 636 1365 20 140 17 485 -5 630 -37 237 -101 440 -205 655 -162 331
|
||||
-404 620 -696 830 -33 24 -28 16 26 -41z"/>
|
||||
<path d="M2750 5673 c-63 -22 -143 -75 -197 -131 -192 -196 -318 -616 -298
|
||||
-990 9 -174 36 -314 118 -607 l34 -120 316 -3 317 -2 0 62 c1 81 22 199 49
|
||||
265 11 29 42 87 69 130 143 230 186 366 185 588 -1 420 -140 736 -356 809 -60
|
||||
20 -177 20 -237 -1z"/>
|
||||
<path d="M4017 5056 c-174 -62 -301 -280 -348 -591 -19 -129 -17 -337 4 -425
|
||||
32 -136 85 -257 159 -368 81 -119 127 -268 128 -410 l0 -62 317 2 316 3 34
|
||||
120 c106 374 136 590 115 816 -22 225 -66 391 -153 566 -48 97 -74 138 -131
|
||||
198 -39 42 -90 87 -115 102 -99 58 -241 79 -326 49z"/>
|
||||
<path d="M2412 3548 c3 -109 7 -129 30 -173 120 -232 445 -231 559 0 28 58 32
|
||||
76 37 180 l5 115 -318 0 -317 0 4 -122z"/>
|
||||
<path d="M3960 2962 c0 -234 106 -368 300 -380 98 -5 168 20 233 83 75 75 91
|
||||
119 95 263 l4 122 -316 0 -316 0 0 -88z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Spotty",
|
||||
"short_name": "Spotty",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icons/android-chrome-192x192.png?v=GvmqYyKwbb",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons/android-chrome-512x512.png?v=GvmqYyKwbb",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "browser",
|
||||
"orientation": "portrait"
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 261 KiB |
@@ -1,23 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 32 64"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="footprint_mapbox.svg"
|
||||
width="32"
|
||||
height="64"
|
||||
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
inkscape:export-filename="C:\Users\francois\Downloads\footprint_mapbox.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<title
|
||||
id="title1">Logo Marker</title>
|
||||
inkscape:export-ydpi="96">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
@@ -26,35 +26,15 @@
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by/4.0/" />
|
||||
<dc:title>Logo Marker</dc:title>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Franzz</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by/4.0/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8">
|
||||
<linearGradient
|
||||
id="linearGradient4520"
|
||||
inkscape:swatch="solid">
|
||||
osb:paint="solid">
|
||||
<stop
|
||||
style="stop-color:#6dff58;stop-opacity:1;"
|
||||
offset="0"
|
||||
@@ -95,11 +75,7 @@
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter5223"
|
||||
x="-0.12000002"
|
||||
y="-0.090000001"
|
||||
width="1.2900001"
|
||||
height="1.2175">
|
||||
id="filter5223">
|
||||
<feFlood
|
||||
flood-opacity="0.498039"
|
||||
flood-color="rgb(0,0,0)"
|
||||
@@ -138,27 +114,24 @@
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2054"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:snap-grids="true"
|
||||
inkscape:zoom="7.375"
|
||||
inkscape:cx="2.0338983"
|
||||
inkscape:cy="47.389831"
|
||||
inkscape:window-x="2549"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:cx="2.1125062"
|
||||
inkscape:cy="47.406997"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4"
|
||||
units="px"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
units="px" />
|
||||
<path
|
||||
id="path4698"
|
||||
d="M 14.766749,31.35437 C 5.6856259,18.189433 4.0000018,16.83831 4.0000018,11.999996 4.0000018,5.37256 9.3725628,0 15.999999,0 c 6.627437,0 11.999999,5.37256 11.999999,11.999996 0,4.838314 -1.685625,6.189437 -10.766749,19.354374 -0.595937,0.860873 -1.870625,0.860809 -2.4665,0 z"
|
||||
d="M 14.766749,31.354369 C 5.685625,18.189432 4,16.838309 4,11.999995 4,5.3725591 9.372562,1.6977696e-7 15.999999,1.6977696e-7 22.627436,1.6977696e-7 27.999997,5.3725591 27.999997,11.999995 c 0,4.838314 -1.685625,6.189437 -10.766748,19.354374 -0.595937,0.860873 -1.870625,0.860809 -2.4665,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#6dff58;fill-opacity:1;stroke:none;stroke-width:0.0625;stroke-opacity:1;filter:url(#filter5223)"
|
||||
style="fill:#6dff58;fill-opacity:1;stroke:none;stroke-width:0.06249999;stroke-opacity:1;filter:url(#filter5223)"
|
||||
inkscape:label="marker" />
|
||||
<path
|
||||
id="shoes"
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="spot.svg"
|
||||
width="512"
|
||||
height="512"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
inkscape:export-filename="C:\Users\francois\Downloads\footprint_mapbox.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8">
|
||||
<linearGradient
|
||||
id="linearGradient4520"
|
||||
osb:paint="solid">
|
||||
<stop
|
||||
style="stop-color:#6dff58;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4518" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter4696">
|
||||
<feFlood
|
||||
flood-opacity="0.498039"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood4686" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite4688" />
|
||||
<feGaussianBlur
|
||||
in="composite1"
|
||||
stdDeviation="3"
|
||||
result="blur"
|
||||
id="feGaussianBlur4690" />
|
||||
<feOffset
|
||||
dx="3"
|
||||
dy="3"
|
||||
result="offset"
|
||||
id="feOffset4692" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite4694" />
|
||||
</filter>
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter5223">
|
||||
<feFlood
|
||||
flood-opacity="0.498039"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood5213" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite5215" />
|
||||
<feGaussianBlur
|
||||
in="composite1"
|
||||
stdDeviation="1.2"
|
||||
result="blur"
|
||||
id="feGaussianBlur5217" />
|
||||
<feOffset
|
||||
dx="1.2"
|
||||
dy="1.2"
|
||||
result="offset"
|
||||
id="feOffset5219" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite5221" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:snap-grids="true"
|
||||
inkscape:zoom="1.3037281"
|
||||
inkscape:cx="248.69883"
|
||||
inkscape:cy="256.06919"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4"
|
||||
units="px" />
|
||||
<path
|
||||
id="path4698"
|
||||
d="M 14.766749,479.35437 C 5.685625,466.18943 4,464.83831 4,460 c 0,-6.62744 5.372562,-12 11.999999,-12 6.627437,0 11.999998,5.37256 11.999998,12 0,4.83831 -1.685625,6.18943 -10.766748,19.35437 -0.595937,0.86087 -1.870625,0.86081 -2.4665,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#6dff58;fill-opacity:1;stroke:none;stroke-width:0.06249999;stroke-opacity:1;filter:url(#filter5223)"
|
||||
inkscape:label="marker"
|
||||
transform="matrix(13.333333,0,0,13.333333,42.666662,-5930.6664)" />
|
||||
<path
|
||||
id="shoes"
|
||||
d="m 221.87402,255.01429 v -11.375 h -45.49995 v 11.375 c 0,12.56578 10.18419,22.74983 22.74997,22.74983 12.56579,0 22.74998,-10.18405 22.74998,-22.74983 z m 90.99975,68.24979 c 12.56579,0 22.74982,-10.18419 22.74982,-22.74998 v -11.37499 h -45.4998 v 11.37499 c 0,12.56579 10.18419,22.74998 22.74998,22.74998 z M 267.37383,203.30789 c 0,12.40941 4.66373,27.07248 11.37498,37.22819 5.82257,8.81203 11.37498,15.8254 11.37498,37.22804 h 45.4998 l 5.67685,-20.45001 c 2.58791,-9.31681 4.66375,-18.84333 5.34988,-28.54756 0.8216,-11.6203 0.23791,-23.31873 -2.20387,-34.68656 -8.36069,-38.91663 -26.87344,-52.81544 -42.94766,-52.81544 -22.74998,0 -34.12496,29.92325 -34.12496,62.04334 z m -99.81881,-54.72772 c -2.44205,11.36784 -3.02861,23.06626 -2.20402,34.68644 0.68639,9.70437 2.7621,19.23086 5.34624,28.54769 l 5.67683,20.45002 h 45.49995 c 0,-21.39914 5.55243,-28.41615 11.37498,-37.22818 6.71124,-10.15572 11.37499,-24.81865 11.37499,-37.22807 0,-32.12008 -11.37499,-62.043305 -34.12495,-62.043305 -16.07422,0 -34.58699,13.898815 -42.94402,52.815405 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#326526;fill-opacity:1;stroke-width:0.35546762" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg width="406" height="469" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 406 469" style="enable-background:new 0 0 406 469;">
|
||||
<style type="text/css">.st0{fill:#F18A00;}</style>
|
||||
<g id="Layer_3"/>
|
||||
<g id="Layer_8"/>
|
||||
<g id="Layer_9"/>
|
||||
<g id="Layer_7"/>
|
||||
<g id="Layer_6"/>
|
||||
<g id="Layer_4"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="m85.80618,195.80013c-1,-0.8 -1.3,-2.3 -0.6,-3.4c11.1,-18.2 56.5,-85.8 117.3,-85.8c49.6,0 90.4,33.4 110.3,53.3c1.2,1.2 2.9,1.9 4.6,1.9c1.7,0 3.4,-0.7 4.6,-1.9l16.4,-16.3c1,-1 1.1,-2.5 0.2,-3.5c-5.1,-6.1 -15.3,-17.2 -29.8,-28.2c-31.7,-24.1 -67.5,-36.3 -106.3,-36.3c-79.4,0 -129.8,75.4 -142.3,96.5c-0.8,1.4 -2.6,1.7 -3.8,0.7l-55.4,-43.6c-1,-0.8 -1.3,-2.2 -0.7,-3.3c5.9,-10.8 23,-39.4 48.5,-63.7c42.8,-40.7 95.1,-62.2 151.4,-62.2c56,0 109.2,18.9 153.9,54.8c27.3,21.9 45.5,43.2 51.3,50.4c0.8,1 0.7,2.5 -0.2,3.5l-12.3,12.2c-1.1,1.1 -2.9,1 -3.8,-0.2c-7.3,-8.9 -26.2,-30.5 -49.7,-49.2c-41.1,-32.7 -88,-49.2 -139.2,-49.2c-50.9,0 -96.5,18.6 -135.4,55.4c-20.9,19.7 -30.4,34.1 -35.6,42.3c-0.7,1.1 -0.5,2.6 0.6,3.5l16.7,13.2c1.2,0.9 2.6,1.4 4.1,1.4c2.2,0 4.2,-1.1 5.4,-2.9c7.4,-10.7 15.9,-20.6 26.8,-31.1c34.3,-33.1 75.7,-50.6 119.9,-50.6c92,0 151.2,70.7 165.3,89.4c0.8,1.1 0.7,2.5 -0.3,3.4l-49.8,49.3c-1.1,1.1 -2.8,1 -3.8,-0.1c-15.9,-18.1 -63,-66.5 -111.4,-66.5c-23.6,0 -46.6,11.3 -68.4,33.5c-7.2,7.3 -13.9,15.6 -19.9,24.4c-0.8,1.1 -0.5,2.7 0.6,3.6l93.1,73.2c1.1,0.9 1.3,2.5 0.4,3.7l-10.5,13.3c-0.9,1.1 -2.5,1.3 -3.7,0.4l-108.5,-85.3z" />
|
||||
<path class="st0" d="m205.90618,468.90013c-56,0 -109.2,-18.9 -153.9,-54.8c-27.3,-21.9 -45.5,-43.2 -51.3,-50.4c-0.8,-1 -0.7,-2.5 0.2,-3.5l12.3,-12.2c1.1,-1.1 2.9,-1 3.8,0.2c7.3,8.9 26.2,30.5 49.7,49.2c41.1,32.7 88,49.2 139.2,49.2c50.9,0 96.5,-18.6 135.4,-55.4c20.9,-19.7 30.4,-34.1 35.6,-42.3c0.7,-1.1 0.5,-2.6 -0.6,-3.5l-16.7,-13.2c-1.2,-0.9 -2.6,-1.4 -4.1,-1.4c-2.2,0 -4.2,1.1 -5.4,2.9c-7.4,10.7 -15.9,20.6 -26.8,31.1c-34.3,33.1 -75.7,50.6 -119.8,50.6c-92,0 -151.2,-70.7 -165.3,-89.4c-0.8,-1.1 -0.7,-2.5 0.3,-3.4l49.8,-49.3c1.1,-1.1 2.8,-1 3.8,0.1c15.9,18.1 63,66.5 111.4,66.5c23.6,0 46.6,-11.3 68.4,-33.5c7.2,-7.3 13.9,-15.6 19.9,-24.4c0.8,-1.1 0.5,-2.7 -0.6,-3.6l-93.1,-73.2c-1.1,-0.9 -1.3,-2.5 -0.4,-3.7l10.5,-13.3c0.9,-1.1 2.5,-1.3 3.7,-0.4l108.3,85.2c1,0.8 1.3,2.3 0.6,3.4c-11.1,18.2 -56.5,85.8 -117.3,85.8c-49.6,0 -90.4,-33.4 -110.3,-53.3c-1.2,-1.2 -2.9,-1.9 -4.6,-1.9s-3.4,0.7 -4.6,1.9l-16.4,16.3c-1,1 -1.1,2.5 -0.2,3.5c5.1,6.1 15.3,17.2 29.8,28.2c31.7,24.1 67.5,36.3 106.3,36.3c79.4,0 129.8,-75.4 142.3,-96.5c0.8,-1.4 2.6,-1.7 3.8,-0.7l55.4,43.6c1,0.8 1.3,2.2 0.7,3.3c-5.9,10.8 -23,39.4 -48.5,63.7c-42.6,40.8 -95,62.3 -151.3,62.3z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,13 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\ToolBox;
|
||||
use \Settings;
|
||||
|
||||
/**
|
||||
* GPX to GeoJSON Converter
|
||||
*
|
||||
* To convert a gpx file:
|
||||
* 1. Add file <file_name>.gpx to geo/ folder
|
||||
* 2. Assign file to project: UPDATE projects SET codename = '<file_name>' WHERE id_project = <id_project>;
|
||||
* 3. Load any page
|
||||
*
|
||||
* To force gpx rebuild:
|
||||
* ?a=build_geojson&name=<file_name>
|
||||
*/
|
||||
class Converter extends PhpObject {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(__CLASS__, Settings::DEBUG);
|
||||
}
|
||||
|
||||
public static function convertToGeoJson($sCodeName) {
|
||||
$oGpx = new Gpx($sCodeName);
|
||||
$oGeoJson = new GeoJson($sCodeName);
|
||||
|
||||
$oGeoJson->buildTracks($oGpx->getTracks());
|
||||
if($oGeoJson->isSimplicationRequired()) $oGeoJson->buildTracks($oGpx->getTracks(), true);
|
||||
$oGeoJson->sortOffTracks();
|
||||
$oGeoJson->saveFile();
|
||||
|
||||
return $oGpx->getLog().'<br />'.$oGeoJson->getLog();
|
||||
}
|
||||
|
||||
public static function isGeoJsonValid($sCodeName) {
|
||||
$bResult = false;
|
||||
$sGeoJsonFilePath = Geo::getFilePath($sCodeName, GeoJson::EXT);
|
||||
if(file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Geo::getFilePath($sCodeName, Gpx::EXT))) $bResult = true;
|
||||
return $bResult;
|
||||
}
|
||||
}
|
||||
|
||||
class Geo extends PhpObject {
|
||||
|
||||
const GEO_FOLDER = 'geo/';
|
||||
const OPT_SIMPLE = 'simplification';
|
||||
|
||||
protected $asTracks;
|
||||
protected $sFilePath;
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||
$this->sFilePath = self::getFilePath($sCodeName, static::EXT);
|
||||
$this->asTracks = array();
|
||||
}
|
||||
|
||||
public static function getFilePath($sCodeName, $sExt) {
|
||||
return self::GEO_FOLDER.$sCodeName.$sExt;
|
||||
}
|
||||
|
||||
public function getLog() {
|
||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||
}
|
||||
}
|
||||
|
||||
class Gpx extends Geo {
|
||||
|
||||
const EXT = '.gpx';
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
$this->parseFile();
|
||||
}
|
||||
|
||||
public function getTracks() {
|
||||
return $this->asTracks;
|
||||
}
|
||||
|
||||
private function parseFile() {
|
||||
$this->addNotice('Parsing: '.$this->sFilePath);
|
||||
$oXml = simplexml_load_file($this->sFilePath);
|
||||
|
||||
//Tracks
|
||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||
foreach($oXml->trk as $aoTrack) {
|
||||
$asTrack = array(
|
||||
'name' => (string) $aoTrack->name,
|
||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||
'points'=> array()
|
||||
);
|
||||
|
||||
foreach($aoTrack->trkseg as $asSegment) {
|
||||
foreach($asSegment as $asPoint) {
|
||||
$asTrack['points'][] = array(
|
||||
'lon' => (float) $asPoint['lon'],
|
||||
'lat' => (float) $asPoint['lat'],
|
||||
'ele' => (int) $asPoint->ele
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
}
|
||||
|
||||
//Waypoints
|
||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||
}
|
||||
}
|
||||
|
||||
class GeoJson extends Geo {
|
||||
protected const EXT = '.geojson';
|
||||
|
||||
private const MAX_FILESIZE = 2; //MB
|
||||
private const MAX_DEVIATION_FLAT = 0.1; //10%
|
||||
private const MAX_DEVIATION_ELEV = 0.1; //10%
|
||||
const EXT = '.geojson';
|
||||
const MAX_FILESIZE = 2; //MB
|
||||
const MAX_DEVIATION_FLAT = 0.1; //10%
|
||||
const MAX_DEVIATION_ELEV = 0.1; //10%
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
@@ -38,7 +146,7 @@ class GeoJson extends Geo {
|
||||
$iGlobalInvalidPointCount = 0;
|
||||
$iGlobalPointCount = 0;
|
||||
|
||||
$this->asTracks = [];
|
||||
$this->asTracks = array();
|
||||
foreach($asTracks as $asTrackProps) {
|
||||
$asOptions = $this->parseOptions($asTrackProps['cmt']);
|
||||
|
||||
@@ -64,31 +172,26 @@ class GeoJson extends Geo {
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
|
||||
$asTrack = [
|
||||
$asTrack = array(
|
||||
'type' => 'Feature',
|
||||
'properties' => [
|
||||
'properties' => array(
|
||||
'name' => $asTrackProps['name'],
|
||||
'type' => $sType,
|
||||
'description' => $asTrackProps['desc']
|
||||
],
|
||||
'geometry' => [
|
||||
),
|
||||
'geometry' => array(
|
||||
'type' => 'LineString',
|
||||
'coordinates' => []
|
||||
]
|
||||
];
|
||||
|
||||
if($sType != 'hitchhiking' && str_contains($asTrackProps['desc'], ' ➜ ')) {
|
||||
list($sFrom, $sTo) = explode(' ➜ ', $asTrackProps['desc']);
|
||||
$asTrack['properties']['leg'] = ['from'=> $sFrom, 'to'=> $sTo];
|
||||
}
|
||||
'coordinates' => array()
|
||||
)
|
||||
);
|
||||
|
||||
//Track points
|
||||
$asTrackPoints = $asTrackProps['points'];
|
||||
$iPointCount = count($asTrackPoints);
|
||||
$iInvalidPointCount = 0;
|
||||
$asPrevPoint = [];
|
||||
$asPrevPoint = array();
|
||||
foreach($asTrackPoints as $iIndex=>$asPoint) {
|
||||
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:[];
|
||||
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:array();
|
||||
if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
|
||||
if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
|
||||
$iInvalidPointCount++;
|
||||
@@ -108,15 +211,16 @@ 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');
|
||||
|
||||
//Find first & last track points
|
||||
$asTracksEnds = [];
|
||||
$asTracks = [];
|
||||
$asTracksEnds = array();
|
||||
$asTracks = array();
|
||||
foreach($this->asTracks as $iTrackId=>$asTrack) {
|
||||
$sTrackId = 't'.$iTrackId;
|
||||
$asTracksEnds[$sTrackId] = ['first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates'])];
|
||||
$asTracksEnds[$sTrackId] = array('first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates']));
|
||||
$asTracks[$sTrackId] = $asTrack;
|
||||
}
|
||||
|
||||
@@ -153,33 +257,15 @@ class GeoJson extends Geo {
|
||||
//Move track
|
||||
unset($asTracks[$sTrackId]);
|
||||
$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
|
||||
$asTracks = array_slice($asTracks, 0, $iOffset) + [$sTrackId => $asTrack] + array_slice($asTracks, $iOffset);
|
||||
$asTracks = array_slice($asTracks, 0, $iOffset) + array($sTrackId => $asTrack) + array_slice($asTracks, $iOffset);
|
||||
}
|
||||
|
||||
$this->asTracks = array_values($asTracks);
|
||||
}
|
||||
|
||||
public function getCenter() {
|
||||
$iLatMin = 90;
|
||||
$iLatMax = -90;
|
||||
$iLngMin = 180;
|
||||
$iLngMax = -180;
|
||||
$asMainTracks = array_filter($this->asTracks, function ($astrack) {return $astrack['properties']['type'] == 'main';});
|
||||
foreach($asMainTracks as $asMainTrack) {
|
||||
foreach($asMainTrack['geometry']['coordinates'] as $aiCoords) {
|
||||
if($aiCoords[1] > $iLatMax) $iLatMax = $aiCoords[1];
|
||||
if($aiCoords[1] < $iLatMin) $iLatMin = $aiCoords[1];
|
||||
if($aiCoords[0] > $iLngMax) $iLngMax = $aiCoords[0];
|
||||
if($aiCoords[0] < $iLngMin) $iLngMin = $aiCoords[0];
|
||||
}
|
||||
}
|
||||
|
||||
return ['latitude' => ($iLatMin + $iLatMax)/2, 'longitude' => ($iLngMin + $iLngMax)/2];
|
||||
}
|
||||
|
||||
private function parseOptions($sComment) {
|
||||
private function parseOptions($sComment){
|
||||
$sComment = strip_tags(html_entity_decode($sComment));
|
||||
$asOptions = [self::OPT_SIMPLE=>''];
|
||||
$asOptions = array(self::OPT_SIMPLE=>'');
|
||||
foreach(explode("\n", $sComment) as $sLine) {
|
||||
$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
|
||||
}
|
||||
@@ -188,38 +274,38 @@ class GeoJson extends Geo {
|
||||
|
||||
private function isPointValid($asPointA, $asPointO, $asPointB) {
|
||||
/* A----O Calculate angle AO^OB
|
||||
* \ If angle is within [90% Pi ; 110% Pi], O can be discarded
|
||||
* \ O is valid otherwise
|
||||
* B
|
||||
*/
|
||||
* \ If angle is within [90% Pi ; 110% Pi], O can be discarded
|
||||
* \ O is valid otherwise
|
||||
* B
|
||||
*/
|
||||
|
||||
//Path Turn Check -> -> -> ->
|
||||
//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
|
||||
$fVectorOA = ['lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat'])];
|
||||
$fVectorOB = ['lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat'])];
|
||||
$fVectorOA = array('lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat']));
|
||||
$fVectorOB = array('lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat']));
|
||||
|
||||
$fLengthOA = sqrt(pow($asPointA['lon'] - $asPointO['lon'], 2) + pow($asPointA['lat'] - $asPointO['lat'], 2));
|
||||
$fLengthOB = sqrt(pow($asPointO['lon'] - $asPointB['lon'], 2) + pow($asPointO['lat'] - $asPointB['lat'], 2));
|
||||
|
||||
$fVectorOAxOB = $fVectorOA['lon'] * $fVectorOB['lon'] + $fVectorOA['lat'] * $fVectorOB['lat'];
|
||||
$fAngleAOB = ($fLengthOA != 0 && $fLengthOB != 0) ? acos($fVectorOAxOB/($fLengthOA * $fLengthOB)) : 0;
|
||||
$fAngleAOB = acos($fVectorOAxOB/($fLengthOA * $fLengthOB));
|
||||
|
||||
//Elevation Check
|
||||
//Law of Cosines: angle = arccos((OB² + AO² - AB²) / (2*OB*AO))
|
||||
$fLengthAB = sqrt(pow($asPointB['ele'] - $asPointA['ele'], 2) + pow($fLengthOA + $fLengthOB, 2));
|
||||
$fLengthAO = sqrt(pow($asPointO['ele'] - $asPointA['ele'], 2) + pow($fLengthOA, 2));
|
||||
$fLengthOB = sqrt(pow($asPointB['ele'] - $asPointO['ele'], 2) + pow($fLengthOB, 2));
|
||||
$fAngleAOBElev = ($fLengthOB != 0 && $fLengthAO != 0) ? (acos((pow($fLengthOB, 2) + pow($fLengthAO, 2) - pow($fLengthAB, 2)) / (2 * $fLengthOB * $fLengthAO))) : 0;
|
||||
$fAngleAOBElev = acos((pow($fLengthOB, 2) + pow($fLengthAO, 2) - pow($fLengthAB, 2)) / (2 * $fLengthOB * $fLengthAO));
|
||||
|
||||
return ($fAngleAOB <= (1 - self::MAX_DEVIATION_FLAT) * M_PI || $fAngleAOB >= (1 + self::MAX_DEVIATION_FLAT) * M_PI ||
|
||||
$fAngleAOBElev <= (1 - self::MAX_DEVIATION_ELEV) * M_PI || $fAngleAOBElev >= (1 + self::MAX_DEVIATION_ELEV) * M_PI);
|
||||
}
|
||||
|
||||
private function buildGeoJson() {
|
||||
return json_encode(['type'=>'FeatureCollection', 'features'=>$this->asTracks]);
|
||||
return json_encode(array('type'=>'FeatureCollection', 'features'=>$this->asTracks));
|
||||
}
|
||||
|
||||
private static function getDistance($asPointA, $asPointB) {
|
||||
private static function getDistance($asPointA, $asPointB) {
|
||||
$fLatFrom = $asPointA[1];
|
||||
$fLonFrom = $asPointA[0];
|
||||
$fLatTo = $asPointB[1];
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Mask;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
@@ -22,10 +22,9 @@ class Email extends PhpObject {
|
||||
private $asDests;
|
||||
|
||||
public function __construct($sServName, $sTemplateName='') {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->sServName = $sServName;
|
||||
$this->setTemplate($sTemplateName);
|
||||
$this->asDests = [];
|
||||
$this->asDests = array();
|
||||
}
|
||||
|
||||
public function setTemplate($sTemplateName) {
|
||||
@@ -39,7 +38,7 @@ class Email extends PhpObject {
|
||||
* @param array $asDests Contains: id_user, name, email, language, timezone, active
|
||||
*/
|
||||
public function setDestInfo($asDests) {
|
||||
if(array_key_exists('email', $asDests)) $asDests = [$asDests];
|
||||
if(array_key_exists('email', $asDests)) $asDests = array($asDests);
|
||||
$this->asDests = $asDests;
|
||||
}
|
||||
|
||||
@@ -57,13 +56,12 @@ class Email extends PhpObject {
|
||||
$oPHPMailer->Password = Settings::MAIL_PASS; //SMTP password
|
||||
$oPHPMailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; //Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` encouraged
|
||||
$oPHPMailer->Port = 587; //TCP port to connect to, use 465 for `PHPMailer::ENCRYPTION_SMTPS` above
|
||||
$oPHPMailer->setFrom(Settings::MAIL_FROM, Livetrail::PROJECT_NAME);
|
||||
$oPHPMailer->addReplyTo(Settings::MAIL_FROM, Livetrail::PROJECT_NAME);
|
||||
$oPHPMailer->setFrom(Settings::MAIL_FROM, 'Spotty');
|
||||
$oPHPMailer->addReplyTo(Settings::MAIL_FROM, 'Spotty');
|
||||
|
||||
$bSuccess = true;
|
||||
foreach($this->asDests as $asDest) {
|
||||
//Message
|
||||
$this->oTemplate->setLanguage($asDest['language'], Livetrail::DEFAULT_LANG);
|
||||
$this->oTemplate->setLanguage($asDest['language'], Spot::DEFAULT_LANG);
|
||||
$this->oTemplate->setTimezone($asDest['timezone']);
|
||||
|
||||
//Unsubscribe Link
|
||||
@@ -73,7 +71,7 @@ class Email extends PhpObject {
|
||||
$oPHPMailer->addCustomHeader('List-Unsubscribe-Post','List-Unsubscribe=One-Click');
|
||||
|
||||
//Email Content
|
||||
$this->oTemplate->setTag('timezone', 'lang:time.city', self::getTimeZoneCity($asDest['timezone']));
|
||||
$this->oTemplate->setTag('timezone', 'lang:city_time', self::getTimeZoneCity($asDest['timezone']));
|
||||
$sHtmlMessage = $this->oTemplate->getMask();
|
||||
$sPlainMessage = strip_tags(str_replace('<br />', "\n", $sHtmlMessage));
|
||||
|
||||
@@ -87,10 +85,11 @@ class Email extends PhpObject {
|
||||
|
||||
//Content
|
||||
$oPHPMailer->isHTML(true);
|
||||
$oPHPMailer->Subject = $this->oTemplate->getTranslator()->getTranslation($this->sTemplateName.'.subject');
|
||||
$oPHPMailer->Subject = $this->oTemplate->getTranslator()->getTranslation($this->sTemplateName.'_subject');
|
||||
$oPHPMailer->Body = $sHtmlMessage;
|
||||
$oPHPMailer->AltBody = $sPlainMessage;
|
||||
|
||||
$bSuccess = true;
|
||||
try {
|
||||
$bSuccess = $bSuccess && $oPHPMailer->send();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Db;
|
||||
use Franzz\Objects\Translator;
|
||||
@@ -13,32 +13,25 @@ use \Settings;
|
||||
class Feed extends PhpObject {
|
||||
|
||||
//Spot feed
|
||||
private const FEED_HOOK = 'https://api.findmespot.com/spot-main-web/consumer/rest-api/2.0/public/feed/';
|
||||
private const FEED_TYPE_XML = '/message.xml';
|
||||
private const FEED_TYPE_JSON = '/message.json';
|
||||
private const FEED_MAX_REFRESH = 5 * 60; //Seconds
|
||||
const FEED_HOOK = 'https://api.findmespot.com/spot-main-web/consumer/rest-api/2.0/public/feed/';
|
||||
const FEED_TYPE_XML = '/message.xml';
|
||||
const FEED_TYPE_JSON = '/message.json';
|
||||
const FEED_MAX_REFRESH = 5 * 60; //Seconds
|
||||
|
||||
//Weather
|
||||
private const WEATHER_HOOK = 'https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline';
|
||||
private const WEATHER_PARAM = [
|
||||
const WEATHER_HOOK = 'https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline';
|
||||
const WEATHER_PARAM = array(
|
||||
'key' => Settings::WEATHER_TOKEN,
|
||||
'unitGroup' => 'metric',
|
||||
'lang' => 'en',
|
||||
'include' => 'current',
|
||||
'iconSet' => 'icons2'
|
||||
];
|
||||
|
||||
//Timezone
|
||||
private const TIMEZONE_HOOK = 'http://api.geonames.org/timezoneJSON';
|
||||
);
|
||||
|
||||
//DB Tables
|
||||
public const SPOT_TABLE = 'spots';
|
||||
public const FEED_TABLE = 'feeds';
|
||||
public const MSG_TABLE = 'messages';
|
||||
|
||||
//Hide/Display values
|
||||
public const MSG_HIDDEN = 0;
|
||||
public const MSG_DISPLAYED = 1;
|
||||
const SPOT_TABLE = 'spots';
|
||||
const FEED_TABLE = 'feeds';
|
||||
const MSG_TABLE = 'messages';
|
||||
|
||||
/**
|
||||
* Database Handle
|
||||
@@ -51,7 +44,7 @@ class Feed extends PhpObject {
|
||||
private $iLastUpdate;
|
||||
|
||||
public function __construct(Db &$oDb, $iFeedId=0) {
|
||||
parent::__construct(__CLASS__);
|
||||
parent::__construct(__CLASS__, Settings::DEBUG);
|
||||
$this->oDb = &$oDb;
|
||||
if($iFeedId > 0) $this->setFeedId($iFeedId);
|
||||
}
|
||||
@@ -72,10 +65,10 @@ class Feed extends PhpObject {
|
||||
}
|
||||
|
||||
public function createFeedId($oProjectId) {
|
||||
$this->setFeedId($this->oDb->insertRow(self::FEED_TABLE, [
|
||||
$this->setFeedId($this->oDb->insertRow(self::FEED_TABLE, array(
|
||||
Db::getId(Project::PROJ_TABLE) => $oProjectId,
|
||||
'status' => 'INACTIVE'
|
||||
]));
|
||||
)));
|
||||
return $this->getFeedId();
|
||||
}
|
||||
|
||||
@@ -92,14 +85,14 @@ class Feed extends PhpObject {
|
||||
}
|
||||
|
||||
public function getSpots() {
|
||||
$asSpots = $this->oDb->selectRows(['from'=>self::SPOT_TABLE]);
|
||||
$asSpots = $this->oDb->selectRows(array('from'=>self::SPOT_TABLE));
|
||||
foreach($asSpots as &$asSpot) $asSpot['id'] = $asSpot[Db::getId(self::SPOT_TABLE)];
|
||||
return $asSpots;
|
||||
}
|
||||
|
||||
public function getFeeds($iFeedId=0) {
|
||||
$asInfo = ['from'=>self::FEED_TABLE];
|
||||
if($iFeedId > 0) $asInfo['constraint'] = [Db::getId(self::FEED_TABLE)=>$iFeedId];
|
||||
$asInfo = array('from'=>self::FEED_TABLE);
|
||||
if($iFeedId > 0) $asInfo['constraint'] = array(Db::getId(self::FEED_TABLE)=>$iFeedId);
|
||||
$asFeeds = $this->oDb->selectRows($asInfo);
|
||||
|
||||
foreach($asFeeds as &$asFeed) $asFeed['id'] = $asFeed[Db::getId(self::FEED_TABLE)];
|
||||
@@ -111,21 +104,20 @@ class Feed extends PhpObject {
|
||||
return array_shift($asFeeds);
|
||||
}
|
||||
|
||||
public function getMessages($asConstraints=[]) {
|
||||
$sFeedIdCol = Db::getId(self::FEED_TABLE, true);
|
||||
$asInfo = [
|
||||
'select' => [
|
||||
public function getMessages($asConstraints=array()) {
|
||||
$asInfo = array(
|
||||
'select' => array(
|
||||
Db::getId(self::MSG_TABLE), 'ref_msg_id', 'type', //ID
|
||||
'latitude', 'longitude', //Position
|
||||
'site_time', 'timezone', 'unix_time', //Time
|
||||
'weather_icon', 'weather_cond', 'weather_temp' //Weather
|
||||
],
|
||||
),
|
||||
'from' => self::MSG_TABLE,
|
||||
'join' => [self::FEED_TABLE => Db::getId(self::FEED_TABLE)],
|
||||
'constraint'=> [$sFeedIdCol => $this->getFeedId(), 'display' => self::MSG_DISPLAYED],
|
||||
'constOpe' => [$sFeedIdCol => '=', 'display' => '='],
|
||||
'orderBy' => ['site_time'=>'ASC']
|
||||
];
|
||||
'join' => array(self::FEED_TABLE => Db::getId(self::FEED_TABLE)),
|
||||
'constraint'=> array(Db::getId(self::FEED_TABLE, true) => $this->getFeedId()),
|
||||
'constOpe' => array(Db::getId(self::FEED_TABLE, true) => "="),
|
||||
'orderBy' => array('site_time'=>'ASC')
|
||||
);
|
||||
if(!empty($asConstraints)) $asInfo = array_merge($asInfo, $asConstraints);
|
||||
|
||||
$asResult = $this->oDb->selectRows($asInfo);
|
||||
@@ -134,7 +126,7 @@ class Feed extends PhpObject {
|
||||
$iCount = 0;
|
||||
foreach($asResult as &$asMsg) {
|
||||
if($asMsg['weather_icon'] == '' && $iCount < 3) {
|
||||
$asWeather = $this->getWeather([$asMsg['latitude'], $asMsg['longitude']], $asMsg['unix_time']);
|
||||
$asWeather = $this->getWeather(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unix_time']);
|
||||
$asMsg = array_merge($asMsg, $asWeather);
|
||||
$this->oDb->updateRow(self::MSG_TABLE, $asMsg[Db::getId(self::MSG_TABLE)], $asWeather, false);
|
||||
$iCount++;
|
||||
@@ -145,9 +137,10 @@ class Feed extends PhpObject {
|
||||
return $asResult;
|
||||
}
|
||||
|
||||
public function getLastMessageId($asConstraints=[]) {
|
||||
public function getLastMessageId($asConstraints=array()) {
|
||||
|
||||
$asMessages = $this->getMessages($asConstraints);
|
||||
return end($asMessages)[Db::getId(self::MSG_TABLE)] ?? 0;
|
||||
return end($asMessages)[Db::getId(self::MSG_TABLE)];
|
||||
}
|
||||
|
||||
public function checkUpdateFeed($sProjectMode) {
|
||||
@@ -158,38 +151,13 @@ class Feed extends PhpObject {
|
||||
$oLastUpdate = new \DateTime('@'.$this->iLastUpdate);
|
||||
$oNow = new \DateTime('now');
|
||||
$iSecDiff = $oNow->getTimestamp() - $oLastUpdate->getTimestamp();
|
||||
|
||||
if($iSecDiff > self::FEED_MAX_REFRESH) $bNewMsg = $this->updateFeed();
|
||||
|
||||
if($iSecDiff > self::FEED_MAX_REFRESH && !Settings::DEBUG) $bNewMsg = $this->updateFeed();
|
||||
}
|
||||
|
||||
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([$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() {
|
||||
$bNewMsg = false;
|
||||
$asData = $this->retrieveFeed();
|
||||
@@ -200,53 +168,53 @@ class Feed extends PhpObject {
|
||||
|
||||
//Fix unstable Spot API Structure
|
||||
if(array_key_exists('message', $asMsgs)) $asMsgs = $asMsgs['message']; //Sometimes adds an extra "message" level
|
||||
if(!array_key_exists(0, $asMsgs)) $asMsgs = [$asMsgs]; //Jumps a level when there is only 1 message
|
||||
if(!array_key_exists(0, $asMsgs)) $asMsgs = array($asMsgs); //Jumps a level when there is only 1 message
|
||||
|
||||
//Update Spot, Feed & Messages
|
||||
if(!empty($asMsgs) && array_key_exists('messengerId', $asMsgs[0])) {
|
||||
|
||||
//Update Spot Info from the first message
|
||||
$asSpotInfo = [
|
||||
$asSpotInfo = array(
|
||||
'ref_spot_id' => $asMsgs[0]['messengerId'],
|
||||
'name' => $asMsgs[0]['messengerName'],
|
||||
'model' => $asMsgs[0]['modelId']
|
||||
];
|
||||
$iSpotId = $this->oDb->insertUpdateRow(self::SPOT_TABLE, $asSpotInfo, ['ref_spot_id']);
|
||||
);
|
||||
$iSpotId = $this->oDb->insertUpdateRow(self::SPOT_TABLE, $asSpotInfo, array('ref_spot_id'));
|
||||
|
||||
//Update Feed Info and last update date
|
||||
$asFeedInfo = [
|
||||
$asFeedInfo = array(
|
||||
'ref_feed_id' => $asFeed['id'],
|
||||
Db::getId(self::SPOT_TABLE) => $iSpotId,
|
||||
'name' => $asFeed['name'],
|
||||
'description' => $asFeed['description'],
|
||||
'status' => $asFeed['status'],
|
||||
'last_update' => $sNow
|
||||
];
|
||||
$iFeedId = $this->oDb->insertUpdateRow(self::FEED_TABLE, $asFeedInfo, ['ref_feed_id']);
|
||||
);
|
||||
$iFeedId = $this->oDb->insertUpdateRow(self::FEED_TABLE, $asFeedInfo, array('ref_feed_id'));
|
||||
|
||||
//Update Messages
|
||||
foreach($asMsgs as $asMsg) {
|
||||
$asMsg = [
|
||||
$asMsg = array(
|
||||
'ref_msg_id' => $asMsg['id'],
|
||||
Db::getId(self::FEED_TABLE) => $iFeedId,
|
||||
'type' => $asMsg['messageType'],
|
||||
'latitude' => $asMsg['latitude'],
|
||||
'longitude' => $asMsg['longitude'],
|
||||
'iso_time' => $asMsg['dateTime'], //ISO 8601 time (backup)
|
||||
'site_time' => date(Db::TIMESTAMP_FORMAT, $asMsg['unixTime']), //Conversion to Site Time
|
||||
'timezone' => $this->getTimeZone($asMsg['latitude'], $asMsg['longitude']), //Get message time zone
|
||||
'unix_time' => $asMsg['unixTime'], //UNIX Time (backup)
|
||||
'iso_time' => $asMsg['dateTime'], //ISO 8601 time (backup)
|
||||
'site_time' => date(Db::TIMESTAMP_FORMAT, $asMsg['unixTime']), //Conversion to Site Time
|
||||
'timezone' => Spot::getTimeZoneFromDate($asMsg['dateTime']), //Local Time Zone
|
||||
'unix_time' => $asMsg['unixTime'], //UNIX Time (backup)
|
||||
'content' => $asMsg['messageContent'],
|
||||
'battery_state' => $asMsg['batteryState']
|
||||
];
|
||||
);
|
||||
|
||||
$iMsgId = $this->oDb->selectId(self::MSG_TABLE, ['ref_msg_id'=>$asMsg['ref_msg_id']]);
|
||||
$iMsgId = $this->oDb->selectId(self::MSG_TABLE, array('ref_msg_id'=>$asMsg['ref_msg_id']));
|
||||
if(!$iMsgId) {
|
||||
//First Catch
|
||||
$asMsg['posted_on'] = $sNow;
|
||||
|
||||
//Weather Data
|
||||
$asMsg = array_merge($asMsg, $this->getWeather([$asMsg['latitude'], $asMsg['longitude']], $asMsg['unix_time']));
|
||||
$asMsg = array_merge($asMsg, $this->getWeather(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unix_time']));
|
||||
|
||||
$this->oDb->insertRow(self::MSG_TABLE, $asMsg);
|
||||
$bNewMsg = true;
|
||||
@@ -255,7 +223,7 @@ class Feed extends PhpObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
else $this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), ['last_update'=>$sNow]);
|
||||
else $this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), array('last_update'=>$sNow));
|
||||
|
||||
return $bNewMsg;
|
||||
}
|
||||
@@ -278,64 +246,43 @@ class Feed extends PhpObject {
|
||||
$sWeatherIcon = 'unknown';
|
||||
}
|
||||
|
||||
//Get Condition Language ID
|
||||
$sCondLangId = (new Translator(self::WEATHER_PARAM['lang']))->getTranslationKey($sWeatherCond);
|
||||
//Get Condition ID
|
||||
$sCondKey = (new Translator(self::WEATHER_PARAM['lang']))->getTranslationKey($sWeatherCond);
|
||||
|
||||
return [
|
||||
return array(
|
||||
'weather_icon' => $sWeatherIcon,
|
||||
'weather_cond' => $sCondLangId,
|
||||
'weather_cond' => $sCondKey,
|
||||
'weather_temp' => floatval($sWeatherTemp)
|
||||
];
|
||||
}
|
||||
|
||||
private function getTimeZone($iLat, $iLng) {
|
||||
$asParams = [
|
||||
'username' => Settings::TIMEZONE_USER,
|
||||
'lat' => $iLat,
|
||||
'lng' => $iLng
|
||||
];
|
||||
$sApiUrl = self::TIMEZONE_HOOK.'?'.http_build_query($asParams);
|
||||
|
||||
$asTimeZone = json_decode(file_get_contents($sApiUrl), true);
|
||||
|
||||
return $asTimeZone['timezoneId'] ?? Settings::TIMEZONE;
|
||||
);
|
||||
}
|
||||
|
||||
private function retrieveFeed() {
|
||||
if($this->sRefFeedId == '') {
|
||||
$sContent = '{"response":{"errors":{"error":{"code":"","text":"Feed reference ID missing","description":"Feed reference ID missing"}}}}';
|
||||
}
|
||||
else {
|
||||
$sContent = '[]';
|
||||
if($this->sRefFeedId !='') {
|
||||
$sUrl = self::FEED_HOOK.$this->sRefFeedId.self::FEED_TYPE_JSON;
|
||||
$sContent = file_get_contents($sUrl);
|
||||
}
|
||||
|
||||
return json_decode($sContent, true);
|
||||
}
|
||||
|
||||
private function updateField($sField, $oValue) {
|
||||
$bResult = ($this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), [$sField=>$oValue]) > 0);
|
||||
$bResult = ($this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), array($sField=>$oValue)) > 0);
|
||||
$this->setFeedId($this->getFeedId());
|
||||
|
||||
return $bResult;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$bSuccess = false;
|
||||
$sLangId = '';
|
||||
$asLangParams = [];
|
||||
$asData = [];
|
||||
|
||||
$asResult = array();
|
||||
if($this->getFeedId() > 0) {
|
||||
$asData['id'] = $this->getFeedId();
|
||||
$bSuccess = $this->oDb->deleteRow(self::FEED_TABLE, $this->getFeedId());
|
||||
if(!$bSuccess) $sLangId = 'error.commit_db';
|
||||
}
|
||||
else {
|
||||
$sLangId = 'error.impossible_value';
|
||||
$asLangParams = [$this->getFeedId(), 'feed ID'];
|
||||
$asResult = array(
|
||||
'id' => $this->getFeedId(),
|
||||
'del' => $this->oDb->deleteRow(self::FEED_TABLE, $this->getFeedId()),
|
||||
'desc' => $this->oDb->getLastError()
|
||||
);
|
||||
}
|
||||
else $asResult = array('del'=>false, 'desc'=>'Error while setting project: no Feed ID');
|
||||
|
||||
return Livetrail::getResult($bSuccess, $sLangId, $asData, $asLangParams);
|
||||
return $asResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Db;
|
||||
use \Settings;
|
||||
|
||||
class Map extends PhpObject {
|
||||
|
||||
const MAP_TABLE = 'maps';
|
||||
const MAPPING_TABLE = 'mappings';
|
||||
|
||||
private Db $oDb;
|
||||
|
||||
private $asMaps;
|
||||
|
||||
public function __construct(Db &$oDb) {
|
||||
parent::__construct(__CLASS__, Settings::DEBUG);
|
||||
$this->oDb = &$oDb;
|
||||
$this->setMaps();
|
||||
}
|
||||
|
||||
private function setMaps() {
|
||||
$asMaps = $this->oDb->selectRows(array('from'=>self::MAP_TABLE));
|
||||
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
||||
}
|
||||
|
||||
public function getProjectMaps($iProjectId) {
|
||||
$asMappings = $this->oDb->getArrayQuery("SELECT id_map FROM mappings WHERE id_project = ".$iProjectId." OR id_project IS NULL", true);
|
||||
return array_filter($this->asMaps, function($asMap) use($asMappings) {return in_array($asMap['id_map'], $asMappings);});
|
||||
}
|
||||
|
||||
public function getMapUrl($sCodeName, $asParams) {
|
||||
return self::populateParams($this->asMaps[$sCodeName]['pattern'], $asParams);
|
||||
}
|
||||
|
||||
private static function populateParams($sUrl, $asParams) {
|
||||
foreach($asParams as $sParam=>$sValue) {
|
||||
$sUrl = str_replace('{'.$sParam.'}', $sValue, $sUrl);
|
||||
}
|
||||
|
||||
return $sUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Db;
|
||||
use Franzz\Objects\ToolBox;
|
||||
use \Settings;
|
||||
|
||||
class Media extends PhpObject {
|
||||
|
||||
//DB Tables
|
||||
public const MEDIA_TABLE = 'medias';
|
||||
const MEDIA_TABLE = 'medias';
|
||||
|
||||
//Media folders (works because /public/files is a symlink of /files)
|
||||
public const MEDIA_FOLDER = 'files';
|
||||
public const THUMB_FOLDER = self::MEDIA_FOLDER.'/thumbs';
|
||||
//Media folders
|
||||
const MEDIA_FOLDER = 'files/';
|
||||
const THUMB_FOLDER = self::MEDIA_FOLDER.'thumbs/';
|
||||
|
||||
private const THUMB_MAX_WIDTH = 400;
|
||||
const THUMB_MAX_WIDTH = 400;
|
||||
|
||||
private Db $oDb;
|
||||
private Project $oProject;
|
||||
/**
|
||||
* Database Handle
|
||||
* @var Db
|
||||
*/
|
||||
private $oDb;
|
||||
|
||||
/**
|
||||
* Media Project
|
||||
* @var Project
|
||||
*/
|
||||
private $oProject;
|
||||
private $asMedia;
|
||||
private $asMedias;
|
||||
private $sSystemType;
|
||||
|
||||
private $iMediaId;
|
||||
|
||||
public function __construct(Db &$oDb, Project &$oProject, $iMediaId=0) {
|
||||
parent::__construct(__CLASS__);
|
||||
public function __construct(Db &$oDb, &$oProject, $iMediaId=0) {
|
||||
parent::__construct(__CLASS__, Settings::DEBUG);
|
||||
$this->oDb = &$oDb;
|
||||
$this->oProject = &$oProject;
|
||||
$this->asMedia = [];
|
||||
$this->asMedias = [];
|
||||
$this->asMedia = array();
|
||||
$this->asMedias = array();
|
||||
$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
||||
$this->setMediaId($iMediaId);
|
||||
}
|
||||
|
||||
@@ -45,16 +57,16 @@ class Media extends PhpObject {
|
||||
}
|
||||
|
||||
public function setComment($sComment) {
|
||||
$sLangId = '';
|
||||
$asData = [];
|
||||
$sError = '';
|
||||
$asData = array();
|
||||
if($this->iMediaId > 0) {
|
||||
$bResult = $this->oDb->updateRow(self::MEDIA_TABLE, $this->iMediaId, ['comment'=>$sComment]);
|
||||
if(!$bResult) $sLangId = 'error.commit_db';
|
||||
$bResult = $this->oDb->updateRow(self::MEDIA_TABLE, $this->iMediaId, array('comment'=>$sComment));
|
||||
if(!$bResult) $sError = 'error_commit_db';
|
||||
else $asData = $this->getInfo();
|
||||
}
|
||||
else $sLangId = 'media.no_id';
|
||||
else $sError = 'media_no_id';
|
||||
|
||||
return Livetrail::getResult(($sLangId==''), $sLangId, $asData);
|
||||
return Spot::getResult(($sError==''), $sError, $asData);
|
||||
}
|
||||
|
||||
public function getMediasInfo($oMediaIds=null) {
|
||||
@@ -63,11 +75,11 @@ class Media extends PhpObject {
|
||||
|
||||
if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) {
|
||||
if($this->oProject->getProjectId()) {
|
||||
$asParams = [
|
||||
'select' => [Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'type AS subtype', 'comment'],
|
||||
$asParams = array(
|
||||
'select' => array(Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'width', 'height', 'rotate', 'type AS subtype', 'comment'),
|
||||
'from' => self::MEDIA_TABLE,
|
||||
'constraint'=> [Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()]
|
||||
];
|
||||
'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId())
|
||||
);
|
||||
if($bOwnMedia) $asParams['constraint'][Db::getId(self::MEDIA_TABLE)] = $oMediaIds;
|
||||
if($bConstraintArray) $asParams = array_merge($asParams, $oMediaIds);
|
||||
|
||||
@@ -83,7 +95,7 @@ class Media extends PhpObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
return $bOwnMedia?$this->asMedia:$asMedias ?? [];
|
||||
return $bOwnMedia?$this->asMedia:$asMedias;
|
||||
}
|
||||
|
||||
public function getInfo() {
|
||||
@@ -95,89 +107,80 @@ class Media extends PhpObject {
|
||||
}
|
||||
|
||||
public function addMedia($sMediaName, $sMethod='upload') {
|
||||
$sLangId = '';
|
||||
$asParams = [];
|
||||
$sError = '';
|
||||
$asParams = array();
|
||||
if(!$this->isProjectEditable() && $sMethod!='sync') {
|
||||
$sLangId = 'upload.mode_archived';
|
||||
$sError = 'upload_mode_archived';
|
||||
$asParams[] = $this->oProject->getProjectCodeName();
|
||||
}
|
||||
elseif($this->oDb->pingValue(self::MEDIA_TABLE, ['filename'=>$sMediaName]) && $sMethod!='sync') {
|
||||
$sLangId = 'upload.media.exists';
|
||||
elseif($this->oDb->pingValue(self::MEDIA_TABLE, array('filename'=>$sMediaName)) && $sMethod!='sync') {
|
||||
$sError = 'upload_media_exist';
|
||||
$asParams[] = $sMediaName;
|
||||
}
|
||||
else {
|
||||
$asMediaInfo = $this->getMediaInfoFromFile($sMediaName);
|
||||
|
||||
//Converting times to Site Time Zone, by using date()
|
||||
//Media Timezone is kept in a separate field for later conversion to Local Time
|
||||
$asDbInfo = [
|
||||
//Converting times to DB Time Zone, by using date()
|
||||
$asDbInfo = array(
|
||||
Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(),
|
||||
'filename' => $sMediaName,
|
||||
'taken_on' => date(Db::TIMESTAMP_FORMAT, ($asMediaInfo['taken_ts'] > 0)?$asMediaInfo['taken_ts']:$asMediaInfo['file_ts']),
|
||||
'taken_on' => ($asMediaInfo['taken_ts'] > 0)?date(Db::TIMESTAMP_FORMAT, $asMediaInfo['taken_ts']):0,
|
||||
'posted_on' => date(Db::TIMESTAMP_FORMAT, $asMediaInfo['file_ts']),
|
||||
'timezone' => $asMediaInfo['timezone'],
|
||||
'latitude' => is_null($asMediaInfo['latitude'])?'NULL':$asMediaInfo['latitude'],
|
||||
'longitude' => is_null($asMediaInfo['longitude'])?'NULL':$asMediaInfo['longitude'],
|
||||
'altitude' => is_null($asMediaInfo['altitude'])?'NULL':$asMediaInfo['altitude'],
|
||||
'width' => $asMediaInfo['width'],
|
||||
'height' => $asMediaInfo['height'],
|
||||
'rotate' => $asMediaInfo['rotate'],
|
||||
'type' => $asMediaInfo['type']
|
||||
];
|
||||
);
|
||||
|
||||
if($sMethod=='sync') $iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, ['filename']);
|
||||
if($sMethod=='sync') $iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, array('filename'));
|
||||
else $iMediaId = $this->oDb->insertRow(self::MEDIA_TABLE, $asDbInfo);
|
||||
|
||||
if(!$iMediaId) $sLangId = 'error.commit_db';
|
||||
if(!$iMediaId) $sError = 'error_commit_db';
|
||||
else {
|
||||
$this->setMediaId($iMediaId);
|
||||
$asParams = $this->getInfo(); //Creates thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
return Livetrail::getResult(($sLangId==''), $sLangId, $asParams);
|
||||
return Spot::getResult(($sError==''), $sError, $asParams);
|
||||
}
|
||||
|
||||
private function getMediaInfoFromFile($sMediaName) {
|
||||
private function getMediaInfoFromFile($sMediaName)
|
||||
{
|
||||
$sMediaPath = self::getMediaPath($sMediaName);
|
||||
$sType = self::getMediaType($sMediaName);
|
||||
|
||||
$iTimeStamp = $iTakenOn = 0;
|
||||
$iPostedOn = filemtime($sMediaPath);
|
||||
$sTimeZone = date_default_timezone_get();
|
||||
$iWidth = 0;
|
||||
$iHeight = 0;
|
||||
$sRotate = '0';
|
||||
$sTakenOn = '';
|
||||
$fLat = null;
|
||||
$fLng = null;
|
||||
$iAlt = null;
|
||||
switch($sType) {
|
||||
case 'video':
|
||||
$asResult = [];
|
||||
$sParams = implode(' ', [
|
||||
$asResult = array();
|
||||
$sParams = implode(' ', array(
|
||||
'-loglevel error', //Remove comments
|
||||
'-select_streams v:0', //First video channel
|
||||
'-show_entries '. //filter tags : Width, Height, Creation Time, Location & Rotation
|
||||
'format_tags=creation_time,com.apple.quicktime.creationdate,com.apple.quicktime.location.ISO6709:'.
|
||||
'-show_entries '. //filter tags : Width, Height, Creation Time & Rotation
|
||||
'format_tags=creation_time,com.apple.quicktime.creationdate:'.
|
||||
'stream_tags=rotate,creation_time:'.
|
||||
'stream=width,height',
|
||||
'-print_format json', //output format: json
|
||||
'-i' //input file
|
||||
]);
|
||||
exec('ffprobe '.$sParams.' '.escapeshellarg($sMediaPath), $asResult);
|
||||
));
|
||||
exec('ffprobe '.$sParams.' "'.$sMediaPath.'"', $asResult);
|
||||
$asExif = json_decode(implode('', $asResult), true);
|
||||
|
||||
//Taken On
|
||||
if(isset($asExif['format']['tags']['com.apple.quicktime.creationdate'])) {
|
||||
$sTakenOn = $asExif['format']['tags']['com.apple.quicktime.creationdate']; //contains Time Zone
|
||||
$sTimeZone = Livetrail::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone;
|
||||
$sTimeZone = Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone;
|
||||
}
|
||||
else $sTakenOn = $asExif['format']['tags']['creation_time'] ?? $asExif['streams'][0]['tags']['creation_time'];
|
||||
|
||||
//Location
|
||||
if(isset($asExif['format']['tags']['com.apple.quicktime.location.ISO6709'])) {
|
||||
list($fLat, $fLng, $iAlt) = self::getLatLngAltFromISO6709($asExif['format']['tags']['com.apple.quicktime.location.ISO6709']);
|
||||
}
|
||||
|
||||
//Width & Height
|
||||
$iWidth = $asExif['streams'][0]['width'];
|
||||
$iHeight = $asExif['streams'][0]['height'];
|
||||
@@ -187,7 +190,7 @@ class Media extends PhpObject {
|
||||
break;
|
||||
case 'image':
|
||||
$asExif = @exif_read_data($sMediaPath, 0, true);
|
||||
if($asExif === false) $asExif = [];
|
||||
if($asExif === false) $asExif = array();
|
||||
list($iWidth, $iHeight) = getimagesize($sMediaPath);
|
||||
|
||||
//Posted On
|
||||
@@ -203,20 +206,13 @@ class Media extends PhpObject {
|
||||
* 3. Timezone extracted from DateTimeOriginal
|
||||
* 4. Uploader Browser Time Zone (PHP Default Time Zone)
|
||||
*/
|
||||
$sTimeZone = $asExif['EXIF']['OffsetTimeOriginal'] ?? $asExif['EXIF']['UndefinedTag:0x9011'] ?? Livetrail::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone;
|
||||
}
|
||||
|
||||
//Location
|
||||
if(array_key_exists('GPS', $asExif)) {
|
||||
$asGps = $asExif['GPS'];
|
||||
$fLat = self::getLatLngFromExif($asGps['GPSLatitudeRef'] ?? $asGps['UndefinedTag:0x0001'], $asGps['GPSLatitude'] ?? $asGps['UndefinedTag:0x0002']);
|
||||
$fLng = self::getLatLngFromExif($asGps['GPSLongitudeRef'] ?? $asGps['UndefinedTag:0x0003'], $asGps['GPSLongitude'] ?? $$asGps['UndefinedTag:0x0004']);
|
||||
$iAlt = (($asGps['GPSAltitudeRef'] ?? $asGps['UndefinedTag:0x0005'] ?? '0') == '1'?-1:1) * ($asGps['GPSAltitude'] ?? $asGps['UndefinedTag:0x0006'] ?? 0);
|
||||
$sTimeZone = $asExif['EXIF']['OffsetTimeOriginal'] ?? $asExif['EXIF']['UndefinedTag:0x9011'] ?? Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone;
|
||||
}
|
||||
|
||||
//Orientation
|
||||
if(array_key_exists('IFD0', $asExif) && array_key_exists('Orientation', $asExif['IFD0'])) {
|
||||
switch($asExif['IFD0']['Orientation']) {
|
||||
switch($asExif['IFD0']['Orientation'])
|
||||
{
|
||||
case 1: $sRotate = '0'; break; //None
|
||||
case 3: $sRotate = '180'; break; //Flip over
|
||||
case 6: $sRotate = '90'; break; //Clockwise
|
||||
@@ -228,27 +224,28 @@ class Media extends PhpObject {
|
||||
|
||||
//Assign the correct Time Zone to $sTakenOn if it is not already contained in it. Then get Unix Timestamp
|
||||
//Time Zone (2nd parameter) will be ignored if already contained in $sTakenOn
|
||||
$iTakenOn = 0;
|
||||
if($sTakenOn != '') {
|
||||
$oTakenOn = new \DateTime($sTakenOn, new \DateTimeZone($sTimeZone));
|
||||
$iTakenOn = $oTakenOn->format('U');
|
||||
}
|
||||
|
||||
return [
|
||||
//Merge timestamps
|
||||
$iTimeStamp = ($iTakenOn > 0)?$iTakenOn:$iPostedOn;
|
||||
|
||||
return array(
|
||||
'timestamp' => $iTimeStamp,
|
||||
'timezone' => $sTimeZone,
|
||||
'latitude' => $fLat,
|
||||
'longitude' => $fLng,
|
||||
'altitude' => $iAlt,
|
||||
'taken_ts' => $iTakenOn,
|
||||
'file_ts' => $iPostedOn,
|
||||
'width' => $iWidth,
|
||||
'height' => $iHeight,
|
||||
'rotate' => $sRotate,
|
||||
'type' => $sType
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
private function getMediaThumbnail($sMediaName) {
|
||||
private function getMediaThumbnail($sMediaName)
|
||||
{
|
||||
$sMediaPath = self::getMediaPath($sMediaName);
|
||||
$sThumbPath = self::getMediaPath($sMediaName, 'thumbnail');
|
||||
|
||||
@@ -261,67 +258,39 @@ class Media extends PhpObject {
|
||||
case 'video':
|
||||
//Get a screenshot of the video 1 second in
|
||||
$sTempPath = self::getMediaPath(uniqid('temp_').'.png');
|
||||
$asResult = [];
|
||||
$sParams = implode(' ', [
|
||||
'-i '.escapeshellarg($sMediaPath), //input file
|
||||
'-ss 00:00:01.000', //Image taken after x seconds
|
||||
'-vframes 1', //number of video frames to output
|
||||
escapeshellarg($sTempPath), //output file
|
||||
]);
|
||||
$asResult = array();
|
||||
$sParams = implode(' ', array(
|
||||
'-i "'.$sMediaPath.'"', //input file
|
||||
'-ss 00:00:01.000', //Image taken after x seconds
|
||||
'-vframes 1', //number of video frames to output
|
||||
'"'.$sTempPath.'"', //output file
|
||||
));
|
||||
exec('ffmpeg '.$sParams, $asResult);
|
||||
|
||||
//Resize
|
||||
$asThumbInfo = ToolBox::createThumbnail($sTempPath, self::THUMB_MAX_WIDTH, 0, $sThumbPath, true);
|
||||
break;
|
||||
default:
|
||||
$asThumbInfo = ['error'=>'error.unknown_type', 'out'=>''];
|
||||
}
|
||||
|
||||
}
|
||||
else $asThumbInfo = ['error'=>'', 'out'=>$sThumbPath];
|
||||
else $asThumbInfo = array('error'=>'', 'out'=>$sThumbPath);
|
||||
|
||||
return ($asThumbInfo['error']=='')?$asThumbInfo['out']:$sMediaPath;
|
||||
}
|
||||
|
||||
private static function getMediaPath($sMediaName, $sFileType='media') {
|
||||
if($sFileType=='thumbnail') return self::THUMB_FOLDER.'/'.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':'');
|
||||
else return self::MEDIA_FOLDER.'/'.$sMediaName;
|
||||
if($sFileType=='thumbnail') return self::THUMB_FOLDER.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':'');
|
||||
else return self::MEDIA_FOLDER.$sMediaName;
|
||||
}
|
||||
|
||||
private static function getMediaType($sMediaName) {
|
||||
$sMediaPath = self::getMediaPath($sMediaName);
|
||||
$sMediaMime = mime_content_type($sMediaPath);
|
||||
switch($sMediaMime) {
|
||||
case 'video/quicktime':
|
||||
case 'video/mp4': $sType = 'video'; break;
|
||||
case 'video/quicktime': $sType = 'video'; break;
|
||||
default: $sType = 'image'; break;
|
||||
}
|
||||
|
||||
return $sType;
|
||||
}
|
||||
|
||||
private static function getLatLngFromExif($sDirection, $asCoords) {
|
||||
$fCoord = 0;
|
||||
foreach($asCoords as $iIndex=>$sCoord) {
|
||||
$fValue = 0;
|
||||
$asCoordParts = explode('/', $sCoord);
|
||||
switch(count($asCoordParts)) {
|
||||
case 1:
|
||||
$fValue = $asCoordParts[0];
|
||||
break;
|
||||
case 2:
|
||||
$fValue = floatval($asCoordParts[0]) / floatval($asCoordParts[1]);
|
||||
break;
|
||||
}
|
||||
|
||||
$fCoord += $fValue / pow(60, $iIndex);
|
||||
}
|
||||
|
||||
return $fCoord * (($sDirection == 'W' || $sDirection == 'S')?-1:1);
|
||||
}
|
||||
|
||||
private static function getLatLngAltFromISO6709($sIso6709) {
|
||||
preg_match('/^(?P<lat>[\+\-][0,1]?\d{2}\.\d+)(?P<lng>[\+\-][0,1]?\d{2}\.\d+)(?P<alt>[\+\-]\d+)?/', $sIso6709, $asMatches);
|
||||
return [floatval($asMatches['lat']), floatval($asMatches['lng']), floatval($asMatches['alt'] ?? 0)];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Db;
|
||||
use \Settings;
|
||||
|
||||
class Project extends PhpObject {
|
||||
|
||||
//Spot Mode
|
||||
public const MODE_PREVIZ = 'P';
|
||||
public const MODE_BLOG = 'B';
|
||||
public const MODE_HISTO = 'H';
|
||||
public const MODES = ['previz'=>self::MODE_PREVIZ, 'blog'=>self::MODE_BLOG, 'histo'=>self::MODE_HISTO];
|
||||
const MODE_PREVIZ = 'P';
|
||||
const MODE_BLOG = 'B';
|
||||
const MODE_HISTO = 'H';
|
||||
const MODES = array('previz'=>self::MODE_PREVIZ, 'blog'=>self::MODE_BLOG, 'histo'=>self::MODE_HISTO);
|
||||
|
||||
//DB Tables
|
||||
public const PROJ_TABLE = 'projects';
|
||||
const PROJ_TABLE = 'projects';
|
||||
|
||||
/**
|
||||
* Database Handle
|
||||
@@ -22,13 +23,13 @@ class Project extends PhpObject {
|
||||
private $oDb;
|
||||
|
||||
private $iProjectId;
|
||||
private $sMode;
|
||||
private $sName;
|
||||
private $sCodeName;
|
||||
private $sMode;
|
||||
private $asActive;
|
||||
|
||||
public function __construct(Db &$oDb, $iProjectId=0) {
|
||||
parent::__construct(__CLASS__);
|
||||
parent::__construct(__CLASS__, Settings::DEBUG);
|
||||
$this->oDb = &$oDb;
|
||||
if($iProjectId > 0) $this->setProjectId($iProjectId);
|
||||
}
|
||||
@@ -50,17 +51,17 @@ class Project extends PhpObject {
|
||||
* Mode --P--][--------B--------][--P--][-----------B---------------][---P---][-----B-----][---------H----------
|
||||
*/
|
||||
$sQuery =
|
||||
'SELECT MAX(id_project) '.
|
||||
'FROM projects '.
|
||||
'WHERE active_to = ('.
|
||||
'SELECT MIN(active_to) '. //Select closest project in the future
|
||||
'FROM projects '.
|
||||
'WHERE active_to > NOW() '. //Select Next project
|
||||
'OR active_to = ('. //In case there is no next project, select the last one
|
||||
'SELECT MAX(active_to) '.
|
||||
'FROM projects'.
|
||||
')'.
|
||||
')';
|
||||
"SELECT MAX(id_project) ".
|
||||
"FROM projects ".
|
||||
"WHERE active_to = (".
|
||||
"SELECT MIN(active_to) ". //Select closest project in the future
|
||||
"FROM projects ".
|
||||
"WHERE active_to > NOW() ". //Select Next project
|
||||
"OR active_to = (". //In case there is no next project, select the last one
|
||||
"SELECT MAX(active_to) ".
|
||||
"FROM projects".
|
||||
")".
|
||||
")";
|
||||
$asResult = $this->oDb->getArrayQuery($sQuery, true);
|
||||
$this->iProjectId = array_shift($asResult);
|
||||
}
|
||||
@@ -69,7 +70,7 @@ class Project extends PhpObject {
|
||||
}
|
||||
|
||||
public function createProjectId() {
|
||||
$this->setProjectId($this->oDb->insertRow(self::PROJ_TABLE, ['codename'=>'']));
|
||||
$this->setProjectId($this->oDb->insertRow(self::PROJ_TABLE, array('codename'=>'')));
|
||||
return $this->getProjectId();
|
||||
}
|
||||
|
||||
@@ -111,50 +112,39 @@ class Project extends PhpObject {
|
||||
return $this->oDb->selectColumn(
|
||||
Feed::FEED_TABLE,
|
||||
Db::getId(Feed::FEED_TABLE),
|
||||
[Db::getId(self::PROJ_TABLE) => $this->getProjectId()]
|
||||
array(Db::getId(self::PROJ_TABLE) => $this->getProjectId())
|
||||
);
|
||||
}
|
||||
|
||||
public function getProjects($iProjectId=0) {
|
||||
$bSpecificProj = ($iProjectId > 0);
|
||||
$sDefaultProjectCodeName = $this->getProjectCodeName();
|
||||
$asInfo = [
|
||||
'select'=> [
|
||||
Db::getId(self::PROJ_TABLE).' AS id',
|
||||
$asInfo = array(
|
||||
'select'=> array(
|
||||
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'
|
||||
],
|
||||
'from' => self::PROJ_TABLE,
|
||||
'orderBy' => ['active_from' => 'ASC']
|
||||
];
|
||||
if($bSpecificProj) $asInfo['constraint'] = [Db::getId(self::PROJ_TABLE)=>$iProjectId];
|
||||
"IF(NOW() BETWEEN active_from AND active_to, 1, IF(NOW() < active_from, 0, 2)) AS mode"
|
||||
),
|
||||
'from' => self::PROJ_TABLE
|
||||
);
|
||||
if($bSpecificProj) $asInfo['constraint'] = array(Db::getId(self::PROJ_TABLE)=>$iProjectId);
|
||||
|
||||
$asProjects = $this->oDb->selectRows($asInfo, 'codename');
|
||||
foreach($asProjects as $sCodeName => &$asProject) {
|
||||
//Update geoJSON
|
||||
if(Converter::hasGpxFile($sCodeName) && (!Converter::isGeoJsonValid($sCodeName) || !$asProject['latitude'] && !$asProject['longitude'])) {
|
||||
$aiCenter = Converter::convertToGeoJson($sCodeName)['center'];
|
||||
$asProject['longitude'] = $aiCenter['longitude'];
|
||||
$asProject['latitude'] = $aiCenter['latitude'];
|
||||
|
||||
$this->oDb->updateRow(self::PROJ_TABLE, $asProject['id'], ['latitude' => $asProject['latitude'], 'longitude' => $asProject['longitude']]);
|
||||
}
|
||||
|
||||
foreach($asProjects as $sCodeName=>&$asProject) {
|
||||
switch($asProject['mode']) {
|
||||
case 0: $asProject['mode'] = self::MODE_PREVIZ; break;
|
||||
case 1: $asProject['mode'] = self::MODE_BLOG; break;
|
||||
case 2: $asProject['mode'] = self::MODE_HISTO; break;
|
||||
}
|
||||
$asProject['editable'] = $this->isModeEditable($asProject['mode']);
|
||||
$asProject['gpxfilepath'] = Livetrail::addTimestampToFilePath(Gpx::getFrontendFilePath($sCodeName));
|
||||
$asProject['geofilepath'] = Livetrail::addTimestampToFilePath(GeoJson::getFrontendFilePath($sCodeName));
|
||||
|
||||
if($sCodeName != '' && !Converter::isGeoJsonValid($sCodeName)) Converter::convertToGeoJson($sCodeName);
|
||||
|
||||
$asProject['geofilepath'] = Spot::addTimestampToFilePath(Geo::getFilePath($sCodeName, GeoJson::EXT));
|
||||
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Geo::getFilePath($sCodeName, Gpx::EXT));
|
||||
$asProject['codename'] = $sCodeName;
|
||||
$asProject['default'] = ($sCodeName == $sDefaultProjectCodeName);
|
||||
}
|
||||
return $bSpecificProj?$asProject:$asProjects;
|
||||
}
|
||||
@@ -174,7 +164,7 @@ class Project extends PhpObject {
|
||||
return $iLastUpdate;
|
||||
}
|
||||
|
||||
public function getLastMessageId($asConstraints=[]): int {
|
||||
public function getLastMessageId($asConstraints=array()): int {
|
||||
$iLastMsg = 0;
|
||||
|
||||
$asFeedIds = $this->getFeedIds();
|
||||
@@ -189,38 +179,37 @@ class Project extends PhpObject {
|
||||
if($this->getProjectId() > 0) {
|
||||
$asProject = $this->getProject();
|
||||
|
||||
$this->sName = $asProject['name'];
|
||||
$this->sCodeName = $asProject['codename'];
|
||||
$this->sMode = $asProject['mode'];
|
||||
$this->asActive = ['from'=>$asProject['active_from'], 'to'=>$asProject['active_to']];
|
||||
$this->asActive = array('from'=>$asProject['active_from'], 'to'=>$asProject['active_to']);
|
||||
$this->sCodeName = $asProject['codename'];
|
||||
$this->sName = $asProject['name'];
|
||||
$this->asGeo = array('geofile'=>$asProject['geofilepath'], 'gpxfile'=>$asProject['gpxfilepath']);
|
||||
}
|
||||
else $this->addError('Error while setting project: no project ID');
|
||||
}
|
||||
|
||||
private function updateField($sField, $oValue) {
|
||||
$bResult = ($this->oDb->updateRow(self::PROJ_TABLE, $this->getProjectId(), [$sField=>$oValue]) > 0);
|
||||
$bResult = ($this->oDb->updateRow(self::PROJ_TABLE, $this->getProjectId(), array($sField=>$oValue)) > 0);
|
||||
$this->setProjectInfo();
|
||||
|
||||
return $bResult;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$asResult = [];
|
||||
$asResult = array();
|
||||
if($this->getProjectId() > 0) {
|
||||
$asFeedIds = $this->getFeedIds();
|
||||
foreach($asFeedIds as $iFeedId) {
|
||||
$asResult['feed'][] = (new Feed($this->oDb, $iFeedId))->delete();
|
||||
}
|
||||
|
||||
$bDeleted = $this->oDb->deleteRow(self::PROJ_TABLE, $this->getProjectId());
|
||||
$asResult['project'][] = [
|
||||
$asResult['project'][] = array(
|
||||
'id' => $this->getProjectId(),
|
||||
'del' => $bDeleted,
|
||||
'desc_lang_id' => $bDeleted?'':'error.commit_db',
|
||||
'desc_lang_params' => []
|
||||
];
|
||||
'del' => $this->oDb->deleteRow(self::PROJ_TABLE, $this->getProjectId()),
|
||||
'desc' => $this->oDb->getLastError()
|
||||
);
|
||||
}
|
||||
else $asResult['project'][] = ['del'=>false, 'desc_lang_id'=>'error.impossible_value', 'desc_lang_params'=>[$this->getProjectId(), 'project ID']];
|
||||
else $asResult['project'][] = array('del'=>false, 'desc'=>'Error while setting project: no project ID');
|
||||
|
||||
return $asResult;
|
||||
}
|
||||
@@ -229,7 +218,7 @@ class Project extends PhpObject {
|
||||
return self::isModeEditable($this->getMode());
|
||||
}
|
||||
|
||||
public static function isModeEditable($sMode) {
|
||||
static public function isModeEditable($sMode) {
|
||||
return ($sMode != self::MODE_HISTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\Db;
|
||||
use Franzz\Objects\Main;
|
||||
use Franzz\Objects\Translator;
|
||||
use Franzz\Objects\ToolBox;
|
||||
use Franzz\Objects\Mask;
|
||||
use \Settings;
|
||||
|
||||
/* Timezones
|
||||
* ---------
|
||||
* Site Time: Time Zone in which the User is viewing the Site (default PHP/SQL Timezone)
|
||||
* Local Time: Time Zone in which the Spot Owner is
|
||||
*
|
||||
* - Feeds (table `feeds`):
|
||||
* - last_update: timestamp in Site Time
|
||||
* - Spot Messages (table `messages`):
|
||||
* - unix_time: UNIX (int) in UTC
|
||||
* - site_time: timestamp in Site Time
|
||||
* - iso_time: raw ISO 8601 in Local Time
|
||||
* - Medias (table `medias`):
|
||||
* - posted_on: timestamp in Site Time
|
||||
* - taken_on: timestamp in Site Time
|
||||
* - Posts (table `posts`):
|
||||
* - site_time: timestamp in Site Time
|
||||
*/
|
||||
|
||||
class Spot extends Main
|
||||
{
|
||||
//Database
|
||||
const POST_TABLE = 'posts';
|
||||
|
||||
const FEED_CHUNK_SIZE = 15;
|
||||
const MAIL_CHUNK_SIZE = 5;
|
||||
|
||||
const DEFAULT_LANG = 'en';
|
||||
|
||||
private Project $oProject;
|
||||
private Media $oMedia;
|
||||
private User $oUser;
|
||||
private Map $oMap;
|
||||
|
||||
public function __construct($sProcessPage, $sTimezone)
|
||||
{
|
||||
parent::__construct($sProcessPage, true, __FILE__, $sTimezone);
|
||||
|
||||
$this->oUser = new User($this->oDb);
|
||||
|
||||
$this->oLang = new Translator('', self::DEFAULT_LANG);
|
||||
|
||||
$this->oProject = new Project($this->oDb);
|
||||
$this->oMedia = new Media($this->oDb, $this->oProject);
|
||||
|
||||
$this->oMap = new Map($this->oDb);
|
||||
}
|
||||
|
||||
protected function install()
|
||||
{
|
||||
//Install DB
|
||||
$this->oDb->install();
|
||||
|
||||
//Add first user
|
||||
$iUserId = $this->oDb->insertRow(User::USER_TABLE, array(
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@admin.com',
|
||||
'language' => self::DEFAULT_LANG,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'active' => User::USER_ACTIVE,
|
||||
'clearance' => User::CLEARANCE_ADMIN
|
||||
));
|
||||
$this->oUser->setUserId($iUserId);
|
||||
}
|
||||
|
||||
protected function getSqlOptions()
|
||||
{
|
||||
return array
|
||||
(
|
||||
'tables' => array
|
||||
(
|
||||
Feed::MSG_TABLE => array('ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'timezone', 'unix_time', 'content', 'battery_state', 'posted_on', 'weather_icon', 'weather_cond', 'weather_temp'),
|
||||
Feed::FEED_TABLE => array('ref_feed_id', Db::getId(Feed::SPOT_TABLE), Db::getId(Project::PROJ_TABLE), 'name', 'description', 'status', 'last_update'),
|
||||
Feed::SPOT_TABLE => array('ref_spot_id', 'name', 'model'),
|
||||
Project::PROJ_TABLE => array('name', 'codename', 'active_from', 'active_to'),
|
||||
self::POST_TABLE => array(Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time', 'timezone'),
|
||||
Media::MEDIA_TABLE => array(Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'timezone', 'width', 'height', 'rotate', 'comment'),
|
||||
User::USER_TABLE => array('name', 'email', 'gravatar', 'language', 'timezone', 'active', 'clearance'),
|
||||
Map::MAP_TABLE => array('codename', 'pattern', 'token', 'tile_size', 'min_zoom', 'max_zoom', 'attribution'),
|
||||
Map::MAPPING_TABLE => array(Db::getId(Map::MAP_TABLE) , Db::getId(Project::PROJ_TABLE))
|
||||
),
|
||||
'types' => array
|
||||
(
|
||||
'active' => "BOOLEAN DEFAULT ".User::USER_INACTIVE,
|
||||
'clearance' => "TINYINT(1) DEFAULT ".User::CLEARANCE_USER,
|
||||
'active_from' => "TIMESTAMP DEFAULT 0",
|
||||
'active_to' => "TIMESTAMP DEFAULT 0",
|
||||
'battery_state' => "VARCHAR(10)",
|
||||
'codename' => "VARCHAR(100)",
|
||||
'content' => "LONGTEXT",
|
||||
'comment' => "LONGTEXT",
|
||||
'description' => "VARCHAR(100)",
|
||||
'email' => "VARCHAR(320) NOT NULL",
|
||||
'filename' => "VARCHAR(100) NOT NULL",
|
||||
'iso_time' => "VARCHAR(24)",
|
||||
'language' => "VARCHAR(2)",
|
||||
'last_update' => "TIMESTAMP DEFAULT 0",
|
||||
'latitude' => "DECIMAL(7,5)",
|
||||
'longitude' => "DECIMAL(8,5)",
|
||||
'model' => "VARCHAR(20)",
|
||||
'name' => "VARCHAR(100)",
|
||||
'pattern' => "VARCHAR(200) NOT NULL",
|
||||
'posted_on' => "TIMESTAMP DEFAULT 0",
|
||||
'ref_feed_id' => "VARCHAR(40)",
|
||||
'ref_msg_id' => "INT",
|
||||
'ref_spot_id' => "VARCHAR(10)",
|
||||
'rotate' => "SMALLINT",
|
||||
'site_time' => "TIMESTAMP DEFAULT 0", //DEFAULT 0 removes auto-set to current time
|
||||
'status' => "VARCHAR(10)",
|
||||
'taken_on' => "TIMESTAMP DEFAULT 0",
|
||||
'timezone' => "CHAR(64)", //see mysql.time_zone_name
|
||||
'token' => "VARCHAR(4096)",
|
||||
'type' => "VARCHAR(20)",
|
||||
'unix_time' => "INT",
|
||||
'min_zoom' => "TINYINT UNSIGNED",
|
||||
'max_zoom' => "TINYINT UNSIGNED",
|
||||
'attribution' => "VARCHAR(100)",
|
||||
'gravatar' => "LONGTEXT",
|
||||
'weather_icon' => "VARCHAR(30)",
|
||||
'weather_cond' => "VARCHAR(30)",
|
||||
'weather_temp' => "DECIMAL(3,1)",
|
||||
'tile_size' => "SMALLINT UNSIGNED DEFAULT 256",
|
||||
'width' => "INT",
|
||||
'height' => "INT"
|
||||
),
|
||||
'constraints' => array
|
||||
(
|
||||
Feed::MSG_TABLE => array("UNIQUE KEY `uni_ref_msg_id` (`ref_msg_id`)", "INDEX(`ref_msg_id`)"),
|
||||
Feed::FEED_TABLE => array("UNIQUE KEY `uni_ref_feed_id` (`ref_feed_id`)", "INDEX(`ref_feed_id`)"),
|
||||
Feed::SPOT_TABLE => array("UNIQUE KEY `uni_ref_spot_id` (`ref_spot_id`)", "INDEX(`ref_spot_id`)"),
|
||||
Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
|
||||
Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)",
|
||||
User::USER_TABLE => "UNIQUE KEY `uni_email` (`email`)",
|
||||
Map::MAP_TABLE => "UNIQUE KEY `uni_map_name` (`codename`)"
|
||||
),
|
||||
'cascading_delete' => array
|
||||
(
|
||||
Feed::SPOT_TABLE => array(Feed::FEED_TABLE),
|
||||
Feed::FEED_TABLE => array(Feed::MSG_TABLE),
|
||||
Project::PROJ_TABLE => array(Feed::FEED_TABLE, Media::MEDIA_TABLE, self::POST_TABLE, Map::MAPPING_TABLE),
|
||||
Map::MAP_TABLE => array(Map::MAPPING_TABLE)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getAppMainPage()
|
||||
{
|
||||
//Cache Page List
|
||||
$asPages = array_diff($this->asMasks, array('email_update', 'email_conf'));
|
||||
if(!$this->oUser->checkUserClearance(User::CLEARANCE_ADMIN)) {
|
||||
$asPages = array_diff($asPages, array('admin', 'upload'));
|
||||
}
|
||||
|
||||
return parent::getMainPage(
|
||||
array(
|
||||
'vars' => array(
|
||||
'chunk_size' => self::FEED_CHUNK_SIZE,
|
||||
'default_project_codename' => $this->oProject->getProjectCodeName(),
|
||||
'projects' => $this->oProject->getProjects(),
|
||||
'user' => $this->oUser->getUserInfo()
|
||||
),
|
||||
'consts' => array(
|
||||
'server' => $this->asContext['serv_name'],
|
||||
'modes' => Project::MODES,
|
||||
'clearances' => User::CLEARANCES,
|
||||
'default_timezone' => Settings::TIMEZONE
|
||||
)
|
||||
),
|
||||
'index',
|
||||
array(
|
||||
'language' => $this->oLang->getLanguage(),
|
||||
'host_url' => $this->asContext['serv_name'],
|
||||
'filepath_css' => self::addTimestampToFilePath('style/spot.css'),
|
||||
'filepath_js_d3' => self::addTimestampToFilePath('script/d3.min.js'),
|
||||
'filepath_js_leaflet' => self::addTimestampToFilePath('script/leaflet.min.js'),
|
||||
'filepath_js_jquery' => self::addTimestampToFilePath('script/jquery.min.js'),
|
||||
'filepath_js_jquery_mods' => self::addTimestampToFilePath('script/jquery.mods.js'),
|
||||
'filepath_js_spot' => self::addTimestampToFilePath('script/spot.js'),
|
||||
'filepath_js_lightbox' => self::addTimestampToFilePath('script/lightbox.js')
|
||||
),
|
||||
$asPages
|
||||
);
|
||||
}
|
||||
|
||||
public function checkUserClearance($iClearance) {
|
||||
return $this->oUser->checkUserClearance($iClearance);
|
||||
}
|
||||
|
||||
/* Managing projects */
|
||||
|
||||
public function setProjectId($iProjectId=0) {
|
||||
$this->oProject->setProjectId($iProjectId);
|
||||
}
|
||||
|
||||
public function updateProject() {
|
||||
$bNewMsg = false;
|
||||
$bSuccess = true;
|
||||
$sDesc = '';
|
||||
|
||||
//Update all feeds belonging to the project
|
||||
$asFeeds = $this->oProject->getFeedIds();
|
||||
foreach($asFeeds as $iFeedId) {
|
||||
$oFeed = new Feed($this->oDb, $iFeedId);
|
||||
$bNewMsg = $bNewMsg || $oFeed->checkUpdateFeed($this->oProject->getMode());
|
||||
}
|
||||
|
||||
//Send Update Email
|
||||
if($bNewMsg) {
|
||||
$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;
|
||||
}
|
||||
|
||||
$bSuccess = $oEmail->send();
|
||||
if(!$bSuccess) $sDesc = $oEmail->ErrorInfo;
|
||||
else $sDesc = 'mail_sent';
|
||||
}
|
||||
else $sDesc = 'no_new_msg';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
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);
|
||||
$sFileName = 'spot_cron.sh';
|
||||
$sContent =
|
||||
'#!/bin/bash'."\n".
|
||||
'wget -qO- '.$this->asContext['serv_name'].'index.php?a=update_project > /dev/null'."\n".
|
||||
'#Crontab job: 0 * * * * . '.dirname($_SERVER['SCRIPT_FILENAME']).'/'.$sFileName.' > /dev/null'."\n";
|
||||
$bSuccess = (file_put_contents($sFileName, $sContent)!==false);
|
||||
return self::getJsonResult($bSuccess, '');
|
||||
}
|
||||
|
||||
public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false)
|
||||
{
|
||||
$asMessages = $this->getSpotMessages($asMessageIds);
|
||||
usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
|
||||
|
||||
//Add medias
|
||||
if(!empty($asMessages)) {
|
||||
$asMedias = $this->getMedias('taken_on', $asMediaIds);
|
||||
usort($asMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
|
||||
|
||||
//Assign medias to closest message
|
||||
$iIndex = 0;
|
||||
$iMaxIndex = count($asMessages) - 1;
|
||||
foreach($asMedias as $asMedia) {
|
||||
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
|
||||
if($iIndex == 0) $iMsgIndex = $iIndex;
|
||||
elseif($iIndex > $iMaxIndex) $iMsgIndex = $iMaxIndex;
|
||||
else {
|
||||
$iHalfWayPoint = ($asMessages[$iIndex - 1]['unix_time'] + $asMessages[$iIndex]['unix_time'])/2;
|
||||
$iMsgIndex = ($asMedia['unix_time'] >= $iHalfWayPoint)?$iIndex:($iIndex - 1);
|
||||
}
|
||||
|
||||
$asMessages[$iMsgIndex]['medias'][] = $asMedia;
|
||||
}
|
||||
}
|
||||
|
||||
//Spot Last Update
|
||||
$asLastUpdate = array();
|
||||
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
|
||||
|
||||
$asResult = array(
|
||||
'messages' => $asMessages,
|
||||
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId()),
|
||||
'last_update' => $asLastUpdate
|
||||
);
|
||||
|
||||
return $bInternal?$asResult:self::getJsonResult(true, '', $asResult);
|
||||
}
|
||||
|
||||
public function subscribe($sEmail, $sNickName) {
|
||||
$asResult = $this->oUser->addUser($sEmail, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
|
||||
$asUserInfo = $this->oUser->getUserInfo();
|
||||
|
||||
//Send Confirmation Email
|
||||
if($asResult['result'] && $asResult['desc']=='lang:nl_subscribed') {
|
||||
$oConfEmail = new Email($this->asContext['serv_name'], 'email_conf');
|
||||
$oConfEmail->setDestInfo($asUserInfo);
|
||||
$oConfEmail->send();
|
||||
}
|
||||
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asUserInfo);
|
||||
}
|
||||
|
||||
public function unsubscribe() {
|
||||
$asResult = $this->oUser->removeUser();
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
||||
}
|
||||
|
||||
public function unsubscribeFromEmail($iUserId) {
|
||||
$this->oUser->setUserId($iUserId);
|
||||
$this->oLang->setLanguage($this->oUser->getLang(), self::DEFAULT_LANG);
|
||||
$asResult = $this->oUser->removeUser();
|
||||
|
||||
$sDesc = explode(':', $asResult['desc'])[1];
|
||||
return $this->oLang->getTranslation($sDesc);
|
||||
}
|
||||
|
||||
private function getSpotMessages($asMsgIds=array())
|
||||
{
|
||||
$asConstraints = $this->getFeedConstraints(Feed::MSG_TABLE);
|
||||
if(!empty($asMsgIds)) {
|
||||
$asConstraints['constraint'][Db::getId(Feed::MSG_TABLE)] = $asMsgIds;
|
||||
$asConstraints['constOpe'][Db::getId(Feed::MSG_TABLE)] = 'IN';
|
||||
}
|
||||
|
||||
$asCombinedMessages = array();
|
||||
|
||||
//Get messages from all feeds belonging to the project
|
||||
$asFeeds = $this->oProject->getFeedIds();
|
||||
foreach($asFeeds as $iFeedId) {
|
||||
$oFeed = new Feed($this->oDb, $iFeedId);
|
||||
$asMessages = $oFeed->getMessages($asConstraints);
|
||||
foreach($asMessages as $asMessage)
|
||||
{
|
||||
$asMessage['latitude'] = floatval($asMessage['latitude']);
|
||||
$asMessage['longitude'] = floatval($asMessage['longitude']);
|
||||
$asMessage['lat_dms'] = self::decToDms($asMessage['latitude'], 'lat');
|
||||
$asMessage['lon_dms'] = self::decToDms($asMessage['longitude'], 'lon');
|
||||
$asMessage['displayed_id'] = $asMessage[Db::getId(Feed::MSG_TABLE)];
|
||||
$asMessage['static_img_url'] = $this->oMap->getMapUrl('static', array('x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']));
|
||||
$asMessage['marker_img_url'] = $this->oMap->getMapUrl('static_marker', array('x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']));
|
||||
|
||||
$this->addTimeStamp($asMessage, $asMessage['unix_time'], $asMessage['timezone']);
|
||||
$asCombinedMessages[] = $asMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return $asCombinedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid medias based on $sTimeRefField:
|
||||
* - taken_on: Date/time on which the media was taken
|
||||
* - posted_on: Date/time on which the media was uploaded
|
||||
* @param String $sTimeRefField Field to calculate relative times: 'taken_on' or 'posted_on'
|
||||
* @return Array Medias info
|
||||
*/
|
||||
private function getMedias($sTimeRefField, $asMediaIds=array())
|
||||
{
|
||||
//Constraints
|
||||
$asConstraints = $this->getFeedConstraints(Media::MEDIA_TABLE, $sTimeRefField);
|
||||
if(!empty($asMediaIds)) {
|
||||
$asConstraints['constraint'][Db::getId(Media::MEDIA_TABLE)] = $asMediaIds;
|
||||
$asConstraints['constOpe'][Db::getId(Media::MEDIA_TABLE)] = 'IN';
|
||||
}
|
||||
|
||||
$asMedias = $this->oMedia->getMediasInfo($asConstraints);
|
||||
foreach($asMedias as &$asMedia) {
|
||||
$iTimeStampTakenOn = strtotime($asMedia['taken_on']);
|
||||
$iTimeStampPostedOn = strtotime($asMedia['posted_on']);
|
||||
$asMedia['taken_on_formatted'] = $this->getTimeFormat($iTimeStampTakenOn);
|
||||
$asMedia['taken_on_formatted_local'] = $this->getTimeFormat($iTimeStampTakenOn, $asMedia['timezone']);
|
||||
$asMedia['posted_on_formatted'] = $this->getTimeFormat($iTimeStampPostedOn);
|
||||
$asMedia['posted_on_formatted_local'] = $this->getTimeFormat($iTimeStampPostedOn, $asMedia['timezone']);
|
||||
$asMedia['displayed_id'] = $asMedia[Db::getId(Media::MEDIA_TABLE)];
|
||||
|
||||
$this->addTimeStamp($asMedia, strtotime($asMedia[$sTimeRefField]), $asMedia['timezone']);
|
||||
}
|
||||
|
||||
return $asMedias;
|
||||
}
|
||||
|
||||
private function getPosts($asPostIds=array())
|
||||
{
|
||||
$asInfo = array(
|
||||
'select' => array(Db::getFullColumnName(self::POST_TABLE, '*'), 'gravatar'),
|
||||
'from' => self::POST_TABLE,
|
||||
'join' => array(User::USER_TABLE => Db::getId(User::USER_TABLE))
|
||||
);
|
||||
$asInfo = array_merge($asInfo, $this->getFeedConstraints(self::POST_TABLE));
|
||||
|
||||
if(!empty($asPostIds)) {
|
||||
$asInfo['constraint'][Db::getId(self::POST_TABLE)] = $asPostIds;
|
||||
$asInfo['constOpe'][Db::getId(self::POST_TABLE)] = 'IN';
|
||||
}
|
||||
$asPosts = $this->oDb->selectRows($asInfo);
|
||||
|
||||
foreach($asPosts as &$asPost) {
|
||||
$iUnixTimeStamp = strtotime($asPost['site_time']); //assumes site timezone
|
||||
$asPost['formatted_name'] = Toolbox::mb_ucwords($asPost['name']);
|
||||
unset($asPost[Db::getId(User::USER_TABLE)]);
|
||||
|
||||
$this->addTimeStamp($asPost, $iUnixTimeStamp, $asPost['timezone']);
|
||||
}
|
||||
|
||||
return $asPosts;
|
||||
}
|
||||
|
||||
private function addTimeStamp(&$asData, $iTime, $sTimeZone='') {
|
||||
$asData['unix_time'] = (int) $iTime;
|
||||
$asData['relative_time'] = Toolbox::getDateTimeDesc($iTime, $this->oLang->getLanguage());
|
||||
$asData['formatted_time'] = $this->getTimeFormat($iTime);
|
||||
if($sTimeZone != '') $asData['formatted_time_local'] = $this->getTimeFormat($iTime, $sTimeZone);
|
||||
}
|
||||
|
||||
private function getFeedConstraints($sType, $sTimeField='site_time', $sReturnFormat='array') {
|
||||
$asConsArray = array();
|
||||
$sConsSql = "";
|
||||
$asActPeriod = $this->oProject->getActivePeriod();
|
||||
|
||||
//Filter on Project ID
|
||||
$sConsSql = "WHERE ".Db::getId(Project::PROJ_TABLE)." = ".$this->oProject->getProjectId();
|
||||
$asConsArray = array(
|
||||
'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()),
|
||||
'constOpe' => array(Db::getId(Project::PROJ_TABLE) => "=")
|
||||
);
|
||||
|
||||
//Time Filter
|
||||
switch($sType) {
|
||||
case Feed::MSG_TABLE:
|
||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod;
|
||||
$asConsArray['constOpe'][$sTimeField] = "BETWEEN";
|
||||
$sConsSql .= " AND ".$sTimeField." BETWEEN '".$asActPeriod['from']."' AND '".$asActPeriod['to']."'";
|
||||
break;
|
||||
case Media::MEDIA_TABLE:
|
||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
||||
$asConsArray['constOpe'][$sTimeField] = "<=";
|
||||
$sConsSql .= " AND ".$sTimeField." <= '".$asActPeriod['to']."'";
|
||||
break;
|
||||
case self::POST_TABLE:
|
||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
||||
$asConsArray['constOpe'][$sTimeField] = "<=";
|
||||
$sConsSql .= " AND ".$sTimeField." <= '".$asActPeriod['to']."'";
|
||||
break;
|
||||
}
|
||||
|
||||
return ($sReturnFormat=='array')?$asConsArray:$sConsSql;
|
||||
}
|
||||
|
||||
public function getNewFeed($iRefIdFirst) {
|
||||
$asResult = array();
|
||||
$sDesc = '';
|
||||
|
||||
if($this->oProject->isEditable()) {
|
||||
$asMessageIds = $asMediaIds = array();
|
||||
|
||||
//New Feed Items
|
||||
$asResult = $this->getFeed($iRefIdFirst, ">", "DESC");
|
||||
foreach($asResult['feed'] as $asItem) {
|
||||
switch($asItem['type']) {
|
||||
case 'message':
|
||||
$asMessageIds[] = $asItem['id'];
|
||||
break;
|
||||
case 'media':
|
||||
$asMediaIds[] = $asItem['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//New Markers
|
||||
$asMarkers = $this->getMarkers(
|
||||
empty($asMessageIds)?array(0):$asMessageIds,
|
||||
empty($asMediaIds)?array(0):$asMediaIds,
|
||||
true
|
||||
);
|
||||
|
||||
$asResult = array_merge($asResult, $asMarkers);
|
||||
}
|
||||
else $sDesc = 'mode_histo';
|
||||
|
||||
return self::getJsonResult(true, $sDesc, $asResult);
|
||||
}
|
||||
|
||||
public function getNextFeed($iRefIdLast=0, $bInternal=false) {
|
||||
if($this->oProject->getMode() == Project::MODE_HISTO) {
|
||||
$sDirection = ">";
|
||||
$sSort = "ASC";
|
||||
}
|
||||
else {
|
||||
$sDirection = "<";
|
||||
$sSort = "DESC";
|
||||
}
|
||||
$asResult = $this->getFeed($iRefIdLast, $sDirection, $sSort);
|
||||
return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult);
|
||||
}
|
||||
|
||||
public function getFeed($iRefId=0, $sDirection, $sSort) {
|
||||
$this->oDb->cleanSql($iRefId);
|
||||
$this->oDb->cleanSql($sDirection);
|
||||
$this->oDb->cleanSql($sSort);
|
||||
|
||||
$sMediaRefField = 'posted_on';
|
||||
$sProjectIdField = Db::getId(Project::PROJ_TABLE);
|
||||
$sMsgIdField = Db::getId(Feed::MSG_TABLE);
|
||||
$sMediaIdField = Db::getId(Media::MEDIA_TABLE);
|
||||
$sPostIdField = Db::getId(self::POST_TABLE);
|
||||
$sFeedIdField = Db::getId(Feed::FEED_TABLE);
|
||||
$sQuery = implode(" ", array(
|
||||
"SELECT type, id, ref",
|
||||
"FROM (",
|
||||
"SELECT {$sProjectIdField}, {$sMsgIdField} AS id, 'message' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.0', {$sMsgIdField}) AS ref",
|
||||
"FROM ".Feed::MSG_TABLE,
|
||||
"INNER JOIN ".Feed::FEED_TABLE." USING({$sFeedIdField})",
|
||||
$this->getFeedConstraints(Feed::MSG_TABLE, 'site_time', 'sql'),
|
||||
"UNION",
|
||||
"SELECT {$sProjectIdField}, {$sMediaIdField} AS id, 'media' AS type, CONCAT(UNIX_TIMESTAMP({$sMediaRefField}), '.1', {$sMediaIdField}) AS ref",
|
||||
"FROM ".Media::MEDIA_TABLE,
|
||||
$this->getFeedConstraints(Media::MEDIA_TABLE, $sMediaRefField, 'sql'),
|
||||
"UNION",
|
||||
"SELECT {$sProjectIdField}, {$sPostIdField} AS id, 'post' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.2', {$sPostIdField}) AS ref",
|
||||
"FROM ".self::POST_TABLE,
|
||||
$this->getFeedConstraints(self::POST_TABLE, 'site_time', 'sql'),
|
||||
") AS items",
|
||||
($iRefId > 0)?("WHERE ref ".$sDirection." ".$iRefId):"",
|
||||
"ORDER BY ref ".$sSort,
|
||||
"LIMIT ".self::FEED_CHUNK_SIZE
|
||||
));
|
||||
|
||||
//Get new chunk
|
||||
$asItems = $this->oDb->getArrayQuery($sQuery, true);
|
||||
|
||||
//Update Reference Point with latest/earliest value
|
||||
$iRefIdFirst = $iRefIdLast = 0;
|
||||
if(!empty($asItems)) {
|
||||
$iRefIdLast = end($asItems)['ref'];
|
||||
$iRefIdFirst = reset($asItems)['ref'];
|
||||
}
|
||||
|
||||
//Sort Table IDs by type & Get attributes
|
||||
$asFeedIds = array('message'=>array(), 'media'=>array(), 'message'=>array());
|
||||
foreach($asItems as $asItem) {
|
||||
$asFeedIds[$asItem['type']][$asItem['id']] = $asItem;
|
||||
}
|
||||
$asFeedAttrs = array(
|
||||
'message' => empty($asFeedIds['message'])?array():$this->getSpotMessages(array_keys($asFeedIds['message'])),
|
||||
'media' => empty($asFeedIds['media'])?array():$this->getMedias($sMediaRefField, array_keys($asFeedIds['media'])),
|
||||
'post' => empty($asFeedIds['post'])?array():$this->getPosts(array_keys($asFeedIds['post']))
|
||||
);
|
||||
|
||||
//Replace Array Key with Item ID
|
||||
foreach($asFeedAttrs as $sType=>$asFeedAttr) {
|
||||
foreach($asFeedAttr as $asFeed) {
|
||||
$asFeeds[$sType][$asFeed['id_'.$sType]] = $asFeed;
|
||||
}
|
||||
}
|
||||
|
||||
//Assign
|
||||
foreach($asItems as &$asItem) {
|
||||
$asItem = array_merge($asFeeds[$asItem['type']][$asItem['id']], $asItem);
|
||||
}
|
||||
|
||||
return array('ref_id_last'=>$iRefIdLast, 'ref_id_first'=>$iRefIdFirst, 'sort'=>$sSort, 'feed'=>$asItems);
|
||||
}
|
||||
|
||||
public function addPost($sName, $sPost)
|
||||
{
|
||||
$iPostId = 0;
|
||||
$sDesc = '';
|
||||
|
||||
if($this->oProject->isEditable()) {
|
||||
$asData = array(
|
||||
Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(),
|
||||
'name' => mb_strtolower(trim($sName)),
|
||||
'content' => trim($sPost),
|
||||
'site_time' => date(Db::TIMESTAMP_FORMAT), //Now in Site Time
|
||||
'timezone' => date_default_timezone_get() //Site Time Zone
|
||||
);
|
||||
if($this->oUser->getUserId() > 0) $asData[Db::getId(User::USER_TABLE)] = $this->oUser->getUserId();
|
||||
|
||||
$iPostId = $this->oDb->insertRow(self::POST_TABLE, $asData);
|
||||
|
||||
$this->oUser->updateNickname($sName);
|
||||
}
|
||||
else $sDesc = 'mode_histo';
|
||||
|
||||
return self::getJsonResult(($iPostId > 0), $sDesc);
|
||||
}
|
||||
|
||||
public function upload()
|
||||
{
|
||||
$oUploader = new Uploader($this->oMedia, $this->oLang);
|
||||
|
||||
return $oUploader->sBody;
|
||||
}
|
||||
|
||||
public function addComment($iMediaId, $sComment) {
|
||||
$oMedia = new Media($this->oDb, $this->oProject, $iMediaId);
|
||||
$asResult = $oMedia->setComment($sComment);
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
||||
}
|
||||
|
||||
public function getAdminSettings($sType='') {
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$asData = array(
|
||||
'project' => $this->oProject->getProjects(),
|
||||
'feed' => $oFeed->getFeeds(),
|
||||
'spot' => $oFeed->getSpots(),
|
||||
'user' => $this->oUser->getActiveUsersInfo()
|
||||
);
|
||||
|
||||
foreach($asData['project'] as &$asProject) {
|
||||
$asProject['active_from'] = substr($asProject['active_from'], 0, 10);
|
||||
$asProject['active_to'] = substr($asProject['active_to'], 0, 10);
|
||||
}
|
||||
|
||||
return self::getJsonResult(true, '', $asData);
|
||||
}
|
||||
|
||||
public function setAdminSettings($sType, $iId, $sField, $sValue) {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
$asResult = array();
|
||||
|
||||
switch($sType) {
|
||||
case 'project':
|
||||
$oProject = new Project($this->oDb, $iId);
|
||||
switch($sField) {
|
||||
case 'name':
|
||||
$bSuccess = $oProject->setProjectName($sValue);
|
||||
break;
|
||||
case 'codename':
|
||||
$bSuccess = $oProject->setProjectCodeName($sValue);
|
||||
break;
|
||||
case 'active_from':
|
||||
$bSuccess = $oProject->setActivePeriod($sValue.' 00:00:00', 'from');
|
||||
break;
|
||||
case 'active_to':
|
||||
$bSuccess = $oProject->setActivePeriod($sValue.' 23:59:59', 'to');
|
||||
break;
|
||||
default:
|
||||
$sDesc = $this->oLang->getTranslation('unknown_field', $sField);
|
||||
}
|
||||
$asResult = $oProject->getProject();
|
||||
$asResult['active_from'] = substr($asResult['active_from'], 0, 10);
|
||||
$asResult['active_to'] = substr($asResult['active_to'], 0, 10);
|
||||
break;
|
||||
case 'feed':
|
||||
$oFeed = new Feed($this->oDb, $iId);
|
||||
switch($sField) {
|
||||
case 'ref_feed_id':
|
||||
$bSuccess = $oFeed->setRefFeedId($sValue);
|
||||
break;
|
||||
case 'id_spot':
|
||||
$bSuccess = $oFeed->setSpotId($sValue);
|
||||
break;
|
||||
case 'id_project':
|
||||
$bSuccess = $oFeed->setProjectId($sValue);
|
||||
break;
|
||||
default:
|
||||
$sDesc = $this->oLang->getTranslation('unknown_field', $sField);
|
||||
}
|
||||
$asResult = $oFeed->getFeed();
|
||||
break;
|
||||
case 'user':
|
||||
switch($sField) {
|
||||
case 'clearance':
|
||||
$asReturnCode = $this->oUser->setUserClearance($iId, $sValue);
|
||||
$bSuccess = $asReturnCode['result'];
|
||||
$sDesc = $asReturnCode['desc'];
|
||||
break;
|
||||
default:
|
||||
$sDesc = $this->oLang->getTranslation('unknown_field', $sField);
|
||||
}
|
||||
$asResult = $this->oUser->getActiveUserInfo($iId);
|
||||
break;
|
||||
}
|
||||
if(!$bSuccess && $sDesc=='') $sDesc = Mask::LANG_PREFIX.'error_commit_db';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
|
||||
}
|
||||
|
||||
public function delAdminSettings($sType, $iId) {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
|
||||
switch($sType) {
|
||||
case 'project':
|
||||
$oProject = new Project($this->oDb, $iId);
|
||||
$asResult = $oProject->delete();
|
||||
$sDesc = $asResult['project'][0]['desc'];
|
||||
break;
|
||||
case 'feed':
|
||||
$oFeed = new Feed($this->oDb, $iId);
|
||||
$asResult = array('feed'=>array($oFeed->delete()));
|
||||
$sDesc = $asResult['feed'][0]['desc'];
|
||||
break;
|
||||
}
|
||||
$bSuccess = ($sDesc=='');
|
||||
|
||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
||||
}
|
||||
|
||||
public function createProject() {
|
||||
$oProject = new Project($this->oDb);
|
||||
$iNewProjectId = $oProject->createProjectId();
|
||||
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$oFeed->createFeedId($iNewProjectId);
|
||||
|
||||
return self::getJsonResult($iNewProjectId>0, '', array(
|
||||
'project' => array($oProject->getProject()),
|
||||
'feed' => array($oFeed->getFeed())
|
||||
));
|
||||
}
|
||||
|
||||
public static function decToDms($dValue, $sType) {
|
||||
if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude
|
||||
else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude
|
||||
|
||||
$dLeft = abs($dValue);
|
||||
|
||||
//Degrees
|
||||
$iDegree = floor($dLeft);
|
||||
$dLeft -= $iDegree;
|
||||
|
||||
//Minutes
|
||||
$iMinute = floor($dLeft * 60);
|
||||
$dLeft -= $iMinute / 60;
|
||||
|
||||
//Seconds
|
||||
$fSecond = round($dLeft * 3600, 1);
|
||||
|
||||
return
|
||||
$iDegree.'°'.
|
||||
self::getNumberWithLeadingZeros($iMinute, 2, 0)."'".
|
||||
self::getNumberWithLeadingZeros($fSecond, 2, 1).'"'.
|
||||
$sDirection;
|
||||
}
|
||||
|
||||
public static function getNumberWithLeadingZeros($fValue, $iNbLeadingZeros, $iNbDigits){
|
||||
$sDecimalSeparator = ".";
|
||||
if($iNbDigits > 0) $iNbLeadingZeros += mb_strlen($sDecimalSeparator) + $iNbDigits;
|
||||
$sPattern = '%0'.$iNbLeadingZeros.$sDecimalSeparator.$iNbDigits.'f';
|
||||
return sprintf($sPattern, $fValue);
|
||||
}
|
||||
|
||||
public function getTimeFormat($iTime, $sTimeZone='') {
|
||||
if($sTimeZone=='') $sTimeZone = date_default_timezone_get();
|
||||
|
||||
$oDate = new \DateTime('@'.$iTime);
|
||||
$oDate->setTimezone(new \DateTimeZone($sTimeZone));
|
||||
|
||||
$sDate = $oDate->format('d/m/Y');
|
||||
$sTime = $oDate->format('H:i');
|
||||
return $this->oLang->getTranslation('date_time', array($sDate, $sTime));
|
||||
}
|
||||
|
||||
public static function getTimeZoneFromDate($sDate) {
|
||||
$sTimeZone = null;
|
||||
|
||||
preg_match('/(?<timezone>(\+|\-)\d{2}:?(\d{2}|))$/', $sDate, $asMatch);
|
||||
if(array_key_exists('timezone', $asMatch)) {
|
||||
$sTimeZone = $asMatch['timezone'];
|
||||
|
||||
//Complete short form: +12 => +1200
|
||||
if(strlen($sTimeZone) == 3) $sTimeZone .= '00';
|
||||
|
||||
//Add colon: +1200 => +12:00
|
||||
if(!strpos($sTimeZone, ':')) $sTimeZone = substr_replace($sTimeZone, ':', 3, 0);
|
||||
}
|
||||
return $sTimeZone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\UploadHandler;
|
||||
use Franzz\Objects\Translator;
|
||||
|
||||
class Uploader extends UploadHandler
|
||||
{
|
||||
/**
|
||||
* Medias Management
|
||||
* @var Media
|
||||
*/
|
||||
private $oMedia;
|
||||
|
||||
/**
|
||||
* Languages
|
||||
* @var Translator
|
||||
*/
|
||||
private $oLang;
|
||||
|
||||
public $sBody;
|
||||
|
||||
function __construct(Media &$oMedia, Translator &$oLang)
|
||||
{
|
||||
$this->oMedia = &$oMedia;
|
||||
$this->oLang = &$oLang;
|
||||
$this->sBody = '';
|
||||
parent::__construct(array('image_versions'=>array(), 'accept_file_types'=>'/\.(gif|jpe?g|png|mov|mp4)$/i'));
|
||||
}
|
||||
|
||||
protected function validate($uploaded_file, $file, $error, $index, $content_range) {
|
||||
$bResult = parent::validate($uploaded_file, $file, $error, $index, $content_range);
|
||||
|
||||
//Check project mode
|
||||
if(!$this->oMedia->isProjectEditable()) {
|
||||
$file->error = $this->get_error_message('upload_mode_archived', array($this->oMedia->getProjectCodeName()));
|
||||
$bResult = false;
|
||||
}
|
||||
|
||||
return $bResult;
|
||||
}
|
||||
|
||||
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) {
|
||||
$file = parent::handle_file_upload($uploaded_file, $name, $size, $type, $error, $index, $content_range);
|
||||
|
||||
if(empty($file->error)) {
|
||||
$asResult = $this->oMedia->addMedia($file->name);
|
||||
if(!$asResult['result']) $file->error = $this->get_error_message($asResult['desc'], $asResult['data']);
|
||||
else {
|
||||
$file->id = $this->oMedia->getMediaId();
|
||||
$file->thumbnail = $asResult['data']['thumb_path'];
|
||||
}
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
protected function body($sBodyPart) {
|
||||
$this->sBody .= $sBodyPart;
|
||||
}
|
||||
|
||||
protected function get_error_message($sError, $asParams=array()) {
|
||||
$sTranslatedError = $this->oLang->getTranslation($sError, $asParams);
|
||||
if($sTranslatedError) return $sTranslatedError;
|
||||
elseif(array_key_exists($sError, $this->error_messages)) return $this->error_messages[$sError];
|
||||
else return $sError;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Db;
|
||||
use \Settings;
|
||||
|
||||
class User extends PhpObject {
|
||||
|
||||
//DB Tables
|
||||
const USER_TABLE = 'users';
|
||||
|
||||
//Clearance Levels
|
||||
const USER_ACTIVE = 1;
|
||||
const USER_INACTIVE = 0;
|
||||
const CLEARANCE_USER = 0;
|
||||
const CLEARANCE_ADMIN = 9;
|
||||
const CLEARANCES = array('user'=>self::CLEARANCE_USER, 'admin'=>self::CLEARANCE_ADMIN);
|
||||
|
||||
//Cookie
|
||||
const COOKIE_ID_USER = 'subscriber';
|
||||
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
||||
/**
|
||||
* Database Handle
|
||||
* @var Db
|
||||
*/
|
||||
private $oDb;
|
||||
|
||||
//User Info
|
||||
private $iUserId;
|
||||
private $asUserInfo;
|
||||
|
||||
public function __construct(Db &$oDb) {
|
||||
parent::__construct(__CLASS__, Settings::DEBUG);
|
||||
$this->oDb = &$oDb;
|
||||
$this->iUserId = 0;
|
||||
$this->asUserInfo = array(
|
||||
'id' => 0,
|
||||
Db::getId(self::USER_TABLE) => 0,
|
||||
'name' => '',
|
||||
'email' => '',
|
||||
'language' => '',
|
||||
'timezone' => '',
|
||||
'active' => self::USER_INACTIVE,
|
||||
'clearance' => self::CLEARANCE_USER
|
||||
);
|
||||
$this->checkUserCookie();
|
||||
}
|
||||
|
||||
public function getLang() {
|
||||
return $this->asUserInfo['language'];
|
||||
}
|
||||
|
||||
public function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
$sEmail = trim($sEmail);
|
||||
|
||||
//Check Email availability
|
||||
$iUserId = $this->oDb->selectValue(self::USER_TABLE, Db::getId(self::USER_TABLE), array('email'=>$sEmail, 'active'=>self::USER_ACTIVE));
|
||||
|
||||
if($iUserId > 0) {
|
||||
//Just log user in
|
||||
$sDesc = 'lang:nl_email_exists';
|
||||
$bSuccess = true;
|
||||
}
|
||||
else {
|
||||
//Add/Reactivate user
|
||||
$iUserId = $this->oDb->insertUpdateRow(
|
||||
self::USER_TABLE,
|
||||
array('email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone, 'active'=>self::USER_ACTIVE),
|
||||
array('email')
|
||||
);
|
||||
|
||||
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
||||
else {
|
||||
$sDesc = 'lang:nl_subscribed';
|
||||
$bSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
if($bSuccess) {
|
||||
$this->setUserId($iUserId);
|
||||
|
||||
//Set Cookie (valid 1 year)
|
||||
$this->updateCookie(self::COOKIE_DURATION);
|
||||
|
||||
//Update Nickname if user has already posted
|
||||
$this->updateNickname($sNickName);
|
||||
|
||||
//Retrieve Gravatar image
|
||||
$this->updateGravatar($iUserId, $sEmail);
|
||||
}
|
||||
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
public function removeUser() {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
|
||||
if($this->iUserId > 0) {
|
||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('active'=>self::USER_INACTIVE));
|
||||
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
||||
else {
|
||||
$sDesc = 'lang:nl_unsubscribed';
|
||||
$this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
|
||||
$bSuccess = true;
|
||||
}
|
||||
}
|
||||
else $sDesc = 'lang:nl_unknown_email';
|
||||
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
public function updateNickname($sNickname) {
|
||||
if($this->getUserId() > 0 && $sNickname!='') $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('name'=>$sNickname));
|
||||
}
|
||||
|
||||
private function updateGravatar($iUserId, $sEmail) {
|
||||
$sImage = ($sEmail != '')?@file_get_contents('https://www.gravatar.com/avatar/'.md5($sEmail).'.png?d=404&s=24'):'';
|
||||
$this->oDb->updateRow(self::USER_TABLE, $iUserId, array('gravatar' => base64_encode($sImage)));
|
||||
}
|
||||
|
||||
private function checkUserCookie() {
|
||||
if(isset($_COOKIE[self::COOKIE_ID_USER])){
|
||||
$this->setUserId($_COOKIE[self::COOKIE_ID_USER]);
|
||||
|
||||
//Extend cookie life
|
||||
if($this->getUserId() > 0) $this->updateCookie(self::COOKIE_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserId() {
|
||||
return $this->iUserId;
|
||||
}
|
||||
|
||||
public function setUserId($iUserId) {
|
||||
$this->iUserId = 0;
|
||||
|
||||
$asUser = $this->getActiveUserInfo($iUserId);
|
||||
if(!empty($asUser)) {
|
||||
$this->iUserId = $iUserId;
|
||||
$this->asUserInfo = $asUser;
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserInfo() {
|
||||
return $this->asUserInfo;
|
||||
}
|
||||
|
||||
public function getActiveUserInfo($iUserId) {
|
||||
$asUsersInfo = array();
|
||||
if($iUserId > 0) $asUsersInfo = $this->getActiveUsersInfo($iUserId);
|
||||
return empty($asUsersInfo)?array():array_shift($asUsersInfo);
|
||||
}
|
||||
|
||||
public function getActiveUsersInfo($iUserId=-1) {
|
||||
|
||||
//Mapping between user fields and DB fields
|
||||
$asSelect = array_keys($this->asUserInfo);
|
||||
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE)." AS id";
|
||||
|
||||
//Non-admin cannot access clearance info
|
||||
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) unset($asSelect['clearance']);
|
||||
|
||||
$asInfo = array(
|
||||
'select' => $asSelect,
|
||||
'from' => self::USER_TABLE,
|
||||
'constraint'=> array('active'=>self::USER_ACTIVE)
|
||||
);
|
||||
if($iUserId != -1) $asInfo['constraint'][Db::getId(self::USER_TABLE)] = $iUserId;
|
||||
|
||||
return $this->oDb->selectRows($asInfo);
|
||||
}
|
||||
|
||||
public function checkUserClearance($iClearance)
|
||||
{
|
||||
return ($this->asUserInfo['clearance'] >= $iClearance);
|
||||
}
|
||||
|
||||
public function setUserClearance($iUserId, $iClearance) {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
|
||||
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) $sDesc = 'unauthorized';
|
||||
else {
|
||||
if(!in_array($iClearance, self::CLEARANCES)) $sDesc = 'Setting wrong clearance "'.$iClearance.'" to user ID "'.$iUserId.'"';
|
||||
else {
|
||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('clearance'=>$iClearance));
|
||||
if(!$iUserId) $sDesc = 'lang:error_commit_db';
|
||||
else $bSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
private function updateCookie($iDeltaTime) {
|
||||
setcookie(self::COOKIE_ID_USER, ($iDeltaTime < 0)?'':$this->getUserId(), array('samesite' => 'Lax', 'expires' => time() + $iDeltaTime));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/* Requests Handler */
|
||||
|
||||
//Start buffering
|
||||
ob_start();
|
||||
|
||||
$oLoader = require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
use Franzz\Objects\ToolBox;
|
||||
use Franzz\Objects\Main;
|
||||
use Franzz\Spot\Spot;
|
||||
use Franzz\Spot\User;
|
||||
|
||||
ToolBox::fixGlobalVars($argv ?? array());
|
||||
|
||||
//Available variables
|
||||
$sAction = $_REQUEST['a'] ?? '';
|
||||
$sTimezone = $_REQUEST['t'] ?? '';
|
||||
$sName = $_GET['name'] ?? '';
|
||||
$sContent = $_GET['content'] ?? '';
|
||||
$iProjectId = $_REQUEST['id_project'] ?? 0 ;
|
||||
$sField = $_REQUEST['field'] ?? '';
|
||||
$oValue = $_REQUEST['value'] ?? '';
|
||||
$iId = $_REQUEST['id'] ?? 0 ;
|
||||
$sType = $_REQUEST['type'] ?? '';
|
||||
$sEmail = $_REQUEST['email'] ?? '';
|
||||
|
||||
//Initiate class
|
||||
$oSpot = new Spot(__FILE__, $sTimezone);
|
||||
$oSpot->setProjectId($iProjectId);
|
||||
|
||||
$sResult = '';
|
||||
if($sAction!='')
|
||||
{
|
||||
switch($sAction)
|
||||
{
|
||||
case 'markers':
|
||||
$sResult = $oSpot->getMarkers();
|
||||
break;
|
||||
case 'next_feed':
|
||||
$sResult = $oSpot->getNextFeed($iId);
|
||||
break;
|
||||
case 'new_feed':
|
||||
$sResult = $oSpot->getNewFeed($iId);
|
||||
break;
|
||||
case 'add_post':
|
||||
$sResult = $oSpot->addPost($sName, $sContent);
|
||||
break;
|
||||
case 'subscribe':
|
||||
$sResult = $oSpot->subscribe($sEmail, $sName);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
$sResult = $oSpot->unsubscribe();
|
||||
break;
|
||||
case 'unsubscribe_email':
|
||||
$sResult = $oSpot->unsubscribeFromEmail($iId);
|
||||
break;
|
||||
case 'update_project':
|
||||
$sResult = $oSpot->updateProject();
|
||||
break;
|
||||
default:
|
||||
if($oSpot->checkUserClearance(User::CLEARANCE_ADMIN))
|
||||
{
|
||||
switch($sAction)
|
||||
{
|
||||
case 'upload':
|
||||
$sResult = $oSpot->upload();
|
||||
break;
|
||||
case 'add_comment':
|
||||
$sResult = $oSpot->addComment($iId, $sContent);
|
||||
break;
|
||||
case 'admin_new':
|
||||
$sResult = $oSpot->createProject();
|
||||
break;
|
||||
case 'admin_get':
|
||||
$sResult = $oSpot->getAdminSettings();
|
||||
break;
|
||||
case 'admin_set':
|
||||
$sResult = $oSpot->setAdminSettings($sType, $iId, $sField, $oValue);
|
||||
break;
|
||||
case 'admin_del':
|
||||
$sResult = $oSpot->delAdminSettings($sType, $iId);
|
||||
break;
|
||||
case 'generate_cron':
|
||||
$sResult = $oSpot->genCronFile();
|
||||
break;
|
||||
case 'sql':
|
||||
$sResult = $oSpot->getDbBuildScript();
|
||||
break;
|
||||
default:
|
||||
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
else $sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
else $sResult = $oSpot->getAppMainPage();
|
||||
|
||||
$sDebug = ob_get_clean();
|
||||
if(Settings::DEBUG && $sDebug!='') $oSpot->addUncaughtError($sDebug);
|
||||
|
||||
echo $sResult;
|
||||
@@ -0,0 +1,177 @@
|
||||
locale = en_NZ
|
||||
page_og_desc = Keep contact with François when he is off hiking
|
||||
error_commit_db = Issue committing to DB
|
||||
unknown_field = Field "$0" is unknown
|
||||
|
||||
nav_back = Back
|
||||
|
||||
admin = Admin Panel
|
||||
admin_config = Config
|
||||
admin_upload = Upload
|
||||
save = Save
|
||||
admin_save_success = Saved
|
||||
|
||||
track_main = Main track
|
||||
track_off-track = Off-track
|
||||
track_hitchhiking = Hitchhiking
|
||||
track_download = Download GPX Track
|
||||
|
||||
upload_title = Picture & Video Uploads
|
||||
upload_mode_archived= Project "$0" is archived. No upload allowed
|
||||
upload_success = $0 uploaded successfully
|
||||
upload_media_exist = Picture $0 already exists
|
||||
|
||||
post_message = Message
|
||||
post_name = Name
|
||||
post_new_message = New message
|
||||
and = and
|
||||
counter = #$0
|
||||
send = Send
|
||||
|
||||
maps = Base Maps
|
||||
map_satellite = Satellite
|
||||
map_otm = Open Topo Map
|
||||
map_ign_france = IGN (France)
|
||||
map_ign_spain = IGN (Spain)
|
||||
map_linz = LINZ
|
||||
map_usgs = USGS
|
||||
map_natgeo = National Geographic
|
||||
map_outdoors = Mapbox Outdoors
|
||||
|
||||
image = Picture
|
||||
images = Pictures
|
||||
image_taken = taken on $0
|
||||
video = Video
|
||||
video_taken = shot on $0
|
||||
add_on = added on $0
|
||||
click_watch = Click to watch video
|
||||
click_zoom = Click to zoom
|
||||
media_count = Media $0 / $1
|
||||
media_no_id = Missing Media ID in request
|
||||
media_comment_update= Comment of media "$0" updated
|
||||
see_on_google = See on Google Maps
|
||||
copy_to_clipboard = Copy direct link to clipboard
|
||||
link_copied = Link copied!
|
||||
|
||||
city_time = $0 Time
|
||||
local_time = $0 Local Time
|
||||
your_time = $0 Your Time
|
||||
date_time = $0 at $1
|
||||
time_zone = Time Zone
|
||||
|
||||
id_project = Project ID
|
||||
project = Project
|
||||
projects = Projects
|
||||
new_project = New Project
|
||||
update_project = Update Project
|
||||
hikes = Hikes
|
||||
mode = Mode
|
||||
mode_previz = Project in preparation
|
||||
mode_blog = Active Project
|
||||
mode_histo = Archived Project
|
||||
code_name = Code name
|
||||
start = Start
|
||||
end = End
|
||||
feeds = Feeds
|
||||
id_feed = Feed ID
|
||||
ref_feed_id = Ref. Feed ID
|
||||
id_spot = Spot ID
|
||||
name = Name
|
||||
status = Status
|
||||
last_update = Last Spot update
|
||||
ref_spot_id = Ref. Spot ID
|
||||
model = Model
|
||||
delete = Delete
|
||||
id_user = User ID
|
||||
user_name = User Name
|
||||
active_users = Active Users
|
||||
language = Language
|
||||
clearance = Clearance
|
||||
toolbox = Toolbox
|
||||
|
||||
unit_day = day
|
||||
unit_days = days
|
||||
unit_hour = h
|
||||
|
||||
newsletter = Keep in touch!
|
||||
nl_email_placeholder= my@email.com
|
||||
nl_invalid_email = It doesn't look like an email
|
||||
nl_subscribed_desc = You're all set. We'll send you updates as soon as we get them
|
||||
nl_unsubscribed_desc= Write down your email address and we'll send you François' position as soon as we get it :)
|
||||
nl_email_exists = This email is already subscribed. You can unsubscribe by clicking on the button above.
|
||||
nl_subscribe = Subscribe
|
||||
nl_subscribed = Thanks! You'll receive a confirmation email shortly
|
||||
nl_unsubscribe = Unsubscribe
|
||||
nl_unsubscribed = Done. No more junk mail from us
|
||||
nl_unknown_email = Unknown email address
|
||||
|
||||
email_unsubscribe = PS: Changed your mind?
|
||||
email_unsub_btn = Unsubscribe
|
||||
|
||||
email_conf_subject = Successful Registration
|
||||
conf_preheader = Thanks for keeping in touch!
|
||||
conf_thanks_sub = You're all set!
|
||||
conf_body_para_1 = Thank you for checking in on my wanderings :). I'll make sure to keep you posted on my progress along the trail.
|
||||
conf_body_para_2 = I usually check-in once a day, plus sometimes on special events, like successful peak ascents. I am using a GPS-based device (PLB) which does not require phone reception to work. Thus the messages should be pretty frequent, but, being awestruck by the beauty of nature, I could also just forget to send a signal once in a while. So do not worry if you don't receive anything for a couple of days.
|
||||
conf_body_para_3 = If I've posted some pictures recently, you should also get them in the same email.
|
||||
conf_body_conclusion= See you down the road!
|
||||
conf_signature = --François
|
||||
|
||||
email_update_subject= Spotted!
|
||||
update_preheader = New position received
|
||||
update_title = Message
|
||||
update_latest_news = Latest news:
|
||||
|
||||
distance = Distance
|
||||
elevation = Elevation
|
||||
segment_length = Segment length
|
||||
type = Track Type
|
||||
legend = Legend
|
||||
|
||||
credits_project = Spotty Project
|
||||
credits_git = Git Repository
|
||||
credits_license = under GPLv3 license
|
||||
|
||||
weather_type_1 = Blowing Or Drifting Snow
|
||||
weather_type_2 = Drizzle
|
||||
weather_type_3 = Heavy Drizzle
|
||||
weather_type_4 = Light Drizzle
|
||||
weather_type_5 = Heavy Drizzle/Rain
|
||||
weather_type_6 = Light Drizzle/Rain
|
||||
weather_type_7 = Duststorm
|
||||
weather_type_8 = Fog
|
||||
weather_type_9 = Freezing Drizzle/Freezing Rain
|
||||
weather_type_10 = Heavy Freezing Drizzle/Freezing Rain
|
||||
weather_type_11 = Light Freezing Drizzle/Freezing Rain
|
||||
weather_type_12 = Freezing Fog
|
||||
weather_type_13 = Heavy Freezing Rain
|
||||
weather_type_14 = Light Freezing Rain
|
||||
weather_type_15 = Funnel Cloud/Tornado
|
||||
weather_type_16 = Hail Showers
|
||||
weather_type_17 = Ice
|
||||
weather_type_18 = Lightning Without Thunder
|
||||
weather_type_19 = Mist
|
||||
weather_type_20 = Precipitation In Vicinity
|
||||
weather_type_21 = Rain
|
||||
weather_type_22 = Heavy Rain And Snow
|
||||
weather_type_23 = Light Rain And Snow
|
||||
weather_type_24 = Rain Showers
|
||||
weather_type_25 = Heavy Rain
|
||||
weather_type_26 = Light Rain
|
||||
weather_type_27 = Sky Coverage Decreasing
|
||||
weather_type_28 = Sky Coverage Increasing
|
||||
weather_type_29 = Sky Unchanged
|
||||
weather_type_30 = Smoke Or Haze
|
||||
weather_type_31 = Snow
|
||||
weather_type_32 = Snow And Rain Showers
|
||||
weather_type_33 = Snow Showers
|
||||
weather_type_34 = Heavy Snow
|
||||
weather_type_35 = Light Snow
|
||||
weather_type_36 = Squalls
|
||||
weather_type_37 = Thunderstorm
|
||||
weather_type_38 = Thunderstorm Without Precipitation
|
||||
weather_type_39 = Diamond Dust
|
||||
weather_type_40 = Hail
|
||||
weather_type_41 = Overcast
|
||||
weather_type_42 = Partially cloudy
|
||||
weather_type_43 = Clear
|
||||
@@ -0,0 +1,177 @@
|
||||
locale = es_ES
|
||||
page_og_desc = Mantente en contacto con François durante sus aventuras a la montaña
|
||||
error_commit_db = Error SQL
|
||||
unknown_field = Campo "$0" desconocido
|
||||
|
||||
nav_back = Atrás
|
||||
|
||||
admin = Administración
|
||||
admin_config = Configuración
|
||||
admin_upload = Cargar
|
||||
save = Guardar
|
||||
admin_save_success = Guardado
|
||||
|
||||
track_main = Camino principal
|
||||
track_off-track = Variante
|
||||
track_hitchhiking = Autostop
|
||||
track_download = Descarga la ruta GPX
|
||||
|
||||
upload_title = Cargar fotos y videos
|
||||
upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
|
||||
upload_success = $0 ha sido subido
|
||||
upload_media_exist = La imagen $0 ya existe
|
||||
|
||||
post_message = Mensaje
|
||||
post_name = Nombre
|
||||
post_new_message = Mensaje nuevo
|
||||
and = y
|
||||
counter = No. $0
|
||||
send = Enviar
|
||||
|
||||
maps = Mapas de base
|
||||
map_satellite = Satélite
|
||||
map_otm = Open Topo Map
|
||||
map_ign_france = IGN (Francia)
|
||||
map_ign_spain = IGN (España)
|
||||
map_linz = LINZ
|
||||
map_usgs = USGS
|
||||
map_natgeo = National Geographic
|
||||
map_outdoors = Mapbox Topo
|
||||
|
||||
image = Foto
|
||||
images = Fotos
|
||||
image_taken = Foto tomada el $0
|
||||
video = Video
|
||||
video_taken = Video filmado el $0
|
||||
add_on = Agregado el $0
|
||||
click_watch = Haz clic para ver el video
|
||||
click_zoom = Haz clic para ampliar
|
||||
media_count = Media $0 de $1
|
||||
media_no_id = Falta el ID del sujeto
|
||||
media_comment_update= Comentario "$0" actualizado
|
||||
see_on_google = Ver la posición en Google Maps
|
||||
copy_to_clipboard = Copiar el enlace
|
||||
link_copied = ¡Enlace copiado!
|
||||
|
||||
city_time = Hora de $0
|
||||
local_time = $0 hora local
|
||||
your_time = $0 en tu zona horaria
|
||||
date_time = $0 a la $1
|
||||
time_zone = Huso horario
|
||||
|
||||
id_project = Proyecto ID
|
||||
project = Proyecto
|
||||
projects = Proyectos
|
||||
new_project = Nuevo proyecto
|
||||
update_project = Actualizar el proyecto
|
||||
hikes = Senderos
|
||||
mode = Modo
|
||||
mode_previz = Proyecto en preparación
|
||||
mode_blog = Proyecto activo
|
||||
mode_histo = Proyecto archivado
|
||||
code_name = Nombre clave
|
||||
start = Inicio
|
||||
end = Fin
|
||||
feeds = Feeds
|
||||
id_feed = ID Feed
|
||||
ref_feed_id = ID Feed ref.
|
||||
id_spot = ID Spot
|
||||
name = Descripción
|
||||
status = Estado
|
||||
last_update = Última actualización de Spot
|
||||
ref_spot_id = ID Spot ref.
|
||||
model = Modelo
|
||||
delete = Borrar
|
||||
id_user = ID del usuario
|
||||
user_name = Nombre
|
||||
active_users = Usuarios activos
|
||||
language = Idioma
|
||||
clearance = Nivel de autorización
|
||||
toolbox = Herramientas
|
||||
|
||||
unit_day = Día
|
||||
unit_days = Días
|
||||
unit_hour = h
|
||||
|
||||
newsletter = Mantenerse en contacto
|
||||
nl_email_placeholder= nombre@email.com
|
||||
nl_invalid_email = Esto no parece una dirección de correo electrónico
|
||||
nl_subscribed_desc = Todo esta listo. Te enviaremos noticias frescas en cuanto las recibamos. Prometido...
|
||||
nl_unsubscribed_desc= Anade tu dirección de correo electrónico y te enviaremos la posicion actualizada de François tan pronto como la recibamos :)
|
||||
nl_email_exists = Esta dirección de correo electrónico ya está registrada. Puedes darte de baja haciendo clic en el botón de arriba.
|
||||
nl_subscribe = Suscribir
|
||||
nl_subscribed = ¡Gracias! Recibirás un correo electrónico de confirmación
|
||||
nl_unsubscribe = Desinscribirse
|
||||
nl_unsubscribed = Está hecho. ¡No más spam!
|
||||
nl_unknown_email = Dirección de email desconocida
|
||||
|
||||
email_unsubscribe = PD: ¿Demasiados correos electrónicos?
|
||||
email_unsub_btn = Desinscribirse
|
||||
|
||||
email_conf_subject = Confirmación
|
||||
conf_preheader = ¡Gracias por mantenerte en contacto!
|
||||
conf_thanks_sub = ¡Hecho!
|
||||
conf_body_para_1 = Os agradezco mucho que sigais mi proyecto, y os intereseis de la evolucion. Os prometo que os mantendré informados sobre mi progreso.
|
||||
conf_body_para_2 = Normalmente envío un mensaje una vez al día. Cuando voy a lugares guays, envío uno extra (cimas, ese tipo de cosas). Estoy usando una dispositivo GPS para enviar la señal, por lo que no necesito una red telefónica para que funcione. Sin embargo, puede haber ocasiones en las que presione el botón. Por lo tanto, no se preocupe si no recibe mensajes durante uno o dos días.
|
||||
conf_body_para_3 = Cuando añada fotos en la página, también deberás encontrarlas en este correo electrónico.
|
||||
conf_body_conclusion= ¡Nos vemos en el camino!
|
||||
conf_signature = --François
|
||||
|
||||
email_update_subject= Nueva posición recibida
|
||||
update_preheader = ¡Nueva posición!
|
||||
update_title = Mensaje
|
||||
update_latest_news = Últimas noticias:
|
||||
|
||||
distance = Distancia
|
||||
elevation = Elevación
|
||||
segment_length = Tamaño del segmento
|
||||
type = Tipo de sendero
|
||||
legend = Leyenda
|
||||
|
||||
credits_project = Proyecto Spotty
|
||||
credits_git = Repositorio de Git
|
||||
credits_license = bajo licencia GPLv3
|
||||
|
||||
weather_type_1 Nieve que sopla o a la deriva
|
||||
weather_type_2 Llovizna
|
||||
weather_type_3 Llovizna fuerte
|
||||
weather_type_4 Llovizna ligera
|
||||
weather_type_5 Fuerte llovizna / lluvia
|
||||
weather_type_6 Llovizna ligera / Lluvia
|
||||
weather_type_7 Tormenta de arena
|
||||
weather_type_8 Niebla
|
||||
weather_type_9 Llovizna helada / Lluvia helada
|
||||
weather_type_10 Fuerte llovizna helada / lluvia helada
|
||||
weather_type_11 Llovizna helada ligera / lluvia helada
|
||||
weather_type_12 Niebla helada
|
||||
weather_type_13 Lluvia helada intensa
|
||||
weather_type_14 Lluvia helada ligera
|
||||
weather_type_15 Nube de embudo / Tornado
|
||||
weather_type_16 Lluvias de granizo
|
||||
weather_type_17 Hielo
|
||||
weather_type_18 Rayo sin trueno
|
||||
weather_type_19 Niebla
|
||||
weather_type_20 Precipitación en las proximidades
|
||||
weather_type_21 Lluvia
|
||||
weather_type_22 Fuertes lluvias y nieve
|
||||
weather_type_23 Lluvia ligera y nieve
|
||||
weather_type_24 Lluvias
|
||||
weather_type_25 Lluvia Pesada
|
||||
weather_type_26 Lluvia ligera
|
||||
weather_type_27 Disminución de la cobertura del cielo
|
||||
weather_type_28 Aumento de la cobertura del cielo
|
||||
weather_type_29 Cielo sin cambios
|
||||
weather_type_30 Humo o neblina
|
||||
weather_type_31 Nieve
|
||||
weather_type_32 Lluvias y nieve
|
||||
weather_type_33 Duchas de nieve
|
||||
weather_type_34 Fuertes nevadas
|
||||
weather_type_35 Nieve ligera
|
||||
weather_type_36 Chubascos
|
||||
weather_type_37 Tormenta
|
||||
weather_type_38 Tormenta sin precipitaciones
|
||||
weather_type_39 Polvo de diamante
|
||||
weather_type_40 Granizo
|
||||
weather_type_41 Nublado
|
||||
weather_type_42 Parcialmente nublado
|
||||
weather_type_43 Claro
|
||||
@@ -0,0 +1,177 @@
|
||||
locale = fr_CH
|
||||
page_og_desc = Gardez le contact avec François lorsqu'il part sur les chemins
|
||||
error_commit_db = Error lors de la requête SQL
|
||||
unknown_field = Champ "$0" inconnu
|
||||
|
||||
nav_back = Retour
|
||||
|
||||
admin = Administration
|
||||
admin_config = Paramètres
|
||||
admin_upload = Uploader
|
||||
save = Sauvegarder
|
||||
admin_save_success = Sauvegardé
|
||||
|
||||
track_main = Trajet principal
|
||||
track_off-track = Variante
|
||||
track_hitchhiking = Hors rando
|
||||
track_download = Télécharger la trace GPX
|
||||
|
||||
upload_title = Uploader photos & vidéos
|
||||
upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
|
||||
upload_success = $0 a été uploadé
|
||||
upload_media_exist = l'image $0 existe déjà
|
||||
|
||||
post_message = Message
|
||||
post_name = Nom
|
||||
post_new_message = Nouveau message
|
||||
and = et
|
||||
counter = N°$0
|
||||
send = Envoyer
|
||||
|
||||
maps = Fonds de carte
|
||||
map_satellite = Satellite
|
||||
map_otm = Open Topo Map
|
||||
map_ign_france = IGN (France)
|
||||
map_ign_spain = IGN (Espagne)
|
||||
map_linz = LINZ
|
||||
map_usgs = USGS
|
||||
map_natgeo = National Geographic
|
||||
map_outdoors = Mapbox Topo
|
||||
|
||||
image = Photo
|
||||
images = Photos
|
||||
image_taken = prise le $0
|
||||
video = Vidéo
|
||||
video_taken = filmée le $0
|
||||
add_on = ajoutée le $0
|
||||
click_watch = Click pour voir la vidéo
|
||||
click_zoom = Click pour zoomer
|
||||
media_count = Média $0 sur $1
|
||||
media_no_id = ID du média manquant
|
||||
media_comment_update= Commentaire du media "$0" mis-à-jour
|
||||
see_on_google = Voir la position sur Google Maps
|
||||
copy_to_clipboard = Copie le lien dans le presse-papier
|
||||
link_copied = Lien copié !
|
||||
|
||||
city_time = heure de $0
|
||||
local_time = $0 heure locale
|
||||
your_time = $0 dans votre fuseau horaire
|
||||
date_time = $0 à $1
|
||||
time_zone = Fuseau horaire
|
||||
|
||||
id_project = ID projet
|
||||
project = Projet
|
||||
projects = Projets
|
||||
new_project = Nouveau projet
|
||||
update_project = Mettre à jour le projet
|
||||
hikes = Randonnées
|
||||
mode = Mode
|
||||
mode_previz = Projet en cours de préparation
|
||||
mode_blog = Projet actif
|
||||
mode_histo = Projet archivé
|
||||
code_name = Nom de code
|
||||
start = Départ
|
||||
end = Arrivée
|
||||
feeds = Feeds
|
||||
id_feed = ID Feed
|
||||
ref_feed_id = ID Feed ref.
|
||||
id_spot = ID Spot
|
||||
name = Description
|
||||
status = Statut
|
||||
last_update = Dernière maj depuis Spot
|
||||
ref_spot_id = ID Spot ref.
|
||||
model = Modèle
|
||||
delete = Supprimer
|
||||
id_user = ID Utilisateur
|
||||
user_name = Nom
|
||||
active_users = Utilisateurs actifs
|
||||
language = Langue
|
||||
clearance = Niveau d'autorisation
|
||||
toolbox = Boite à outils
|
||||
|
||||
unit_day = jour
|
||||
unit_days = jours
|
||||
unit_hour = h
|
||||
|
||||
newsletter = Rester en contact
|
||||
nl_email_placeholder= mon@email.com
|
||||
nl_invalid_email = Ceci ne ressemble pas à une adresse email
|
||||
nl_subscribed_desc = C'est tout bon. On t'envoie des nouvelles fraiches dès qu'on les reçoit. Parole de scout.
|
||||
nl_unsubscribed_desc= Ajoute ton adresse email et on t'enverra la nouvelle position de François dès qu'on la reçoit :)
|
||||
nl_email_exists = Cette adresse email est déjà enregistrée. Vous pouvez vous désinscrire en cliquant sur le bouton ci-dessus.
|
||||
nl_subscribe = S'abonner
|
||||
nl_subscribed = Merci ! Tu vas recevoir un email de confirmation très bientôt
|
||||
nl_unsubscribe = Se désinscrire
|
||||
nl_unsubscribed = C'est fait. Fini le spam!
|
||||
nl_unknown_email = Adresse email inconnue
|
||||
|
||||
email_unsubscribe = PS: Trop d'emails ?
|
||||
email_unsub_btn = Se désinscrire
|
||||
|
||||
email_conf_subject = Confirmation
|
||||
conf_preheader = Merci de rester en contact !
|
||||
conf_thanks_sub = C'est tout bon !
|
||||
conf_body_para_1 = C'est gentil de venir voir où j'en suis. Promis, je vous tiendrais au courant de mon avancée.
|
||||
conf_body_para_2 = En général, j'envoie un message une fois par jour. Lorsque je passe à des endroits sympas, j'en envoie un supplémentaire (ascension de sommets, ce genre de choses). J'utilise une balise GPS pour envoyer le signal, je n'ai donc pas besoin de réseau téléphonique pour que cela fonctionne. Cependant, il peut m'arriver d'appuyer sur le bouton. Donc pas de raison de s'inquiéter si vous ne recevez pas de messages pendant une journée ou deux.
|
||||
conf_body_para_3 = Si j'ai ajouté des photos sur le site récemment, vous devriez aussi les retrouver dans cet email.
|
||||
conf_body_conclusion= A bientôt sur les chemins !
|
||||
conf_signature = --François
|
||||
|
||||
email_update_subject= Nouvelle position reçue
|
||||
update_preheader = Nouvelle position !
|
||||
update_title = Message
|
||||
update_latest_news = Dernières nouvelles :
|
||||
|
||||
distance = Distance
|
||||
elevation = Dénivelé
|
||||
segment_length = Taille du segment
|
||||
type = Type de rando
|
||||
legend = Légende
|
||||
|
||||
credits_project = Projet Spotty
|
||||
credits_git = Dépôt Git
|
||||
credits_license = sous licence GPLv3
|
||||
|
||||
weather_type_1 = Poudrerie ou neige à la dérive
|
||||
weather_type_2 = Bruine
|
||||
weather_type_3 = Bruine lourde
|
||||
weather_type_4 = Bruine légère
|
||||
weather_type_5 = Forte bruine / pluie
|
||||
weather_type_6 = Légère bruine / pluie
|
||||
weather_type_7 = Tempête de poussière
|
||||
weather_type_8 = Brouillard
|
||||
weather_type_9 = Bruine verglaçante / Pluie verglaçante
|
||||
weather_type_10 = Forte bruine verglaçante / pluie verglaçante
|
||||
weather_type_11 = Légère bruine verglaçante / pluie verglaçante
|
||||
weather_type_12 = Brouillard verglaçant
|
||||
weather_type_13 = Forte pluie verglaçante
|
||||
weather_type_14 = Légère pluie verglaçante
|
||||
weather_type_15 = Nuage d'entonnoir / Tornade
|
||||
weather_type_16 = Douches de grêle
|
||||
weather_type_17 = La glace
|
||||
weather_type_18 = Foudre sans tonnerre
|
||||
weather_type_19 = Brouillard
|
||||
weather_type_20 = Précipitations à proximité
|
||||
weather_type_21 = Pluie
|
||||
weather_type_22 = Forte pluie et neige
|
||||
weather_type_23 = Légère pluie et neige
|
||||
weather_type_24 = Averses de pluie
|
||||
weather_type_25 = Forte pluie
|
||||
weather_type_26 = Pluie légère
|
||||
weather_type_27 = Couverture du ciel en baisse
|
||||
weather_type_28 = Augmentation de la couverture du ciel
|
||||
weather_type_29 = Ciel inchangé
|
||||
weather_type_30 = Fumée ou brume
|
||||
weather_type_31 = Neige
|
||||
weather_type_32 = Averses de neige et de pluie
|
||||
weather_type_33 = Douches de neige
|
||||
weather_type_34 = Beaucoup de neige
|
||||
weather_type_35 = Neige légère
|
||||
weather_type_36 = Grains
|
||||
weather_type_37 = Orage
|
||||
weather_type_38 = Orage sans précipitations
|
||||
weather_type_39 = La poussière de diamant
|
||||
weather_type_40 = Saluer
|
||||
weather_type_41 = Couvert
|
||||
weather_type_42 = Partiellement nuageux
|
||||
weather_type_43 = Clair
|
||||
@@ -1,172 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\ToolBox;
|
||||
|
||||
//TODO Keep only local specificities and move bulk to Franzz\Objects\Controller
|
||||
class Controller extends PhpObject {
|
||||
private const MUTATING_ACTIONS = [
|
||||
'add_post',
|
||||
'subscribe',
|
||||
'unsubscribe',
|
||||
'login',
|
||||
'logout',
|
||||
'update_project',
|
||||
'upload',
|
||||
'add_comment',
|
||||
'add_position',
|
||||
'admin_set',
|
||||
'admin_create',
|
||||
'admin_delete'
|
||||
];
|
||||
private const SESSION_WRITING_ACTIONS = [
|
||||
'login',
|
||||
'logout'
|
||||
];
|
||||
|
||||
private Livetrail $oLivetrail;
|
||||
private array $asReq;
|
||||
private string $sCsrfToken = '';
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(__CLASS__);
|
||||
}
|
||||
|
||||
private function setReqVal(string $sKey, $oValue, string $sValidation=''): void {
|
||||
$this->asReq[$sKey] = $this->validateValue($sValidation, $oValue);
|
||||
}
|
||||
|
||||
public function handle($sProcessPage, array $argv = []): string {
|
||||
//Start buffering so warnings/notices can be collected
|
||||
ob_start();
|
||||
|
||||
//Parse variables
|
||||
$asReq = ToolBox::getRequest($argv);
|
||||
$this->asReq = [];
|
||||
$sAction = $asReq['a'] ?? '';
|
||||
$this->setReqVal('t', $asReq['t'] ?? '');
|
||||
$this->setReqVal('name', $asReq['name'] ?? '');
|
||||
$this->setReqVal('content', $asReq['content'] ?? '');
|
||||
$this->setReqVal('ref_type', $asReq['ref_type'] ?? '');
|
||||
$this->setReqVal('ref_id', $asReq['ref_id'] ?? 0, 'positiveInt');
|
||||
$this->setReqVal('id_project', $asReq['id_project'] ?? 0, 'positiveInt');
|
||||
$this->setReqVal('id', $asReq['id'] ?? 0);
|
||||
$this->setReqVal('id_entity', $asReq['id'] ?? 0, 'positiveInt');
|
||||
$this->setReqVal('field', $asReq['field'] ?? '');
|
||||
$this->setReqVal('value', $asReq['value'] ?? '');
|
||||
$this->setReqVal('type', $asReq['type'] ?? '');
|
||||
$this->setReqVal('email', $asReq['email'] ?? '');
|
||||
$this->setReqVal('password', $asReq['password'] ?? '');
|
||||
$this->setReqVal('latitude', $asReq['latitude'] ?? '');
|
||||
$this->setReqVal('longitude', $asReq['longitude'] ?? '');
|
||||
$this->setReqVal('timestamp', $asReq['timestamp'] ?? 0, 'positiveInt');
|
||||
$this->setReqVal('csrf_token', $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? ''));
|
||||
|
||||
//Authentication and CSRF protection share the same server-side session.
|
||||
$this->initCsrfToken();
|
||||
|
||||
//Create Livetrail instance
|
||||
$this->oLivetrail = new Livetrail($sProcessPage, $this->asReq['t']);
|
||||
|
||||
//Validate CSRF, then release the session lock before long-running work.
|
||||
$bValidMutationRequest = $this->validateMutationRequest($sAction);
|
||||
if(!$bValidMutationRequest || !in_array($sAction, self::SESSION_WRITING_ACTIONS, true)) $this->closeSession();
|
||||
|
||||
if(!$bValidMutationRequest) $sResult = Livetrail::getJsonResult(false, Livetrail::UNAUTHORIZED);
|
||||
else {
|
||||
$this->oLivetrail->setProjectId($this->asReq['id_project']);
|
||||
$sResult = ($sAction == '')?$this->oLivetrail->getAppMainPage($this->getCsrfToken()):$this->dispatch($sAction);
|
||||
}
|
||||
|
||||
//Clean errors
|
||||
$sDebug = ob_get_clean();
|
||||
if($sDebug != '') $this->oLivetrail->addUncaughtError($sDebug);
|
||||
$this->closeSession();
|
||||
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
private function validateMutationRequest(string $sAction): bool {
|
||||
return
|
||||
PHP_SAPI === 'cli'
|
||||
||
|
||||
!in_array($sAction, self::MUTATING_ACTIONS, true)
|
||||
||
|
||||
($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && $this->checkCsrfToken($this->asReq['csrf_token'])
|
||||
;
|
||||
}
|
||||
|
||||
private function getCsrfToken(): string {
|
||||
if($this->sCsrfToken === '') $this->initCsrfToken();
|
||||
return $this->sCsrfToken;
|
||||
}
|
||||
|
||||
private function setCsrfToken(): void {
|
||||
if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
$this->sCsrfToken = $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
private function initCsrfToken(): void {
|
||||
if(PHP_SAPI === 'cli') return;
|
||||
|
||||
if(session_status() !== PHP_SESSION_ACTIVE) {
|
||||
$bSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
|
||||
session_set_cookie_params(['httponly' => true, 'secure' => $bSecure, 'samesite' => 'Lax']);
|
||||
session_start();
|
||||
}
|
||||
|
||||
$this->setCsrfToken();
|
||||
}
|
||||
|
||||
private function checkCsrfToken(string $sClientToken): bool {
|
||||
$sServerToken = $this->getCsrfToken();
|
||||
return PHP_SAPI === 'cli' || ($sServerToken !== '' && is_string($sClientToken) && hash_equals($sServerToken, $sClientToken));
|
||||
}
|
||||
|
||||
private function closeSession(): void {
|
||||
if(session_status() === PHP_SESSION_ACTIVE) session_write_close();
|
||||
}
|
||||
|
||||
private function dispatch(string $sAction): string {
|
||||
return match($sAction) {
|
||||
'markers' => $this->oLivetrail->getMarkers(),
|
||||
'last_update' => $this->oLivetrail->getLastUpdate(),
|
||||
'next_feed' => $this->oLivetrail->getNextFeed($this->asReq['id']),
|
||||
'new_feed' => $this->oLivetrail->getNewFeed($this->asReq['id']),
|
||||
'add_post' => $this->oLivetrail->addPost($this->asReq['name'], $this->asReq['content'], $this->asReq['ref_type'], $this->asReq['ref_id']),
|
||||
'subscribe' => $this->oLivetrail->subscribe(),
|
||||
'unsubscribe' => $this->oLivetrail->unsubscribe(),
|
||||
'login' => $this->oLivetrail->login($this->asReq['email'], $this->asReq['password'], $this->asReq['name']),
|
||||
'logout' => $this->oLivetrail->logout(),
|
||||
'update_project' => $this->oLivetrail->updateProject(),
|
||||
default => $this->dispatchAdmin($sAction)
|
||||
};
|
||||
}
|
||||
|
||||
private function dispatchAdmin(string $sAction): string {
|
||||
if(!$this->oLivetrail->checkUserClearance(User::CLEARANCE_ADMIN)) {
|
||||
return Livetrail::getJsonResult(false, Livetrail::NOT_FOUND);
|
||||
}
|
||||
|
||||
return match($sAction) {
|
||||
'upload' => $this->oLivetrail->upload(),
|
||||
'add_comment' => $this->oLivetrail->addComment($this->asReq['id_entity'], $this->asReq['content']),
|
||||
'add_position' => $this->oLivetrail->addPosition($this->asReq['latitude'], $this->asReq['longitude'], $this->asReq['timestamp']),
|
||||
'admin_get' => $this->oLivetrail->getAdminSettings(),
|
||||
'admin_set' => $this->oLivetrail->setAdminSettings($this->asReq['type'], $this->asReq['id_entity'], $this->asReq['field'], $this->asReq['value']),
|
||||
'admin_create' => $this->oLivetrail->createAdminSettings($this->asReq['type']),
|
||||
'admin_delete' => $this->oLivetrail->deleteAdminSettings($this->asReq['type'], $this->asReq['id_entity']),
|
||||
'sql' => $this->oLivetrail->getDbBuildScript(),
|
||||
default => Livetrail::getJsonResult(false, Livetrail::NOT_FOUND)
|
||||
};
|
||||
}
|
||||
|
||||
private static function validateValue(string $sValidation, $oValue=0) {
|
||||
return match($sValidation) {
|
||||
'' => $oValue,
|
||||
'positiveInt' => filter_var($oValue, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 0]])
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
use Franzz\Objects\PhpObject;
|
||||
|
||||
/**
|
||||
* GPX to GeoJSON Converter
|
||||
*
|
||||
* To convert a gpx file:
|
||||
* 1. Add file <file_name>.gpx to geo/ folder
|
||||
* 2. Assign file to project: UPDATE projects SET codename = '<file_name>' WHERE id_project = <id_project>;
|
||||
* 3. Load any page
|
||||
*/
|
||||
class Converter extends PhpObject {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(__CLASS__);
|
||||
}
|
||||
|
||||
public static function convertToGeoJson(string $sCodeName) {
|
||||
$oGpx = new Gpx($sCodeName);
|
||||
$oGeoJson = new GeoJson($sCodeName);
|
||||
|
||||
$oGeoJson->buildTracks($oGpx->getTracks());
|
||||
if($oGeoJson->isSimplicationRequired()) $oGeoJson->buildTracks($oGpx->getTracks(), true);
|
||||
$oGeoJson->sortOffTracks();
|
||||
$oGeoJson->saveFile();
|
||||
|
||||
return [
|
||||
'logs' => $oGpx->getLog().'<br />'.$oGeoJson->getLog(),
|
||||
'center' => $oGeoJson->getCenter()
|
||||
];
|
||||
}
|
||||
|
||||
public static function hasGpxFile(string $sCodeName) {
|
||||
return ($sCodeName != '' && file_exists(Gpx::getBackendFilePath($sCodeName)));
|
||||
}
|
||||
|
||||
public static function isGeoJsonValid(string $sCodeName) {
|
||||
$sGpxFilePath = Gpx::getBackendFilePath($sCodeName);
|
||||
$sGeoJsonFilePath = GeoJson::getBackendFilePath($sCodeName);
|
||||
|
||||
return
|
||||
!self::hasGpxFile($sCodeName) //No GPX, no need for geoJSON
|
||||
||
|
||||
file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath); //geoJSON needs to be (re)generated
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use \Settings;
|
||||
|
||||
abstract class Geo extends PhpObject {
|
||||
protected const EXT = '';
|
||||
|
||||
protected const GEO_FOLDER = 'geo';
|
||||
protected const OPT_SIMPLE = 'simplification';
|
||||
|
||||
protected array $asTracks;
|
||||
protected string $sFilePath;
|
||||
|
||||
public function __construct(string $sCodeName) {
|
||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||
$this->sFilePath = self::getBackEndFilePath($sCodeName);
|
||||
$this->asTracks = [];
|
||||
}
|
||||
|
||||
//Access from backend
|
||||
public static function getBackendFilePath(string $sCodeName) {
|
||||
return '../resources/'.self::GEO_FOLDER.'/'.$sCodeName.static::EXT;
|
||||
}
|
||||
|
||||
//Access from frontend (/public/geo is a symlink of /resources/geo)
|
||||
public static function getFrontendFilePath(string $sCodeName) {
|
||||
return self::GEO_FOLDER.'/'.$sCodeName.static::EXT;
|
||||
}
|
||||
|
||||
public function getLog() {
|
||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
use Franzz\Objects\ToolBox;
|
||||
|
||||
class Gpx extends Geo {
|
||||
|
||||
public const EXT = '.gpx';
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
$this->parseFile();
|
||||
}
|
||||
|
||||
public function getTracks() {
|
||||
return $this->asTracks;
|
||||
}
|
||||
|
||||
private function parseFile() {
|
||||
$this->addNotice('Parsing: '.$this->sFilePath);
|
||||
if(!file_exists($this->sFilePath)) $this->addError($this->sFilePath.' file missing');
|
||||
else {
|
||||
$oXml = simplexml_load_file($this->sFilePath);
|
||||
|
||||
//Tracks
|
||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||
foreach($oXml->trk as $aoTrack) {
|
||||
$asTrack = [
|
||||
'name' => (string) $aoTrack->name,
|
||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||
'points'=> []
|
||||
];
|
||||
|
||||
foreach($aoTrack->trkseg as $asSegment) {
|
||||
foreach($asSegment as $asPoint) {
|
||||
$asTrack['points'][] = [
|
||||
'lon' => (float) $asPoint['lon'],
|
||||
'lat' => (float) $asPoint['lat'],
|
||||
'ele' => (int) $asPoint->ele
|
||||
];
|
||||
}
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
}
|
||||
|
||||
//Waypoints
|
||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,952 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
use Franzz\Objects\Db;
|
||||
use Franzz\Objects\Main;
|
||||
use Franzz\Objects\Translator;
|
||||
use Franzz\Objects\ToolBox;
|
||||
use \Settings;
|
||||
|
||||
/* Timezones
|
||||
* ---------
|
||||
* Site Time: Timestamp converted to the Timezone from which the user is viewing the Site (default PHP/SQL Timezone)
|
||||
* Local Time: Timestamp converted to the Timezone from which the content (media/post/message) has been sent (Local Timezone stored in timezone field)
|
||||
*
|
||||
* - Feeds (table `feeds`):
|
||||
* - last_update: timestamp in Site Time
|
||||
* - Spot Messages (table `messages`):
|
||||
* - unix_time: UNIX (int) in UTC
|
||||
* - site_time: timestamp in Site Time
|
||||
* - iso_time: raw ISO 8601 in UTC or Local Time (spot messages are unreliable, timezone is then calculated from GPS coordinates)
|
||||
* - posted_on: timestamp in Site Time
|
||||
* - timezone: Local Timezone
|
||||
* - Medias (table `medias`):
|
||||
* - posted_on: timestamp in Site Time
|
||||
* - taken_on: timestamp in Site Time
|
||||
* - timezone: Local Timezone
|
||||
* - Posts (table `posts`):
|
||||
* - site_time: timestamp in Site Time
|
||||
* - timezone: Local Timezone
|
||||
* - Users (table `users`):
|
||||
* - timezone: Site Timezone (stored user's timezone for emails)
|
||||
*/
|
||||
|
||||
class Livetrail extends Main {
|
||||
//Database
|
||||
public const POST_TABLE = 'posts';
|
||||
private const REF_TYPES = ['post', 'media', 'message'];
|
||||
|
||||
private const FEED_CHUNK_SIZE = 15;
|
||||
private const MAIL_CHUNK_SIZE = 5;
|
||||
|
||||
public const DEFAULT_LANG = 'en';
|
||||
public const PROJECT_NAME = 'LiveTrail';
|
||||
|
||||
private const MAIN_PAGE = 'index';
|
||||
private const VITE_APP = 'src/app.js';
|
||||
|
||||
private Project $oProject;
|
||||
private Media $oMedia;
|
||||
private User $oUser;
|
||||
private Map $oMap;
|
||||
|
||||
public function __construct($sProcessPage, $sTimezone) {
|
||||
parent::__construct($sProcessPage, true, $sTimezone);
|
||||
|
||||
$this->oUser = new User($this->oDb);
|
||||
|
||||
$this->oLang = new Translator('', self::DEFAULT_LANG);
|
||||
|
||||
$this->oProject = new Project($this->oDb);
|
||||
$this->oMedia = new Media($this->oDb, $this->oProject);
|
||||
|
||||
$this->oMap = new Map($this->oDb);
|
||||
}
|
||||
|
||||
protected function install() {
|
||||
//Install DB
|
||||
$this->oDb->install();
|
||||
|
||||
//Add first user
|
||||
$iUserId = $this->oDb->insertRow(User::USER_TABLE, [
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@admin.com',
|
||||
'language' => self::DEFAULT_LANG,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'subscribed'=> User::USER_SUBSCRIBED,
|
||||
'clearance' => User::CLEARANCE_ADMIN
|
||||
]);
|
||||
$this->oUser->setUserId($iUserId);
|
||||
}
|
||||
|
||||
protected function getSqlOptions() {
|
||||
return
|
||||
[
|
||||
'tables' =>
|
||||
[
|
||||
Feed::MSG_TABLE => ['ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'timezone', 'unix_time', 'content', 'battery_state', 'posted_on', 'weather_icon', 'weather_cond', 'weather_temp', 'display'],
|
||||
Feed::FEED_TABLE => ['ref_feed_id', Db::getId(Feed::SPOT_TABLE), Db::getId(Project::PROJ_TABLE), 'name', 'description', 'status', 'last_update'],
|
||||
Feed::SPOT_TABLE => ['ref_spot_id', 'name', 'model'],
|
||||
Project::PROJ_TABLE => ['name', 'codename', 'active_from', 'active_to'],
|
||||
self::POST_TABLE => [Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time', 'timezone', 'ref_id', 'ref_type'],
|
||||
Media::MEDIA_TABLE => [Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'comment'],
|
||||
User::USER_TABLE => ['name', 'email', 'password', 'token', 'token_exp', 'gravatar', 'language', 'timezone', 'subscribed', 'clearance'],
|
||||
Map::MAP_TABLE => ['codename', 'pattern', 'token', 'tile_size', 'min_zoom', 'max_zoom', 'attribution'],
|
||||
Map::MAPPING_TABLE => [Db::getId(Map::MAP_TABLE) , Db::getId(Project::PROJ_TABLE)]
|
||||
],
|
||||
'types' =>
|
||||
[
|
||||
'clearance' => 'TINYINT(1) DEFAULT '.User::CLEARANCE_USER,
|
||||
'active_from' => 'TIMESTAMP DEFAULT 0',
|
||||
'active_to' => 'TIMESTAMP DEFAULT 0',
|
||||
'battery_state' => 'VARCHAR(10)',
|
||||
'codename' => 'VARCHAR(100)',
|
||||
'content' => 'LONGTEXT',
|
||||
'comment' => 'LONGTEXT',
|
||||
'description' => 'VARCHAR(100)',
|
||||
'email' => 'VARCHAR(320) NOT NULL',
|
||||
'filename' => 'VARCHAR(100) NOT NULL',
|
||||
'iso_time' => 'VARCHAR(24)',
|
||||
'language' => 'VARCHAR(2)',
|
||||
'last_update' => 'TIMESTAMP DEFAULT 0',
|
||||
'latitude' => 'DECIMAL(8,6)',
|
||||
'longitude' => 'DECIMAL(9,6)',
|
||||
'altitude' => 'SMALLINT',
|
||||
'model' => 'VARCHAR(20)',
|
||||
'name' => 'VARCHAR(100)',
|
||||
'pattern' => 'VARCHAR(200) NOT NULL',
|
||||
'password' => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
'posted_on' => 'TIMESTAMP DEFAULT 0',
|
||||
'ref_feed_id' => 'VARCHAR(40)',
|
||||
'ref_msg_id' => 'VARCHAR(15)',
|
||||
'ref_spot_id' => 'VARCHAR(10)',
|
||||
'ref_id' => 'INT UNSIGNED',
|
||||
'ref_type' => 'VARCHAR(20)',
|
||||
'rotate' => 'SMALLINT',
|
||||
'site_time' => 'TIMESTAMP DEFAULT 0', //DEFAULT 0 removes auto-set to current time
|
||||
'status' => 'VARCHAR(10)',
|
||||
'subscribed' => 'BOOLEAN DEFAULT '.User::USER_UNSUBSCRIBED,
|
||||
'taken_on' => 'TIMESTAMP DEFAULT 0',
|
||||
'timezone' => 'CHAR(64) NOT NULL', //see mysql.time_zone_name
|
||||
'token' => 'VARCHAR(4096)',
|
||||
'token_exp' => 'TIMESTAMP DEFAULT 0',
|
||||
'type' => 'VARCHAR(20)',
|
||||
'unix_time' => 'INT',
|
||||
'min_zoom' => 'TINYINT UNSIGNED',
|
||||
'max_zoom' => 'TINYINT UNSIGNED',
|
||||
'attribution' => 'VARCHAR(100)',
|
||||
'gravatar' => 'LONGTEXT',
|
||||
'weather_icon' => 'VARCHAR(30)',
|
||||
'weather_cond' => 'VARCHAR(30)',
|
||||
'weather_temp' => 'DECIMAL(3,1)',
|
||||
'tile_size' => 'SMALLINT UNSIGNED DEFAULT 256',
|
||||
'width' => 'INT',
|
||||
'height' => 'INT',
|
||||
'display' => 'BOOLEAN DEFAULT '.Feed::MSG_DISPLAYED
|
||||
],
|
||||
'constraints' =>
|
||||
[
|
||||
Feed::MSG_TABLE => ['UNIQUE KEY `uni_ref_msg_id` (`ref_msg_id`)', 'INDEX(`ref_msg_id`)'],
|
||||
Feed::FEED_TABLE => ['UNIQUE KEY `uni_ref_feed_id` (`ref_feed_id`)', 'INDEX(`ref_feed_id`)'],
|
||||
Feed::SPOT_TABLE => ['UNIQUE KEY `uni_ref_spot_id` (`ref_spot_id`)', 'INDEX(`ref_spot_id`)'],
|
||||
Project::PROJ_TABLE => 'UNIQUE KEY `uni_proj_name` (`codename`)',
|
||||
Media::MEDIA_TABLE => 'UNIQUE KEY `uni_file_name` (`filename`)',
|
||||
User::USER_TABLE => 'UNIQUE KEY `uni_email` (`email`)',
|
||||
Map::MAP_TABLE => 'UNIQUE KEY `uni_map_name` (`codename`)',
|
||||
Map::MAPPING_TABLE => 'default_on_generic_map_only CHECK (`default_map` = 0 OR `id_project` IS NULL)'
|
||||
],
|
||||
'cascading_delete' =>
|
||||
[
|
||||
Feed::SPOT_TABLE => [Feed::FEED_TABLE],
|
||||
Feed::FEED_TABLE => [Feed::MSG_TABLE],
|
||||
Project::PROJ_TABLE => [Feed::FEED_TABLE, Media::MEDIA_TABLE, self::POST_TABLE, Map::MAPPING_TABLE],
|
||||
Map::MAP_TABLE => [Map::MAPPING_TABLE]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getAppMainPage(string $sCsrfToken='') {
|
||||
$asViteAssets = $this->getViteAssets();
|
||||
|
||||
return parent::getMainPage(
|
||||
[
|
||||
'projects' => $this->oProject->getProjects(),
|
||||
'user' => $this->oUser->getUserInfo(),
|
||||
'consts' => [
|
||||
'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' => self::PROJECT_NAME,
|
||||
'default_page' => 'project',
|
||||
'csrf_token' => $sCsrfToken
|
||||
]
|
||||
],
|
||||
self::MAIN_PAGE,
|
||||
[
|
||||
'tags' => [
|
||||
'language' => $this->oLang->getLanguage(),
|
||||
'title' => self::PROJECT_NAME,
|
||||
'app_entry' => $asViteAssets['app']
|
||||
],
|
||||
'instances' => [
|
||||
'css' => $asViteAssets['css'],
|
||||
'module' => $asViteAssets['module']
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getViteAssets() {
|
||||
$asManifest = json_decode(file_get_contents(__DIR__.'/../public/.vite/manifest.json'), true);
|
||||
$asAppImport = $asManifest[self::VITE_APP];
|
||||
|
||||
//Recursive search for chunk imports
|
||||
$asImports = [];
|
||||
$asSeenImports = [self::VITE_APP => true];
|
||||
$this->appendViteImportedChunks($asManifest, $asAppImport, $asSeenImports, $asImports);
|
||||
|
||||
//CSS
|
||||
$asCssFiles = [];
|
||||
foreach(array_merge([$asAppImport], $asImports) as $asChunk) {
|
||||
foreach($asChunk['css'] ?? [] as $sCssFile) $asCssFiles[] = $sCssFile;
|
||||
}
|
||||
|
||||
//Modules
|
||||
$asModuleFiles = [];
|
||||
foreach($asImports as $asImport) {
|
||||
if(str_ends_with($asImport['file'] ?? '', '.js')) $asModuleFiles[] = $asImport['file'];
|
||||
}
|
||||
|
||||
return [
|
||||
'app' => $asAppImport['file'],
|
||||
'css' => $this->getViteAssetInstances($asCssFiles),
|
||||
'module' => $this->getViteAssetInstances($asModuleFiles)
|
||||
];
|
||||
}
|
||||
|
||||
private function appendViteImportedChunks($asManifest, $asChunk, &$asSeenImports, &$asImports) {
|
||||
foreach($asChunk['imports'] ?? [] as $sImport) {
|
||||
if(isset($asSeenImports[$sImport]) || !isset($asManifest[$sImport])) continue;
|
||||
|
||||
$asSeenImports[$sImport] = true;
|
||||
$this->appendViteImportedChunks($asManifest, $asManifest[$sImport], $asSeenImports, $asImports);
|
||||
$asImports[] = $asManifest[$sImport];
|
||||
}
|
||||
}
|
||||
|
||||
private function getViteAssetInstances($asFilePaths) {
|
||||
return array_map(
|
||||
function($sFilePath) { return ['filename' => $sFilePath]; },
|
||||
$asFilePaths
|
||||
);
|
||||
}
|
||||
|
||||
public function checkUserClearance($iClearance) {
|
||||
return $this->oUser->checkUserClearance($iClearance);
|
||||
}
|
||||
|
||||
/* Managing projects */
|
||||
|
||||
public function setProjectId($iProjectId=0) {
|
||||
$this->oProject->setProjectId($iProjectId);
|
||||
}
|
||||
|
||||
public function updateProject() {
|
||||
$bNewMsg = false;
|
||||
$bSuccess = true;
|
||||
$sLangId = '';
|
||||
|
||||
//Update all feeds belonging to the project
|
||||
$asFeeds = $this->oProject->getFeedIds();
|
||||
foreach($asFeeds as $iFeedId) {
|
||||
$oFeed = new Feed($this->oDb, $iFeedId);
|
||||
$bNewMsg = $bNewMsg || $oFeed->checkUpdateFeed($this->oProject->getMode());
|
||||
}
|
||||
|
||||
//Send Update Email
|
||||
if($bNewMsg) {
|
||||
$bSuccess = $this->sendEmail();
|
||||
$sLangId = $bSuccess?'email.sent':'email.failure';
|
||||
}
|
||||
else $sLangId = 'spot.no_new_msg';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sLangId);
|
||||
}
|
||||
|
||||
private function sendEmail() {
|
||||
$oEmail = new Email($this->asContext['serv_name'], 'email.update');
|
||||
$oEmail->setDestInfo($this->oUser->getSubscribedUsersInfo());
|
||||
|
||||
//Add Position
|
||||
$asSpotMessages = $this->getSpotMessages([$this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE))]);
|
||||
$asLastMessage = array_shift($asSpotMessages);
|
||||
$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', [
|
||||
'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 getMarkers($asMessageIds=[], $asMediaIds=[], $bInternal=false) {
|
||||
//Get messages
|
||||
$asMessages = $this->getSpotMessages($asMessageIds);
|
||||
foreach($asMessages as &$asMessage) {
|
||||
$asMessage['id'] = $asMessage[Db::getId(Feed::MSG_TABLE)];
|
||||
$asMessage['type'] = 'message';
|
||||
$asMessage['subtype'] = 'message';
|
||||
}
|
||||
|
||||
//Get Geo-positioned Medias
|
||||
//FIXME Make more efficient than requesting images twice from DB
|
||||
$asMedias = $this->getMedias('taken_on', $asMediaIds);
|
||||
$asGeoMedias = $this->getMedias('posted_on', $asMediaIds, true);
|
||||
foreach($asGeoMedias as &$asGeoMedia) {
|
||||
$iId = $asGeoMedia[Db::getId(Media::MEDIA_TABLE)];
|
||||
unset($asGeoMedia[Db::getId(Media::MEDIA_TABLE)]);
|
||||
|
||||
$asGeoMedia['id'] = $iId;
|
||||
$asGeoMedia['type'] = 'media';
|
||||
$asGeoMedia['medias'] = array_values(array_filter($asMedias, function($asMedia) use ($iId) {
|
||||
return $asMedia['id_media'] == $iId;
|
||||
}));
|
||||
}
|
||||
|
||||
//Assign medias to closest message
|
||||
if(!empty($asMessages)) {
|
||||
usort($asMessages, function($a, $b) {return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
||||
usort($asMedias, function($a, $b) {return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
||||
|
||||
$iIndex = 0;
|
||||
$iMaxIndex = count($asMessages) - 1;
|
||||
foreach($asMedias as $asMedia) {
|
||||
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
|
||||
if($iIndex == 0) $iMsgIndex = $iIndex;
|
||||
elseif($iIndex > $iMaxIndex) $iMsgIndex = $iMaxIndex;
|
||||
else {
|
||||
$iHalfWayPoint = ($asMessages[$iIndex - 1]['unix_time'] + $asMessages[$iIndex]['unix_time'])/2;
|
||||
$iMsgIndex = ($asMedia['unix_time'] >= $iHalfWayPoint)?$iIndex:($iIndex - 1);
|
||||
}
|
||||
|
||||
$asMessages[$iMsgIndex]['medias'][] = $asMedia;
|
||||
}
|
||||
}
|
||||
|
||||
//Combine markers
|
||||
$asMarkers = [...$asMessages, ...$asGeoMedias];
|
||||
usort($asMarkers, function($a, $b) {return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
||||
|
||||
$asResult = [
|
||||
'markers' => $asMarkers,
|
||||
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId())
|
||||
];
|
||||
|
||||
return $bInternal?$asResult:self::getJsonResult(true, '', $asResult);
|
||||
}
|
||||
|
||||
public function getLastUpdate() {
|
||||
$asLastUpdate = [];
|
||||
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
|
||||
return self::getJsonResult(true, '', $asLastUpdate);
|
||||
}
|
||||
|
||||
public function login($sEmail, $sPassword, $sNickName) {
|
||||
$asResult = $this->oUser->login($sEmail, $sPassword, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
|
||||
|
||||
if($asResult['result'] && $asResult['data']['subscribe']) return $this->subscribe();
|
||||
else return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $this->oUser->getUserInfo(), $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
$asResult = $this->oUser->logout();
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], User::DEFAULT_USER, $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
public function subscribe() {
|
||||
$asResult = $this->oUser->setSubscription(true);
|
||||
$asUserInfo = $this->oUser->getUserInfo();
|
||||
if($asResult['result'] && !Settings::DEBUG) {
|
||||
$oConfEmail = new Email($this->asContext['serv_name'], 'email.confirmation');
|
||||
$oConfEmail->setDestInfo($asUserInfo);
|
||||
$oConfEmail->send();
|
||||
}
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asUserInfo, $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
public function unsubscribe() {
|
||||
$asResult = $this->oUser->setSubscription(false);
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $this->oUser->getUserInfo(), $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
private function getSpotMessages($asMsgIds=[]) {
|
||||
$asConstraints = $this->getFeedConstraints(Feed::MSG_TABLE);
|
||||
if(!empty($asMsgIds)) {
|
||||
$asConstraints['constraint'][Db::getId(Feed::MSG_TABLE)] = $asMsgIds;
|
||||
$asConstraints['constOpe'][Db::getId(Feed::MSG_TABLE)] = 'IN';
|
||||
}
|
||||
|
||||
$asCombinedMessages = [];
|
||||
|
||||
//Get messages from all feeds belonging to the project
|
||||
$asFeeds = $this->oProject->getFeedIds();
|
||||
foreach($asFeeds as $iFeedId) {
|
||||
$oFeed = new Feed($this->oDb, $iFeedId);
|
||||
$asMessages = $oFeed->getMessages($asConstraints);
|
||||
foreach($asMessages as $asMessage) {
|
||||
$asMessage['latitude'] = floatval($asMessage['latitude']);
|
||||
$asMessage['longitude'] = floatval($asMessage['longitude']);
|
||||
$asMessage['lat_dms'] = self::decToDms($asMessage['latitude'], 'lat');
|
||||
$asMessage['lon_dms'] = self::decToDms($asMessage['longitude'], 'lon');
|
||||
$asMessage['displayed_id'] = $asMessage[Db::getId(Feed::MSG_TABLE)];
|
||||
$asMessage['static_img_url'] = $this->oMap->getMapUrl('static', ['x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']]);
|
||||
$asMessage['marker_img_url'] = $this->oMap->getMapUrl('static_marker', ['x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']]);
|
||||
|
||||
$this->addTimeStamp($asMessage, $asMessage['unix_time'], $asMessage['timezone']);
|
||||
$asCombinedMessages[] = $asMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return $asCombinedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid medias based on $sTimeRefField:
|
||||
* - taken_on: Date/time on which the media was taken
|
||||
* - posted_on: Date/time on which the media was uploaded
|
||||
* @param String $sTimeRefField Field to calculate relative times: 'taken_on' or 'posted_on'
|
||||
* @return Array Medias info
|
||||
*/
|
||||
private function getMedias($sTimeRefField, $asMediaIds=[], $bOnlyGeoMedia=false) {
|
||||
//Constraints
|
||||
$asConstraints = $this->getFeedConstraints(Media::MEDIA_TABLE, $sTimeRefField);
|
||||
if(!empty($asMediaIds)) {
|
||||
$asConstraints['constOpe'][Db::getId(Media::MEDIA_TABLE)] = 'IN';
|
||||
$asConstraints['constraint'][Db::getId(Media::MEDIA_TABLE)] = $asMediaIds;
|
||||
}
|
||||
if($bOnlyGeoMedia) {
|
||||
$asConstraints['constOpe']['latitude'] = ' IS NOT ';
|
||||
$asConstraints['constraint']['latitude'] = 'NULL';
|
||||
$asConstraints['constOpe']['longitude'] = ' IS NOT ';
|
||||
$asConstraints['constraint']['longitude'] = 'NULL';
|
||||
}
|
||||
|
||||
$asMedias = $this->oMedia->getMediasInfo($asConstraints);
|
||||
foreach($asMedias as &$asMedia) {
|
||||
$asMedia['displayed_id'] = $asMedia[Db::getId(Media::MEDIA_TABLE)];
|
||||
|
||||
$this->addTimeStamp($asMedia, strtotime($asMedia[$sTimeRefField]), $asMedia['timezone']);
|
||||
$this->addTimeStamp($asMedia, strtotime($asMedia['taken_on']), $asMedia['timezone'], 'taken_on');
|
||||
$this->addTimeStamp($asMedia, strtotime($asMedia['posted_on']), $asMedia['timezone'], 'posted_on');
|
||||
|
||||
if($asMedia['latitude'] != '' && $asMedia['longitude'] != '') {
|
||||
$asMedia['lat_dms'] = self::decToDms($asMedia['latitude'], 'lat');
|
||||
$asMedia['lon_dms'] = self::decToDms($asMedia['longitude'], 'lon');
|
||||
}
|
||||
|
||||
unset($asMedia['taken_on']);
|
||||
unset($asMedia['posted_on']);
|
||||
}
|
||||
|
||||
return $asMedias;
|
||||
}
|
||||
|
||||
private function getPosts($asPostIds=[], $bResolveRef=true) {
|
||||
$asInfo = [
|
||||
'select' => [Db::getFullColumnName(self::POST_TABLE, '*'), 'gravatar'],
|
||||
'from' => self::POST_TABLE,
|
||||
'join' => [User::USER_TABLE => Db::getId(User::USER_TABLE)]
|
||||
];
|
||||
$asInfo = array_merge($asInfo, $this->getFeedConstraints(self::POST_TABLE));
|
||||
|
||||
if(!empty($asPostIds)) {
|
||||
$asInfo['constraint'][Db::getId(self::POST_TABLE)] = $asPostIds;
|
||||
$asInfo['constOpe'][Db::getId(self::POST_TABLE)] = 'IN';
|
||||
}
|
||||
$asPosts = $this->oDb->selectRows($asInfo);
|
||||
|
||||
foreach($asPosts as &$asPost) {
|
||||
$iUnixTimeStamp = strtotime($asPost['site_time']); //assumes site timezone
|
||||
$asPost['formatted_name'] = Toolbox::mb_ucwords($asPost['name']);
|
||||
unset($asPost[Db::getId(User::USER_TABLE)]);
|
||||
|
||||
$this->addTimeStamp($asPost, $iUnixTimeStamp, $asPost['timezone']);
|
||||
|
||||
if($bResolveRef && !empty($asPost['ref_type']) && !empty($asPost['ref_id'])) {
|
||||
switch($asPost['ref_type']) {
|
||||
case 'post': $asRefs = $this->getPosts([$asPost['ref_id']], false); break;
|
||||
case 'media': $asRefs = $this->getMedias('posted_on', [$asPost['ref_id']]); break;
|
||||
case 'message': $asRefs = $this->getSpotMessages([$asPost['ref_id']]); break;
|
||||
default: $asRefs = [];
|
||||
}
|
||||
$asPost['ref_item'] = reset($asRefs);
|
||||
$asPost['ref_item']['type'] = $asPost['ref_type'];
|
||||
$asPost['ref_item']['id'] = $asPost['ref_id'];
|
||||
}
|
||||
unset($asPost['ref_id']);
|
||||
unset($asPost['ref_type']);
|
||||
}
|
||||
|
||||
return $asPosts;
|
||||
}
|
||||
|
||||
private function addTimeStamp(&$asData, $iTime, $sTimeZone='', $sPrefix='') {
|
||||
if($sPrefix != '') $sPrefix = $sPrefix.'_';
|
||||
|
||||
$asData[$sPrefix.'unix_time'] = (int) $iTime;
|
||||
$asData[$sPrefix.'relative_time'] = Toolbox::getDateTimeDesc($iTime, $this->oLang->getLanguage());
|
||||
$asData[$sPrefix.'formatted_time'] = $this->getTimeFormat($iTime);
|
||||
|
||||
if($sTimeZone != '') {
|
||||
$asData[$sPrefix.'formatted_time_local'] = $this->getTimeFormat($iTime, $sTimeZone);
|
||||
$asData[$sPrefix.'day_offset'] = self::getTimeZoneDayOffset($iTime, $sTimeZone);
|
||||
}
|
||||
}
|
||||
|
||||
private function getFeedConstraints($sType, $sTimeField='site_time', $sReturnFormat='array') {
|
||||
$asConsArray = [];
|
||||
$sConsSql = '';
|
||||
$asActPeriod = $this->oProject->getActivePeriod();
|
||||
|
||||
//Filter on Project ID
|
||||
$sConsSql = 'WHERE '.Db::getId(Project::PROJ_TABLE).' = '.$this->oProject->getProjectId();
|
||||
$asConsArray = [
|
||||
'constraint'=> [Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()],
|
||||
'constOpe' => [Db::getId(Project::PROJ_TABLE) => '=']
|
||||
];
|
||||
|
||||
//Time Filter
|
||||
switch($sType) {
|
||||
case Feed::MSG_TABLE:
|
||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod;
|
||||
$asConsArray['constOpe'][$sTimeField] = 'BETWEEN';
|
||||
$asConsArray['constraint']['display'] = Feed::MSG_DISPLAYED;
|
||||
$asConsArray['constOpe']['display'] = '=';
|
||||
$sConsSql .= ' AND '.$sTimeField." BETWEEN '".$asActPeriod['from']."' AND '".$asActPeriod['to']."' AND display = ".Feed::MSG_DISPLAYED;
|
||||
break;
|
||||
case Media::MEDIA_TABLE:
|
||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
||||
$asConsArray['constOpe'][$sTimeField] = '<=';
|
||||
$sConsSql .= ' AND '.$sTimeField." <= '".$asActPeriod['to']."'";
|
||||
break;
|
||||
case self::POST_TABLE:
|
||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
||||
$asConsArray['constOpe'][$sTimeField] = '<=';
|
||||
$sConsSql .= ' AND '.$sTimeField." <= '".$asActPeriod['to']."'";
|
||||
break;
|
||||
}
|
||||
|
||||
return ($sReturnFormat=='array')?$asConsArray:$sConsSql;
|
||||
}
|
||||
|
||||
public function getNewFeed($iRefIdFirst) {
|
||||
$asResult = [];
|
||||
$sLangId = '';
|
||||
|
||||
if($this->oProject->isEditable()) {
|
||||
$asMessageIds = $asMediaIds = [];
|
||||
|
||||
//New Feed Items
|
||||
$asResult = $this->getFeed($iRefIdFirst, '>', 'DESC');
|
||||
foreach($asResult['feed'] as $asItem) {
|
||||
switch($asItem['type']) {
|
||||
case 'message':
|
||||
$asMessageIds[] = $asItem['id'];
|
||||
break;
|
||||
case 'media':
|
||||
$asMediaIds[] = $asItem['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//New Markers
|
||||
$asMarkers = $this->getMarkers(
|
||||
empty($asMessageIds)?[0]:$asMessageIds,
|
||||
empty($asMediaIds)?[0]:$asMediaIds,
|
||||
true
|
||||
);
|
||||
|
||||
$asResult = array_merge($asResult, $asMarkers);
|
||||
}
|
||||
else $sLangId = 'project.modes.histo';
|
||||
|
||||
return self::getJsonResult(true, $sLangId, $asResult);
|
||||
}
|
||||
|
||||
public function getNextFeed($iRefIdLast=0, $bInternal=false) {
|
||||
if($this->oProject->getMode() == Project::MODE_HISTO) {
|
||||
$sDirection = '>';
|
||||
$sSort = 'ASC';
|
||||
}
|
||||
else {
|
||||
$sDirection = '<';
|
||||
$sSort = 'DESC';
|
||||
}
|
||||
$asResult = $this->getFeed($iRefIdLast, $sDirection, $sSort);
|
||||
return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult);
|
||||
}
|
||||
|
||||
private function getFeed($iRefId, $sDirection, $sSort) {
|
||||
$sRefId = is_scalar($iRefId) && preg_match('/^\d+(?:\.\d+)?$/D', (string) $iRefId) ? (string) $iRefId : '0';
|
||||
$sDirection = ($sDirection === '>')?'>':'<';
|
||||
$sSort = ($sSort === 'ASC')?'ASC':'DESC';
|
||||
|
||||
$sProjectIdField = Db::getId(Project::PROJ_TABLE);
|
||||
$sMsgIdField = Db::getId(Feed::MSG_TABLE);
|
||||
$sMediaIdField = Db::getId(Media::MEDIA_TABLE);
|
||||
$sPostIdField = Db::getId(self::POST_TABLE);
|
||||
$sFeedIdField = Db::getId(Feed::FEED_TABLE);
|
||||
$sQuery = implode(' ', [
|
||||
'SELECT type, id, ref',
|
||||
'FROM (',
|
||||
"SELECT {$sProjectIdField}, {$sMsgIdField} AS id, 'message' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.0', {$sMsgIdField}) AS ref",
|
||||
'FROM '.Feed::MSG_TABLE,
|
||||
'INNER JOIN '.Feed::FEED_TABLE." USING({$sFeedIdField})",
|
||||
$this->getFeedConstraints(Feed::MSG_TABLE, 'site_time', 'sql'),
|
||||
'UNION',
|
||||
"SELECT {$sProjectIdField}, {$sMediaIdField} AS id, 'media' AS type, CONCAT(UNIX_TIMESTAMP(posted_on), '.1', {$sMediaIdField}) AS ref",
|
||||
'FROM '.Media::MEDIA_TABLE,
|
||||
$this->getFeedConstraints(Media::MEDIA_TABLE, 'posted_on', 'sql'),
|
||||
'UNION',
|
||||
"SELECT {$sProjectIdField}, {$sPostIdField} AS id, 'post' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.2', {$sPostIdField}) AS ref",
|
||||
'FROM '.self::POST_TABLE,
|
||||
$this->getFeedConstraints(self::POST_TABLE, 'site_time', 'sql'),
|
||||
') AS items',
|
||||
($sRefId !== '0')?('WHERE ref '.$sDirection.' '.$sRefId):'',
|
||||
'ORDER BY ref '.$sSort,
|
||||
'LIMIT '.self::FEED_CHUNK_SIZE
|
||||
]);
|
||||
|
||||
//Get new chunk
|
||||
$asItems = $this->oDb->getArrayQuery($sQuery, true);
|
||||
|
||||
//Update Reference Point with latest/earliest value
|
||||
$iRefIdFirst = $iRefIdLast = 0;
|
||||
if(!empty($asItems)) {
|
||||
$iRefIdLast = end($asItems)['ref'];
|
||||
$iRefIdFirst = reset($asItems)['ref'];
|
||||
}
|
||||
|
||||
//Sort Table IDs by type & Get attributes
|
||||
$asFeedIds = ['message'=>[], 'media'=>[], 'post'=>[]];
|
||||
foreach($asItems as $asItem) {
|
||||
$asFeedIds[$asItem['type']][$asItem['id']] = $asItem;
|
||||
}
|
||||
$asFeedAttrs = [
|
||||
'message' => empty($asFeedIds['message'])?[]:$this->getSpotMessages(array_keys($asFeedIds['message'])),
|
||||
'media' => empty($asFeedIds['media'])?[]:$this->getMedias('posted_on', array_keys($asFeedIds['media'])),
|
||||
'post' => empty($asFeedIds['post'])?[]:$this->getPosts(array_keys($asFeedIds['post']))
|
||||
];
|
||||
|
||||
//Replace Array Key with Item ID
|
||||
$asFeeds = [];
|
||||
foreach($asFeedAttrs as $sType=>$asFeedAttr) {
|
||||
foreach($asFeedAttr as $asFeed) {
|
||||
$asFeeds[$sType][$asFeed['id_'.$sType]] = $asFeed;
|
||||
}
|
||||
}
|
||||
|
||||
//Assign
|
||||
foreach($asItems as &$asItem) {
|
||||
$asItem = array_merge($asFeeds[$asItem['type']][$asItem['id']], $asItem);
|
||||
}
|
||||
|
||||
return ['ref_id_last'=>$iRefIdLast, 'ref_id_first'=>$iRefIdFirst, 'sort'=>$sSort, 'feed'=>$asItems];
|
||||
}
|
||||
|
||||
public function addPost($sName, $sPost, $sRefType='', $iRefId=0) {
|
||||
$iPostId = 0;
|
||||
$sLangId = '';
|
||||
|
||||
if($this->oProject->isEditable()) {
|
||||
$bHasRef = ($iRefId > 0) && in_array($sRefType, self::REF_TYPES, true);
|
||||
$asData = [
|
||||
Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(),
|
||||
'name' => mb_strtolower(trim($sName)),
|
||||
'content' => trim($sPost),
|
||||
'site_time' => date(Db::TIMESTAMP_FORMAT), //Now in Site Time
|
||||
'timezone' => date_default_timezone_get(), //Site Time Zone
|
||||
'ref_id' => $bHasRef?$iRefId:'NULL',
|
||||
'ref_type' => $bHasRef?$sRefType:'NULL'
|
||||
];
|
||||
if($this->oUser->getUserId() > 0) $asData[Db::getId(User::USER_TABLE)] = $this->oUser->getUserId();
|
||||
|
||||
$iPostId = $this->oDb->insertRow(self::POST_TABLE, $asData);
|
||||
if($iPostId == 0) $sLangId = 'error.commit_db';
|
||||
|
||||
$this->oUser->updateNickname($sName);
|
||||
}
|
||||
else $sLangId = 'project.modes.histo';
|
||||
|
||||
return self::getJsonResult(($iPostId > 0), $sLangId);
|
||||
}
|
||||
|
||||
public function upload() {
|
||||
$oUploader = new Uploader($this->oMedia);
|
||||
|
||||
return $oUploader->sBody;
|
||||
}
|
||||
|
||||
public function addComment($iMediaId, $sComment) {
|
||||
$oMedia = new Media($this->oDb, $this->oProject, $iMediaId);
|
||||
$asResult = $oMedia->setComment($sComment);
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data'], $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
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();
|
||||
$sLangId = $bSuccess?'email.sent':'email.failure';
|
||||
}
|
||||
else $sLangId = 'error.commit_db';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sLangId);
|
||||
}
|
||||
|
||||
public function getAdminSettings() {
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$asData = [
|
||||
'project' => $this->oProject->getProjects(),
|
||||
'feed' => $oFeed->getFeeds(),
|
||||
'spot' => $oFeed->getSpots(),
|
||||
'user' => $this->oUser->getSubscribedUsersInfo()
|
||||
];
|
||||
|
||||
foreach($asData['project'] as &$asProject) {
|
||||
$asProject['active_from'] = substr($asProject['active_from'], 0, 10);
|
||||
$asProject['active_to'] = substr($asProject['active_to'], 0, 10);
|
||||
}
|
||||
|
||||
return self::getJsonResult(true, '', $asData);
|
||||
}
|
||||
|
||||
public function setAdminSettings($sType, $iId, $sField, $sValue) {
|
||||
$bSuccess = false;
|
||||
$sLangId = '';
|
||||
$asLangParams = [];
|
||||
$asResult = [];
|
||||
|
||||
if($this->oDb->isId($sField) && $sValue <= 0) return self::getJsonResult(false, 'error.impossible_value', [], [$sValue, $sField]);
|
||||
|
||||
switch($sType) {
|
||||
case 'project':
|
||||
$oProject = new Project($this->oDb, $iId);
|
||||
|
||||
switch($sField) {
|
||||
case 'name':
|
||||
$bSuccess = $oProject->setProjectName($sValue);
|
||||
break;
|
||||
case 'codename':
|
||||
$bSuccess = $oProject->setProjectCodeName($sValue);
|
||||
break;
|
||||
case 'active_from':
|
||||
$bSuccess = $oProject->setActivePeriod($sValue.' 00:00:00', 'from');
|
||||
break;
|
||||
case 'active_to':
|
||||
$bSuccess = $oProject->setActivePeriod($sValue.' 23:59:59', 'to');
|
||||
break;
|
||||
default:
|
||||
$sLangId = 'error.unknown_field';
|
||||
$asLangParams = [$sField];
|
||||
}
|
||||
|
||||
//Identify missing GPX file
|
||||
$sProjectCodeName = ($sField == 'codename')?$sValue:$oProject->getProjectCodeName();
|
||||
if(!Converter::hasGpxFile($sProjectCodeName)) {
|
||||
$bSuccess = true;
|
||||
$sLangId = 'error.file_missing';
|
||||
$asLangParams = ['GPX', $sProjectCodeName.Gpx::EXT];
|
||||
}
|
||||
|
||||
$asResult = $oProject->getProject();
|
||||
$asResult['active_from'] = substr($asResult['active_from'], 0, 10);
|
||||
$asResult['active_to'] = substr($asResult['active_to'], 0, 10);
|
||||
break;
|
||||
case 'feed':
|
||||
$oFeed = new Feed($this->oDb, $iId);
|
||||
switch($sField) {
|
||||
case 'ref_feed_id':
|
||||
$bSuccess = $oFeed->setRefFeedId($sValue);
|
||||
break;
|
||||
case 'id_spot':
|
||||
$bSuccess = $oFeed->setSpotId($sValue);
|
||||
break;
|
||||
case 'id_project':
|
||||
$bSuccess = $oFeed->setProjectId($sValue);
|
||||
break;
|
||||
default:
|
||||
$sLangId = 'error.unknown_field';
|
||||
$asLangParams = [$sField];
|
||||
}
|
||||
$asResult = $oFeed->getFeed();
|
||||
break;
|
||||
case 'user':
|
||||
switch($sField) {
|
||||
case 'clearance':
|
||||
$asReturnCode = $this->oUser->setUserClearance($iId, $sValue);
|
||||
$bSuccess = $asReturnCode['result'];
|
||||
$sLangId = $asReturnCode['desc_lang_id'];
|
||||
$asLangParams = $asReturnCode['desc_lang_params'];
|
||||
break;
|
||||
default:
|
||||
$sLangId = 'error.unknown_field';
|
||||
$asLangParams = [$sField];
|
||||
}
|
||||
$asResult = $this->oUser->getUserById($iId);
|
||||
break;
|
||||
}
|
||||
if(!$bSuccess && $sLangId=='') $sLangId = 'error.commit_db';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sLangId, [$sType=>[$asResult]], $asLangParams);
|
||||
}
|
||||
|
||||
public function createAdminSettings($sType) {
|
||||
$bSuccess = false;
|
||||
$sLangId = '';
|
||||
$asResult = [];
|
||||
|
||||
switch($sType) {
|
||||
case 'project':
|
||||
$oProject = new Project($this->oDb);
|
||||
$iNewProjectId = $oProject->createProjectId();
|
||||
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$oFeed->createFeedId($iNewProjectId);
|
||||
|
||||
$bSuccess = $iNewProjectId > 0;
|
||||
$asResult = [
|
||||
'project' => [$oProject->getProject()],
|
||||
'feed' => [$oFeed->getFeed()]
|
||||
];
|
||||
break;
|
||||
case 'feed':
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$iNewFeedId = $oFeed->createFeedId($this->oProject->getProjectId());
|
||||
$bSuccess = $iNewFeedId > 0;
|
||||
$asResult = [
|
||||
'feed' => [$oFeed->getFeed()]
|
||||
];
|
||||
break;
|
||||
}
|
||||
if(!$bSuccess && $sLangId=='') $sLangId = 'error.commit_db';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sLangId, $asResult);
|
||||
}
|
||||
|
||||
public function deleteAdminSettings($sType, $iId) {
|
||||
$bSuccess = false;
|
||||
$sLangId = '';
|
||||
$asLangParams = [];
|
||||
$asResult = [];
|
||||
|
||||
switch($sType) {
|
||||
case 'project':
|
||||
$oProject = new Project($this->oDb, $iId);
|
||||
$asResult = $oProject->delete();
|
||||
$sLangId = $asResult['project'][0]['desc_lang_id'];
|
||||
$asLangParams = $asResult['project'][0]['desc_lang_params'];
|
||||
$bSuccess = $asResult['project'][0]['del'];
|
||||
break;
|
||||
case 'feed':
|
||||
$oFeed = new Feed($this->oDb, $iId);
|
||||
$asResult = ['feed' => [$oFeed->delete()]];
|
||||
$sLangId = $asResult['feed'][0]['desc_lang_id'];
|
||||
$asLangParams = $asResult['feed'][0]['desc_lang_params'];
|
||||
$bSuccess = $asResult['feed'][0]['result'];
|
||||
break;
|
||||
}
|
||||
if(!$bSuccess && $sLangId=='') $sLangId = 'error.commit_db';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sLangId, $asResult, $asLangParams);
|
||||
}
|
||||
|
||||
public static function decToDms($dValue, $sType) {
|
||||
if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude
|
||||
else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude
|
||||
|
||||
$dLeft = abs($dValue);
|
||||
|
||||
//Degrees
|
||||
$iDegree = floor($dLeft);
|
||||
$dLeft -= $iDegree;
|
||||
|
||||
//Minutes
|
||||
$iMinute = floor($dLeft * 60);
|
||||
$dLeft -= $iMinute / 60;
|
||||
|
||||
//Seconds
|
||||
$fSecond = round($dLeft * 3600, 1);
|
||||
|
||||
return
|
||||
$iDegree.'°'.
|
||||
self::getNumberWithLeadingZeros($iMinute, 2, 0)."'".
|
||||
self::getNumberWithLeadingZeros($fSecond, 2, 1).'"'.
|
||||
$sDirection;
|
||||
}
|
||||
|
||||
public static function getNumberWithLeadingZeros($fValue, $iNbLeadingZeros, $iNbDigits) {
|
||||
$sDecimalSeparator = '.';
|
||||
if($iNbDigits > 0) $iNbLeadingZeros += mb_strlen($sDecimalSeparator) + $iNbDigits;
|
||||
$sPattern = '%0'.$iNbLeadingZeros.$sDecimalSeparator.$iNbDigits.'f';
|
||||
return sprintf($sPattern, $fValue);
|
||||
}
|
||||
|
||||
public function getTimeFormat($iTime, $sTimeZone='') {
|
||||
if($sTimeZone == '') $sTimeZone = date_default_timezone_get();
|
||||
|
||||
$oDate = new \DateTime('@'.$iTime);
|
||||
$oDate->setTimezone(new \DateTimeZone($sTimeZone));
|
||||
|
||||
$sDate = $oDate->format('d/m/Y');
|
||||
$sTime = $oDate->format('H:i');
|
||||
return $this->oLang->getTranslation('time.date_time', [$sDate, $sTime]);
|
||||
}
|
||||
|
||||
public static function getTimeZoneDayOffset($iTime, $sLocalTimeZone) {
|
||||
$sSiteTimeZone = date_default_timezone_get();
|
||||
$iLocalDate = (int) (new \DateTime('@'.$iTime))->setTimezone(new \DateTimeZone($sLocalTimeZone))->format('Ymd');
|
||||
$iSiteDate = (int) (new \DateTime('@'.$iTime))->setTimezone(new \DateTimeZone($sSiteTimeZone ))->format('Ymd');
|
||||
|
||||
return ($iLocalDate == $iSiteDate)?'0':(($iLocalDate < $iSiteDate)?'+1':'-1');
|
||||
}
|
||||
|
||||
public static function getTimeZoneFromDate($sDate) {
|
||||
$sTimeZone = null;
|
||||
|
||||
preg_match('/(?<timezone>(\+|\-)\d{2}:?(\d{2}|))$/', $sDate, $asMatch);
|
||||
if(array_key_exists('timezone', $asMatch)) {
|
||||
$sTimeZone = $asMatch['timezone'];
|
||||
|
||||
//Complete short form: +12 => +1200
|
||||
if(strlen($sTimeZone) == 3) $sTimeZone .= '00';
|
||||
|
||||
//Add colon: +1200 => +12:00
|
||||
if(!strpos($sTimeZone, ':')) $sTimeZone = substr_replace($sTimeZone, ':', 3, 0);
|
||||
}
|
||||
return $sTimeZone;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Db;
|
||||
|
||||
class Map extends PhpObject {
|
||||
|
||||
public const MAP_TABLE = 'maps';
|
||||
public const MAPPING_TABLE = 'mappings';
|
||||
|
||||
private Db $oDb;
|
||||
private $asMaps;
|
||||
|
||||
public function __construct(Db &$oDb) {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->asMaps = [];
|
||||
}
|
||||
|
||||
private function setMaps() {
|
||||
$asMaps = $this->oDb->selectRows(['from'=>self::MAP_TABLE]);
|
||||
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
||||
}
|
||||
|
||||
private function getMaps($sCodeName='') {
|
||||
if(empty($this->asMaps)) $this->setMaps();
|
||||
return ($sCodeName=='')?$this->asMaps:$this->asMaps[$sCodeName];
|
||||
}
|
||||
|
||||
public function getProjectMaps($iProjectId) {
|
||||
$asMappings = $this->oDb->selectRows(
|
||||
[
|
||||
'select' => [Db::getId(self::MAP_TABLE), 'default_map'],
|
||||
'from' => self::MAPPING_TABLE,
|
||||
'constraint'=> ["IFNULL(id_project, {$iProjectId})" => $iProjectId]
|
||||
],
|
||||
Db::getId(self::MAP_TABLE)
|
||||
);
|
||||
|
||||
$asProjectMaps = [];
|
||||
foreach($this->getMaps() as $asMap) {
|
||||
if(array_key_exists($asMap['id_map'], $asMappings)) {
|
||||
$asMap['default_map'] = $asMappings[$asMap['id_map']];
|
||||
$asProjectMaps[] = $asMap;
|
||||
}
|
||||
}
|
||||
|
||||
return $asProjectMaps;
|
||||
}
|
||||
|
||||
public function getMapUrl($sCodeName, $asParams) {
|
||||
$asMap = $this->getMaps($sCodeName);
|
||||
$asParams['token'] = $asMap['token'];
|
||||
return self::populateParams($asMap['pattern'], $asParams);
|
||||
}
|
||||
|
||||
private static function populateParams($sUrl, $asParams) {
|
||||
foreach($asParams as $sParam=>$sValue) {
|
||||
$sUrl = str_replace('{'.$sParam.'}', $sValue, $sUrl);
|
||||
}
|
||||
|
||||
return $sUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
use Franzz\Objects\UploadHandler;
|
||||
|
||||
class Uploader extends UploadHandler {
|
||||
private Media $oMedia;
|
||||
|
||||
public string $sBody;
|
||||
|
||||
public function __construct(Media &$oMedia) {
|
||||
$this->oMedia = &$oMedia;
|
||||
$this->sBody = '';
|
||||
|
||||
parent::__construct([
|
||||
'upload_dir' => Media::MEDIA_FOLDER.'/',
|
||||
'image_versions' => [],
|
||||
'accept_file_types' => '/\.(gif|jpe?g|png|mov|mp4)$/i'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function validate($uploaded_file, $file, $error, $index, $content_range) {
|
||||
$bResult = parent::validate($uploaded_file, $file, $error, $index, $content_range);
|
||||
|
||||
//Check project mode
|
||||
if(!$this->oMedia->isProjectEditable()) {
|
||||
$file->error = true;
|
||||
$file->desc_lang_id = 'upload.mode_archived';
|
||||
$file->desc_lang_params = [$this->oMedia->getProjectCodeName()];
|
||||
$bResult = false;
|
||||
}
|
||||
|
||||
return $bResult;
|
||||
}
|
||||
|
||||
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) {
|
||||
$sExt = strtolower(pathinfo((string) $name, PATHINFO_EXTENSION));
|
||||
$sStoredName = bin2hex(random_bytes(16)).($sExt !== ''?'.'.$sExt:'');
|
||||
$file = parent::handle_file_upload($uploaded_file, $sStoredName, $size, $type, $error, $index, $content_range);
|
||||
|
||||
if(empty($file->error)) {
|
||||
$asResult = $this->oMedia->addMedia($file->name);
|
||||
if(!$asResult['result']) {
|
||||
$file->error = true;
|
||||
$file->desc_lang_id = $asResult['desc_lang_id'];
|
||||
$file->desc_lang_params = $asResult['data'];
|
||||
}
|
||||
else {
|
||||
$file->original_name = basename((string) $name);
|
||||
$file->id = $this->oMedia->getMediaId();
|
||||
$file->thumbnail = $asResult['data']['thumb_path'];
|
||||
}
|
||||
}
|
||||
if(!empty($file->error)) {
|
||||
if(empty($file->desc_lang_id)) $file->desc_lang_id = is_string($file->error)?$file->error:'upload.error';
|
||||
if(empty($file->desc_lang_params)) $file->desc_lang_params = [];
|
||||
$file->error = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
protected function body($sBodyPart) {
|
||||
$this->sBody .= $sBodyPart;
|
||||
}
|
||||
|
||||
protected function get_error_message($sLangId, $asParams=[]) {
|
||||
return array_key_exists($sLangId, $this->error_messages)?'upload.error':$sLangId;
|
||||
}
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Livetrail;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\Db;
|
||||
|
||||
class User extends PhpObject {
|
||||
|
||||
//DB Tables
|
||||
public const USER_TABLE = 'users';
|
||||
|
||||
//Clearance Levels
|
||||
public const CLEARANCE_USER = 0;
|
||||
public const CLEARANCE_ADMIN = 9;
|
||||
public const CLEARANCES = ['user'=>self::CLEARANCE_USER, 'admin'=>self::CLEARANCE_ADMIN];
|
||||
|
||||
public const USER_SUBSCRIBED = 1;
|
||||
public const USER_UNSUBSCRIBED = 0;
|
||||
|
||||
public const DEFAULT_USER = [
|
||||
'id' => 0,
|
||||
'id_user' => 0,
|
||||
'name' => '',
|
||||
'email' => '',
|
||||
'language' => '',
|
||||
'timezone' => '',
|
||||
'subscribed'=> self::USER_UNSUBSCRIBED,
|
||||
'clearance' => self::CLEARANCE_USER
|
||||
];
|
||||
|
||||
//Session & Cookie
|
||||
private const SESSION_ID_USER = 'id_user';
|
||||
private const SESSION_ADMIN = 'admin_authenticated';
|
||||
private const COOKIE_TOKEN = 'login';
|
||||
private const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
||||
|
||||
/**
|
||||
* Database Handle
|
||||
* @var Db
|
||||
*/
|
||||
private $oDb;
|
||||
|
||||
//User Info
|
||||
private $iUserId;
|
||||
private $asUserInfo;
|
||||
|
||||
public function __construct(Db &$oDb) {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->setUserId(0);
|
||||
$this->checkSession();
|
||||
}
|
||||
|
||||
public function getUserId() {
|
||||
return $this->iUserId;
|
||||
}
|
||||
|
||||
public function setUserId($iUserId) {
|
||||
$this->iUserId = 0;
|
||||
$this->asUserInfo = self::DEFAULT_USER;
|
||||
|
||||
if($iUserId > 0) {
|
||||
$asUser = $this->getUserById($iUserId);
|
||||
if(!empty($asUser)) {
|
||||
$this->iUserId = $iUserId;
|
||||
$this->asUserInfo = $asUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserInfo() {
|
||||
return $this->asUserInfo;
|
||||
}
|
||||
|
||||
public function getUserById($iUserId) {
|
||||
$asUsersInfo = [];
|
||||
if($iUserId > 0) $asUsersInfo = $this->getUsersInfo($iUserId);
|
||||
return empty($asUsersInfo)?[]:array_shift($asUsersInfo);
|
||||
}
|
||||
|
||||
public function getUsersInfo($iUserId=-1) {
|
||||
|
||||
//Mapping between user fields and DB fields
|
||||
$asSelect = array_keys($this->asUserInfo);
|
||||
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE).' AS id';
|
||||
|
||||
$asInfo = [
|
||||
'select' => $asSelect,
|
||||
'from' => self::USER_TABLE
|
||||
];
|
||||
if($iUserId != -1) $asInfo['constraint'] = [Db::getId(self::USER_TABLE) => $iUserId];
|
||||
|
||||
return $this->oDb->selectRows($asInfo);
|
||||
}
|
||||
|
||||
public function getLang() {
|
||||
return $this->asUserInfo['language'];
|
||||
}
|
||||
|
||||
private function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
||||
$bSuccess = false;
|
||||
$sLangId = '';
|
||||
|
||||
$iUserId = $this->oDb->insertRow(
|
||||
self::USER_TABLE,
|
||||
['email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone]
|
||||
);
|
||||
|
||||
if($iUserId == 0) $sLangId = 'error.commit_db';
|
||||
else $bSuccess = true;
|
||||
|
||||
//Extra optional values
|
||||
if($bSuccess) {
|
||||
$this->updateNickname($sNickName);
|
||||
$this->updateGravatar($iUserId, $sEmail);
|
||||
}
|
||||
|
||||
return Livetrail::getResult($bSuccess, $sLangId, [Db::getId(self::USER_TABLE) => $iUserId]);
|
||||
}
|
||||
|
||||
public function setSubscription($bSubscribed) {
|
||||
if($this->getUserId() > 0) {
|
||||
$iSubscribed = $bSubscribed?1:0;
|
||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), ['subscribed'=>$iSubscribed]);
|
||||
if(!$iUserId) return Livetrail::getResult(false, 'error.commit_db');
|
||||
$this->asUserInfo['subscribed'] = $iSubscribed;
|
||||
return Livetrail::getResult(true, $iSubscribed?'account.subscribed':'account.unsubscribed');
|
||||
}
|
||||
}
|
||||
|
||||
public function getSubscribedUsersInfo() {
|
||||
$asSelect = array_keys($this->asUserInfo);
|
||||
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE).' AS id';
|
||||
return $this->oDb->selectRows([
|
||||
'select'=>$asSelect,
|
||||
'from'=>self::USER_TABLE,
|
||||
'constraint'=>['subscribed'=>self::USER_SUBSCRIBED]
|
||||
]);
|
||||
}
|
||||
|
||||
public function login($sEmail, $sPassword, $sLang, $sTimezone, $sNickName='') {
|
||||
$bSuccess = false;
|
||||
$bSubscribe = false;
|
||||
$sLangId = '';
|
||||
$sEmail = strtolower(trim($sEmail));
|
||||
|
||||
//Check email value
|
||||
if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$sLangId = 'account.invalid_email';
|
||||
}
|
||||
else {
|
||||
//Check Email presence in DB
|
||||
$asDBUser = $this->oDb->selectRow(
|
||||
self::USER_TABLE,
|
||||
['email' => $sEmail],
|
||||
[Db::getId(self::USER_TABLE), 'password', 'clearance']
|
||||
);
|
||||
$iUserId = $asDBUser[Db::getId(self::USER_TABLE)] ?? 0;
|
||||
|
||||
//User exists
|
||||
if($iUserId > 0) {
|
||||
//Is Admin
|
||||
if($asDBUser['clearance'] >= self::CLEARANCE_ADMIN) {
|
||||
//Request a password
|
||||
if($sPassword === '') $sLangId = empty($asDBUser['password'])?'account.set_password':'account.password_required';
|
||||
|
||||
//Set password
|
||||
elseif(empty($asDBUser['password'])) {
|
||||
if(!$this->oDb->updateRow(self::USER_TABLE, $iUserId, ['password' => password_hash($sPassword, PASSWORD_DEFAULT)])) $sLangId = 'error.commit_db';
|
||||
else {
|
||||
$sLangId = 'account.password_set';
|
||||
$bSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Check password
|
||||
elseif(password_verify($sPassword, $asDBUser['password'])) {
|
||||
$bSuccess = true;
|
||||
$sLangId = 'account.logged_in';
|
||||
}
|
||||
else $sLangId = 'account.invalid_credentials';
|
||||
}
|
||||
else {
|
||||
$bSuccess = true;
|
||||
$sLangId = 'account.logged_in';
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Unknown user, create it
|
||||
$asAddResult = $this->addUser($sEmail, $sLang, $sTimezone, $sNickName);
|
||||
$bSuccess = $asAddResult['result'];
|
||||
$bSubscribe = $bSuccess;
|
||||
$sLangId = $bSuccess?'':$asAddResult['desc_lang_id'];
|
||||
$iUserId = $asAddResult['data'][Db::getId(self::USER_TABLE)] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
if($bSuccess) {
|
||||
$this->setUserId($iUserId);
|
||||
$this->setSession();
|
||||
$this->setTokenCookie();
|
||||
}
|
||||
|
||||
return Livetrail::getResult($bSuccess, $sLangId, ['subscribe'=>$bSubscribe]);
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
if($this->getUserId() > 0) $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), ['token' => '', 'token_exp' => '0000-00-00 00:00:00']);
|
||||
$this->clearSession();
|
||||
$this->clearCookie();
|
||||
$this->setUserId(0);
|
||||
return Livetrail::getResult(true, 'account.logged_out');
|
||||
}
|
||||
|
||||
public function updateNickname($sNickname) {
|
||||
if($this->getUserId() > 0 && $sNickname!='') $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), ['name'=>$sNickname]);
|
||||
}
|
||||
|
||||
private function updateGravatar($iUserId, $sEmail) {
|
||||
$sImage = ($sEmail != '')?@file_get_contents('https://www.gravatar.com/avatar/'.md5($sEmail).'.png?d=404&s=24'):'';
|
||||
$this->oDb->updateRow(self::USER_TABLE, $iUserId, ['gravatar' => base64_encode($sImage)]);
|
||||
}
|
||||
|
||||
public function checkUserClearance($iClearance) {
|
||||
return ($this->asUserInfo['clearance'] >= $iClearance);
|
||||
}
|
||||
|
||||
public function setUserClearance($iUserId, $iClearance) {
|
||||
$bSuccess = false;
|
||||
$sLangId = '';
|
||||
$asLangParams = [];
|
||||
|
||||
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) $sLangId = 'error.no_auth';
|
||||
else {
|
||||
if(!in_array($iClearance, self::CLEARANCES)) {
|
||||
$sLangId = 'error.impossible_value';
|
||||
$asLangParams = [$iClearance, 'clearance'];
|
||||
}
|
||||
else {
|
||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, ['clearance'=>$iClearance]);
|
||||
if(!$iUserId) $sLangId = 'error.commit_db';
|
||||
else $bSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Livetrail::getResult($bSuccess, $sLangId, [], $asLangParams);
|
||||
}
|
||||
|
||||
/* Session */
|
||||
|
||||
private function checkSession() {
|
||||
$iUserId = (int) ($_SESSION[self::SESSION_ID_USER] ?? 0);
|
||||
if($iUserId > 0) {
|
||||
$this->setUserId($iUserId);
|
||||
if($this->checkUserClearance(self::CLEARANCE_ADMIN) && empty($_SESSION[self::SESSION_ADMIN])) $this->logout();
|
||||
}
|
||||
else $this->checkTokenCookie();
|
||||
}
|
||||
|
||||
private function setSession() {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION[self::SESSION_ID_USER] = $this->getUserId();
|
||||
$_SESSION[self::SESSION_ADMIN] = $this->checkUserClearance(self::CLEARANCE_ADMIN);
|
||||
}
|
||||
|
||||
private function clearSession() {
|
||||
unset($_SESSION[self::SESSION_ID_USER], $_SESSION[self::SESSION_ADMIN]);
|
||||
}
|
||||
|
||||
/* Cookie */
|
||||
|
||||
private function checkTokenCookie() {
|
||||
$sCookieValue = $this->getCookie();
|
||||
|
||||
//Check cookie structure
|
||||
$asToken = explode(':', $sCookieValue, 3);
|
||||
$iUserId = $asToken[0];
|
||||
if(count($asToken) !== 3 || !ctype_digit($iUserId) || !ctype_digit($asToken[1]) || !preg_match('/^[a-f0-9]{64}$/', $asToken[2])) {
|
||||
if($sCookieValue !== '') $this->clearCookie();
|
||||
return;
|
||||
}
|
||||
|
||||
//Get user info
|
||||
$asUser = $this->oDb->selectRow(
|
||||
self::USER_TABLE,
|
||||
$iUserId,
|
||||
['clearance', 'token', 'token_exp']
|
||||
);
|
||||
|
||||
//Check token value
|
||||
if(!empty($asUser)
|
||||
&& $asUser['clearance'] == $asToken[1]
|
||||
&& time() <= strtotime($asUser['token_exp'])
|
||||
&& hash_equals($asUser['token'], hash('sha256', $sCookieValue))
|
||||
) {
|
||||
$this->setUserId($iUserId);
|
||||
$this->setSession();
|
||||
}
|
||||
else $this->clearCookie();
|
||||
}
|
||||
|
||||
private function setTokenCookie() {
|
||||
$sToken = bin2hex(random_bytes(32));
|
||||
$iClearance = $this->asUserInfo['clearance'];
|
||||
$sCookieValue = implode(':', [$this->getUserId(), $iClearance, $sToken]);
|
||||
|
||||
$this->oDb->updateRow(
|
||||
self::USER_TABLE,
|
||||
$this->getUserId(),
|
||||
[
|
||||
'token' => hash('sha256', $sCookieValue),
|
||||
'token_exp' => date(Db::TIMESTAMP_FORMAT, time() + self::COOKIE_DURATION)
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCookie($sCookieValue, time() + self::COOKIE_DURATION);
|
||||
}
|
||||
|
||||
private function getCookie() {
|
||||
return $_COOKIE[self::COOKIE_TOKEN] ?? '';
|
||||
}
|
||||
|
||||
private function setCookie($sValue, $iExpires) {
|
||||
setcookie(
|
||||
self::COOKIE_TOKEN,
|
||||
$sValue,
|
||||
[
|
||||
'expires' => $iExpires,
|
||||
'path' => '/',
|
||||
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function clearCookie() {
|
||||
$this->setCookie('', time() - 3600);
|
||||
unset($_COOKIE[self::COOKIE_TOKEN]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<div id="admin">
|
||||
<a name="back" class="button" href="[#]host_url[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
|
||||
<h1>[#]lang:projects[#]</h1>
|
||||
<div id="project_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[#]lang:id_project[#]</th>
|
||||
<th>[#]lang:project[#]</th>
|
||||
<th>[#]lang:mode[#]</th>
|
||||
<th>[#]lang:code_name[#]</th>
|
||||
<th>[#]lang:start[#]</th>
|
||||
<th>[#]lang:end[#]</th>
|
||||
<th>[#]lang:delete[#]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div id="new"></div>
|
||||
</div>
|
||||
<h1>[#]lang:feeds[#]</h1>
|
||||
<div id="feed_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[#]lang:id_feed[#]</th>
|
||||
<th>[#]lang:ref_feed_id[#]</th>
|
||||
<th>[#]lang:id_spot[#]</th>
|
||||
<th>[#]lang:id_project[#]</th>
|
||||
<th>[#]lang:name[#]</th>
|
||||
<th>[#]lang:status[#]</th>
|
||||
<th>[#]lang:last_update[#]</th>
|
||||
<th>[#]lang:delete[#]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h1>Spots</h1>
|
||||
<div id="spot_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[#]lang:id_spot[#]</th>
|
||||
<th>[#]lang:ref_spot_id[#]</th>
|
||||
<th>[#]lang:name[#]</th>
|
||||
<th>[#]lang:model[#]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h1>[#]lang:active_users[#]</h1>
|
||||
<div id="user_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[#]lang:id_user[#]</th>
|
||||
<th>[#]lang:user_name[#]</th>
|
||||
<th>[#]lang:language[#]</th>
|
||||
<th>[#]lang:time_zone[#]</th>
|
||||
<th>[#]lang:clearance[#]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h1>[#]lang:toolbox[#]</h1>
|
||||
<div id="toolbox"></div>
|
||||
<div id="feedback" class="feedback"></div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
oSpot.pageInit = function(asHash) {
|
||||
self.get('admin_get', setProjects);
|
||||
$('#new').addButton('new', self.lang('new_project'), 'new', createProject);
|
||||
$('#toolbox').addButton('refresh', self.lang('update_project'), 'refresh', updateProject);
|
||||
};
|
||||
|
||||
oSpot.onFeedback = function(sType, sMsg, asContext) {
|
||||
delete asContext.a;
|
||||
delete asContext.t;
|
||||
sMsg += ' (';
|
||||
$.each(asContext, function(sKey, sElem) {
|
||||
sMsg += sKey+'='+sElem+' / ' ;
|
||||
});
|
||||
sMsg = sMsg.slice(0, -3)+')';
|
||||
$('#feedback').append($('<p>', {'class': sType}).text(sMsg));
|
||||
};
|
||||
|
||||
function setProjects(asElemTypes) {
|
||||
var aoEvents = [{on:'change', callback:commit}, {on:'keyup', callback:waitAndCommit}];
|
||||
var aoChangeEvent = [aoEvents[0]];
|
||||
|
||||
$.each(asElemTypes, function(sElemType, aoElems) {
|
||||
$.each(aoElems, function(iKey, oElem) {
|
||||
var sElemId = sElemType+'_'+oElem.id;
|
||||
var bNew = ($('#'+sElemId).length == 0);
|
||||
|
||||
var $Elem = (bNew?$('<tr>', {'id': sElemId}):$('#'+sElemId))
|
||||
.data('type', sElemType)
|
||||
.data('id', oElem.id);
|
||||
|
||||
if(oElem.del) $Elem.remove();
|
||||
else if(!bNew) {
|
||||
$Elem.find('input').each(function(iKey, oInput){
|
||||
var $Input = $(oInput);
|
||||
if($Input.attr('name') in oElem && $Input.attr('type')!='date') $Input.val(oElem[$Input.attr('name')]);
|
||||
});
|
||||
}
|
||||
else {
|
||||
$Elem.append($('<td>').text(oElem.id || ''));
|
||||
switch(sElemType) {
|
||||
case 'project':
|
||||
$Elem
|
||||
.append($('<td>').addInput('text', 'name', oElem.name, aoEvents))
|
||||
.append($('<td>', {'class': 'mode'}).text(oElem.mode))
|
||||
.append($('<td>').addInput('text', 'codename', oElem.codename, aoEvents))
|
||||
.append($('<td>').addInput('date', 'active_from', oElem.active_from, aoChangeEvent))
|
||||
.append($('<td>').addInput('date', 'active_to', oElem.active_to, aoChangeEvent))
|
||||
.append($('<td>').addButton('close fa-lg', '', 'del_proj', del));
|
||||
break;
|
||||
case 'feed':
|
||||
$Elem
|
||||
.append($('<td>').addInput('text', 'ref_feed_id', oElem.ref_feed_id, aoEvents))
|
||||
.append($('<td>').addInput('number', 'id_spot', oElem.id_spot, aoEvents))
|
||||
.append($('<td>').addInput('number', 'id_project', oElem.id_project, aoEvents))
|
||||
.append($('<td>').text(oElem.name))
|
||||
.append($('<td>').text(oElem.status))
|
||||
.append($('<td>').text(oElem.last_update))
|
||||
.append($('<td>').addButton('close fa-lg', '', 'del_feed', del));
|
||||
break;
|
||||
case 'spot':
|
||||
$Elem
|
||||
.append($('<td>').text(oElem.ref_spot_id))
|
||||
.append($('<td>').text(oElem.name))
|
||||
.append($('<td>').text(oElem.model))
|
||||
break;
|
||||
case 'user':
|
||||
$Elem
|
||||
.append($('<td>').text(oElem.name))
|
||||
.append($('<td>').text(oElem.language))
|
||||
.append($('<td>').text(oElem.timezone))
|
||||
.append($('<td>').addInput('number', 'clearance', oElem.clearance, aoEvents))
|
||||
break;
|
||||
}
|
||||
|
||||
$Elem.appendTo($('#'+sElemType+'_section').find('table tbody'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createProject() {
|
||||
self.get('admin_new', setProjects);
|
||||
}
|
||||
|
||||
function updateProject() {
|
||||
self.get(
|
||||
'update_project',
|
||||
function(asData, sMsg){oSpot.onFeedback('success', sMsg, {'update':'project'});},
|
||||
{},
|
||||
function(sMsg){oSpot.onFeedback('error', sMsg, {'update':'project'});}
|
||||
);
|
||||
}
|
||||
|
||||
function commit(event, $This) {
|
||||
$This = $This || $(this);
|
||||
if(typeof self.tmp('wait') != 'undefined') clearTimeout(self.tmp('wait'));
|
||||
|
||||
var sOldVal = $This.data('old_value');
|
||||
var sNewVal = $This.val();
|
||||
if(sOldVal!=sNewVal) {
|
||||
$This.data('old_value', sNewVal);
|
||||
|
||||
var $Record = $This.closest('tr');
|
||||
var asInputs = {type: $Record.data('type'), id: $Record.data('id'), field: $This.attr('name'), value: sNewVal};
|
||||
self.get(
|
||||
'admin_set',
|
||||
function(asData){
|
||||
oSpot.onFeedback('success', self.lang('admin_save_success'), asInputs);
|
||||
setProjects(asData);
|
||||
},
|
||||
asInputs,
|
||||
function(sError){
|
||||
$This.data('old_value', sOldVal);
|
||||
oSpot.onFeedback('error', sError, asInputs);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function waitAndCommit(event) {
|
||||
if(typeof self.tmp('wait') != 'undefined') clearTimeout(self.tmp('wait'));
|
||||
self.tmp('wait', setTimeout(()=>{commit(event,$(this));}, 2000));
|
||||
}
|
||||
|
||||
function del() {
|
||||
var $Record = $(this).closest('tr');
|
||||
var asInputs = {type: $Record.data('type'), id: $Record.data('id')};
|
||||
self.get(
|
||||
'admin_del',
|
||||
function(asData){
|
||||
oSpot.onFeedback('success', self.lang('admin_save_success'), asInputs);
|
||||
setProjects(asData);
|
||||
},
|
||||
asInputs,
|
||||
function(sError){
|
||||
oSpot.onFeedback('error', sError, asInputs);
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
<title>[#]lang:email_conf_subject[#]</title>
|
||||
</head>
|
||||
<body>
|
||||
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:conf_preheader[#]</span>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
|
||||
<tr>
|
||||
<td width="20%"><img src="[#]local_server[#]images/icons/mstile-144x144.png" width="90%" border="0" alt="logo" /></td>
|
||||
<td><h1>[#]lang:conf_thanks_sub[#]</h1></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p align="justify">[#]lang:conf_body_para_1[#]</p>
|
||||
<p align="justify">[#]lang:conf_body_para_2[#]</p>
|
||||
<p align="justify">[#]lang:conf_body_para_3[#]</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p>[#]lang:conf_body_conclusion[#]<br />[#]lang:conf_signature[#]</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p>[#]lang:email_unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email_unsub_btn[#]</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,14 +2,14 @@
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
<title>[#]lang:email.update.subject[#]</title>
|
||||
<title>[#]lang:email_update_subject[#]</title>
|
||||
</head>
|
||||
<body>
|
||||
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:email.update.preheader[#]</span>
|
||||
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:update_preheader[#]</span>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
|
||||
<tr>
|
||||
<td width="20%"><img src="[#]local_server[#]assets/images/icons/favicon-96x96.png" width="90%" border="0" alt="logo" /></td>
|
||||
<td><h1>[#]lang:email.update.title[#] [#]type[#] #[#]displayed_id[#]</h1></td>
|
||||
<td width="20%"><img src="[#]local_server[#]images/icons/mstile-144x144.png" width="90%" border="0" alt="logo" /></td>
|
||||
<td><h1>[#]lang:update_title[#] [#]type[#] #[#]displayed_id[#]</h1></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
@@ -22,7 +22,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<h2>[#]lang:email.update.latest_news[#]</h2>
|
||||
<h2>[#]lang:update_latest_news[#]</h2>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<!-- [PART] news [START] -->
|
||||
<tr>
|
||||
@@ -39,7 +39,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p>[#]lang:email.unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email.unsubscribe_button[#]</a></p>
|
||||
<p>[#]lang:email_unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email_unsub_btn[#]</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="[#]language[#]">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="[#]lang:page_og_desc[#]">
|
||||
<meta property="og:title" content="Spotty" />
|
||||
<meta property="og:description" content="[#]lang:page_og_desc[#]" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="[#]host_url[#]" />
|
||||
<meta property="og:image" content="images/ogp.png" />
|
||||
<meta property="og:locale" content="[#]lang:locale[#]" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="images/icons/apple-touch-icon.png?v=GvmqYyKwbb">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="images/icons/favicon-32x32.png?v=GvmqYyKwbb">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="images/icons/favicon-16x16.png?v=GvmqYyKwbb">
|
||||
<link rel="manifest" href="images/icons/site.webmanifest?v=GvmqYyKwbb">
|
||||
<link rel="mask-icon" href="images/icons/safari-pinned-tab.svg?v=GvmqYyKwbb" color="#44d15a">
|
||||
<link rel="shortcut icon" href="images/icons/favicon.ico?v=GvmqYyKwbb">
|
||||
<meta name="msapplication-TileColor" content="#00a300">
|
||||
<meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link type="text/css" href="[#]filepath_css[#]" rel="stylesheet" media="all" />
|
||||
<script type="text/javascript" src="[#]filepath_js_d3[#]"></script>
|
||||
<script type="text/javascript" src="[#]filepath_js_leaflet[#]"></script>
|
||||
<script type="text/javascript" src="[#]filepath_js_jquery[#]"></script>
|
||||
<script type="text/javascript" src="[#]filepath_js_jquery_mods[#]"></script>
|
||||
<script type="text/javascript" src="[#]filepath_js_spot[#]"></script>
|
||||
<script type="text/javascript" src="[#]filepath_js_lightbox[#]"></script>
|
||||
<script type="text/javascript">
|
||||
var oSpot = new Spot([#]GLOBAL_VARS[#]);
|
||||
$(document).ready(oSpot.init);
|
||||
</script>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="main"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
<div id="upload">
|
||||
<a name="back" class="button" href="[#]host_url[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
|
||||
<h1>[#]lang:upload_title[#]</h1>
|
||||
<input id="fileupload" type="file" name="files[]" multiple>
|
||||
<div id="progress">
|
||||
<div class="bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div id="comments"></div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
oSpot.pageInit = function(asHash) {
|
||||
var asProject = self.vars(['projects', self.vars('default_project_codename')]);
|
||||
self.tmp('status-box', $('#status'));
|
||||
if(asProject.editable) {
|
||||
$('#fileupload')
|
||||
.attr('data-url', self.getActionLink('upload'))
|
||||
.fileupload({
|
||||
dataType: 'json',
|
||||
formData: {t: self.consts.timezone},
|
||||
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
|
||||
done: function (e, asData) {
|
||||
$.each(asData.result.files, function(iKey, oFile) {
|
||||
var bError = ('error' in oFile);
|
||||
|
||||
//Feedback
|
||||
addStatus(bError?oFile.error:(self.lang('upload_success', [oFile.name])));
|
||||
|
||||
//Comments
|
||||
if(!bError) addCommentBox(oFile.id, oFile.thumbnail);
|
||||
});
|
||||
},
|
||||
progressall: function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#progress .bar').css('width', progress+'%');
|
||||
}
|
||||
});
|
||||
}
|
||||
else addStatus(self.lang('upload_mode_archived', [asProject.name]), true);
|
||||
};
|
||||
|
||||
function addCommentBox(iMediaId, sThumbnailPath) {
|
||||
$('#comments').append($('<div>', {'class':'comment'})
|
||||
.append($('<img>', {'class':'thumb', 'src':sThumbnailPath}))
|
||||
.append($('<form>')
|
||||
.append($('<input>', {'class':'content', 'name':'content', 'type':'text'}))
|
||||
.append($('<input>', {'class':'id', 'name':'id', 'type':'hidden', 'value':iMediaId}))
|
||||
.append($('<button>', {'class':'save', 'type':'button'})
|
||||
.click(function(){
|
||||
var $Form = $(this).parent();
|
||||
oSpot.get(
|
||||
'add_comment',
|
||||
function(asData){addStatus(self.lang('media_comment_update', asData.filename));},
|
||||
{id:$Form.find('.id').val(), content:$Form.find('.content').val()},
|
||||
function(sMsgId){addStatus(self.lang(sMsgId));},
|
||||
);
|
||||
})
|
||||
.text(self.lang('save'))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function addStatus(sMsg, bClear) {
|
||||
bClear = bClear || false;
|
||||
if(bClear) self.tmp('status-box').empty();
|
||||
|
||||
self.tmp('status-box').append($('<p>').text(sMsg));
|
||||
}
|
||||
</script>
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "livetrail",
|
||||
"description": "Self-hosted live journey map combining SPOT GPS tracking, GPX routes, posts, and media",
|
||||
"version": "3.0.1",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite build --mode development --watch",
|
||||
"prod": "vite build",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Franzz",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"eslint": "^10.9.1",
|
||||
"eslint-plugin-vue": "^10.10.0",
|
||||
"globals": "^17.11.0",
|
||||
"vite": "^8.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.2.0",
|
||||
"@uppy/core": "^6.0.0",
|
||||
"@uppy/xhr-upload": "^6.0.0",
|
||||
"autosize": "^6.0.1",
|
||||
"maplibre-gl": "^6.12.0",
|
||||
"sass": "^1.97.2",
|
||||
"simplebar-vue": "^2.3.3",
|
||||
"vue": "^3.5.40"
|
||||
},
|
||||
"allowScripts": {
|
||||
"@parcel/watcher@2.6.0": true,
|
||||
"vue-demi@0.13.11": true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
use Franzz\Livetrail\Controller;
|
||||
|
||||
echo (new Controller())->handle(__FILE__, $argv ?? []);
|
||||
@@ -1,43 +1,28 @@
|
||||
# Live Trail Project
|
||||
|
||||
Live Trail is a self-hosted web application for sharing journeys on an interactive map. It combines live GPS positions from a [SPOT](https://www.findmespot.com) tracker with GPX routes, posts, photos, videos, and a chronological activity feed, supporting both real-time tracking and historical travel stories.
|
||||
|
||||
# Spot Project
|
||||
[Spot](https://www.findmespot.com) & GPX integration
|
||||
## Dependencies
|
||||
|
||||
* nodejs 24+
|
||||
* composer
|
||||
* php-mbstring
|
||||
* php-imagick
|
||||
* php-gd
|
||||
* php-mysql
|
||||
* php-xml
|
||||
* php-exif
|
||||
* ffmpeg
|
||||
* ffprobe (ffmpeg)
|
||||
* STARTTLS Email Server (use Gmail if none available)
|
||||
* Optional: Geo Caching Server (WMTS Caching Service)
|
||||
|
||||
## PHP Configuration
|
||||
|
||||
* max_execution_time = 300
|
||||
* memory_limit = 500M
|
||||
* post_max_size = 4G
|
||||
* upload_max_filesize = 4G
|
||||
* max_file_uploads = 50
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Clone Git onto web server
|
||||
2. Update php.ini parameters
|
||||
3. Copy timezone data: mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql
|
||||
4. Copy settings-sample.php to settings.php and populate
|
||||
5. Optional: if testing locally, copy one of the config/*.conf files to apache/nginx site folder
|
||||
6. Follow CI/CD script in .gitea/workflows/deploy.yml
|
||||
7. Add a GPX file named <project_codename>.gpx to /resources/geo/
|
||||
8. Go to #admin and create a new project (with code name = <project_codename>) & feed
|
||||
|
||||
2. Copy settings-sample.php to settings.php and populate
|
||||
3. Go to #admin and create a new project, feed & maps
|
||||
4. Add a GPX file named <project_codename>.gpx to /geo/
|
||||
## To Do List
|
||||
|
||||
* ECMA import/export
|
||||
* Reset zoom on image closing (lightbox)
|
||||
* Add mail frequency slider
|
||||
* Use WMTS servers directly when not using Geo Caching Server
|
||||
* Allow HEIF picture format
|
||||
* Garmin InReach Integration
|
||||
* Vector tiles support (https://www.arcgis.com/home/item.html?id=7dc6cea0b1764a1f9af2e679f642f0f5) + Use of GL library. Use Mapbox GL JS / Maplibre GL JS / ESRI-Leaflet-vector?
|
||||
@@ -1,237 +0,0 @@
|
||||
{
|
||||
"action": {
|
||||
"back": "Back",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"send": "Send"
|
||||
},
|
||||
"account": {
|
||||
"login": "Log in",
|
||||
"logout": "Log out",
|
||||
"confirm_password": "Confirm password",
|
||||
"email_placeholder": "my@email.com",
|
||||
"invalid_credentials": "Incorrect password",
|
||||
"invalid_email": "This doesn't look like a valid email address",
|
||||
"logged_in": "Logged in",
|
||||
"logged_out": "Logged out",
|
||||
"newsletter": "Email me new updates",
|
||||
"password": "Password",
|
||||
"password_mismatch": "The passwords don't match",
|
||||
"password_required": "Enter your password to continue",
|
||||
"password_set": "Your password has been set successfully.",
|
||||
"set_password": "Set a password for this administrator account",
|
||||
"subscribed": "Thanks! You'll receive a confirmation email shortly.",
|
||||
"unsubscribed": "Done. No more junk mail from me.",
|
||||
"title": "Account",
|
||||
"status": {
|
||||
"logged_in": "You're logged in.",
|
||||
"logged_in_named": "Hello $0, you're logged in.",
|
||||
"logged_out": "Enter your email address below to receive my position as soon as I find it :)",
|
||||
"subscribed": "You're all set. I'll send you updates!"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"config": "Settings",
|
||||
"create_success": "Created",
|
||||
"delete_success": "Deleted",
|
||||
"save_success": "Saved",
|
||||
"title": "Admin Panel",
|
||||
"toolbox": "Toolbox",
|
||||
"upload": "Upload"
|
||||
},
|
||||
"credits": {
|
||||
"git": "Git repository",
|
||||
"license": "licensed under GPLv3",
|
||||
"project": "$0 Project"
|
||||
},
|
||||
"email": {
|
||||
"confirmation": {
|
||||
"body_1": "Thank you for following my wanderings :). I'll make sure to keep you posted on my progress along the trail.",
|
||||
"body_2": "I usually check in once a day, and sometimes at special moments too, like successful peak ascents. I'm using a GPS-based device (PLB), which doesn't require phone reception to work. So messages should be fairly frequent, but — being awestruck by the beauty of nature — I might also just forget to send a signal once in a while. So don't worry if you don't receive anything for a couple of days.",
|
||||
"body_3": "If I've posted any pictures recently, you'll also find them in the same email.",
|
||||
"conclusion": "See you down the road!",
|
||||
"preheader": "Thanks for keeping in touch!",
|
||||
"signature": "--François",
|
||||
"subject": "Registration confirmed",
|
||||
"thanks_subject": "You're all set!"
|
||||
},
|
||||
"failure": "Failed to send the update email",
|
||||
"sent": "Update email sent",
|
||||
"unsubscribe": "PS: Changed your mind?",
|
||||
"unsubscribe_button": "Unsubscribe",
|
||||
"update": {
|
||||
"latest_news": "Latest news:",
|
||||
"preheader": "New position received",
|
||||
"subject": "Spotted!",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"commit_db": "Error committing to database",
|
||||
"file_missing": "$0 file \"$1\" is missing",
|
||||
"impossible_value": "Value \"$0\" is not valid for field \"$1\"",
|
||||
"no_auth": "Not authorized",
|
||||
"not_found": "Unknown action",
|
||||
"unknown_field": "Unknown field \"$0\"",
|
||||
"unknown_type": "Unknown type \"$0\""
|
||||
},
|
||||
"feed": {
|
||||
"counter": "#$0",
|
||||
"id": "Feed ID",
|
||||
"last_update": "Last SPOT update",
|
||||
"name": "Name",
|
||||
"new": "New feed",
|
||||
"plural": "Feeds",
|
||||
"ref_id": "Feed reference ID",
|
||||
"status": "Status"
|
||||
},
|
||||
"map": {
|
||||
"ign_france": "IGN (France)",
|
||||
"ign_spain": "IGN (Spain)",
|
||||
"linz": "LINZ",
|
||||
"natgeo": "National Geographic",
|
||||
"otm": "Open Topo Map",
|
||||
"outdoors": "Mapbox Outdoors",
|
||||
"satellite": "Satellite",
|
||||
"see_on_google": "View position on Google Maps",
|
||||
"terrain": "3D terrain",
|
||||
"title": "Base maps",
|
||||
"usgs": "USGS"
|
||||
},
|
||||
"media": {
|
||||
"click_watch": "Click to watch the video",
|
||||
"click_zoom": "Click to zoom",
|
||||
"comment_update": "Comment for media \"$0\" updated",
|
||||
"image": "Picture",
|
||||
"image_taken_on": "Taken on $0",
|
||||
"images": "Pictures",
|
||||
"nearby": "Nearby pictures",
|
||||
"no_id": "Missing media ID in request",
|
||||
"posted_on": "Posted on $0",
|
||||
"video": "Video",
|
||||
"video_taken_on": "Shot on $0"
|
||||
},
|
||||
"meta": {
|
||||
"locale": "en_NZ",
|
||||
"page_og_desc": "Stay in touch while I'm away hiking."
|
||||
},
|
||||
"post": {
|
||||
"copy_to_clipboard": "Copy direct link to clipboard",
|
||||
"link_copied": "Link copied!",
|
||||
"message": "Message",
|
||||
"name": "Name",
|
||||
"new_message": "New message",
|
||||
"reply": "Reply to this message"
|
||||
},
|
||||
"project": {
|
||||
"wip": "In progress",
|
||||
"code_name": "Code name",
|
||||
"end": "End",
|
||||
"hikes": "Hikes",
|
||||
"id": "Project ID",
|
||||
"mode": "Mode",
|
||||
"modes": {
|
||||
"blog": "Active project",
|
||||
"histo": "Archived project",
|
||||
"previz": "Project in preparation"
|
||||
},
|
||||
"new": "New project",
|
||||
"overview": "Overview",
|
||||
"plural": "Projects",
|
||||
"single": "Project",
|
||||
"start": "Start",
|
||||
"update_messages": "Update project messages"
|
||||
},
|
||||
"spot": {
|
||||
"id": "Spot ID",
|
||||
"model": "Model",
|
||||
"name": "Spot name",
|
||||
"no_new_msg": "No new messages",
|
||||
"plural": "Spots",
|
||||
"ref_id": "Ref. Spot ID"
|
||||
},
|
||||
"stats": {
|
||||
"duration": "Duration",
|
||||
"distance": "Distance",
|
||||
"elevation": "Elevation",
|
||||
"elevation_gain": "Elevation gain",
|
||||
"elevation_loss": "Elevation loss",
|
||||
"from": "Start",
|
||||
"legend": "Legend",
|
||||
"segment_length": "Segment length",
|
||||
"to": "Finish",
|
||||
"type": "Track type"
|
||||
},
|
||||
"time": {
|
||||
"city": "$0 time",
|
||||
"date_time": "$0 at $1",
|
||||
"local": "$0 local time",
|
||||
"user": "$0 your time",
|
||||
"zone": "Time zone"
|
||||
},
|
||||
"track": {
|
||||
"download": "Download GPX track",
|
||||
"hitchhiking": "Hitchhiking",
|
||||
"main": "Main track",
|
||||
"off-track": "Off-track"
|
||||
},
|
||||
"unit": {
|
||||
"day": "day",
|
||||
"day_short": "D",
|
||||
"days": "days",
|
||||
"hour": "h"
|
||||
},
|
||||
"upload": {
|
||||
"error": "Upload failed",
|
||||
"media": {
|
||||
"exists": "Picture $0 already exists",
|
||||
"title": "Picture and video uploads"
|
||||
},
|
||||
"mode_archived": "Project \"$0\" is archived. Uploads are not allowed.",
|
||||
"position": {
|
||||
"determining": "Determining position…",
|
||||
"error": "Unable to determine position",
|
||||
"new": "New position",
|
||||
"permission_denied": "Location permission was denied",
|
||||
"sending": "Sending position…",
|
||||
"success": "Position uploaded successfully",
|
||||
"timeout": "Position request timed out",
|
||||
"title": "Add position",
|
||||
"unavailable": "Position is unavailable",
|
||||
"unsupported": "This browser does not support geolocation"
|
||||
},
|
||||
"success": "$0 uploaded successfully"
|
||||
},
|
||||
"user": {
|
||||
"subscribed": "Subscribed users",
|
||||
"clearance": "Clearance",
|
||||
"email": "Email",
|
||||
"id": "User ID",
|
||||
"language": "Language",
|
||||
"name": "User name"
|
||||
},
|
||||
"weather": {
|
||||
"clear-day": "Cloud cover is less than 20% during the day",
|
||||
"clear-night": "Cloud cover is less than 20% during the night",
|
||||
"cloudy": "Cloud cover is greater than 90%",
|
||||
"fog": "Visibility is low (less than 1km)",
|
||||
"hail": "Hail showers",
|
||||
"partly-cloudy-day": "Cloud cover is greater than 20% during the day",
|
||||
"partly-cloudy-night": "Cloud cover is greater than 20% during the night",
|
||||
"rain": "Rainfall is greater than zero",
|
||||
"rain-snow": "Snow and rain showers",
|
||||
"rain-snow-showers-day": "Possible rain/snow throughout the day",
|
||||
"rain-snow-showers-night": "Possible rain/snow throughout the night",
|
||||
"showers-day": "Rain showers during the day",
|
||||
"showers-night": "Rain showers during the night",
|
||||
"sleet": "Sleet",
|
||||
"snow": "Snowfall is greater than zero",
|
||||
"snow-showers-day": "Periods of snow during the day",
|
||||
"snow-showers-night": "Periods of snow during the night",
|
||||
"thunder": "Thunderstorms",
|
||||
"thunder-rain": "Thunderstorms throughout the day or night",
|
||||
"thunder-showers-day": "Possible thunderstorms throughout the day",
|
||||
"thunder-showers-night": "Possible thunderstorms throughout the night",
|
||||
"wind": "Wind speed is high (greater than 30 km/h)"
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
{
|
||||
"action": {
|
||||
"back": "Atrás",
|
||||
"delete": "Borrar",
|
||||
"save": "Guardar",
|
||||
"send": "Enviar"
|
||||
},
|
||||
"account": {
|
||||
"login": "Iniciar sesión",
|
||||
"logout": "Cerrar sesión",
|
||||
"confirm_password": "Confirmar contraseña",
|
||||
"email_placeholder": "nombre@email.com",
|
||||
"invalid_credentials": "Contraseña incorrecta",
|
||||
"invalid_email": "Esto no parece una dirección de correo electrónico",
|
||||
"logged_in": "Sesión iniciada",
|
||||
"logged_out": "Sesión cerrada",
|
||||
"newsletter": "Enviarme nuevas actualizaciones por correo",
|
||||
"password": "Contraseña",
|
||||
"password_mismatch": "Las contraseñas no coinciden",
|
||||
"password_required": "Introduce tu contraseña para continuar",
|
||||
"password_set": "Tu contraseña se ha establecido correctamente.",
|
||||
"set_password": "Establece una contraseña para esta cuenta administradora",
|
||||
"subscribed": "¡Gracias! Recibirás un correo electrónico de confirmación.",
|
||||
"unsubscribed": "Listo. ¡No más spam!",
|
||||
"title": "Cuenta",
|
||||
"status": {
|
||||
"logged_in": "Has iniciado sesión.",
|
||||
"logged_in_named": "Hola $0, has iniciado sesión.",
|
||||
"logged_out": "Añade tu dirección de correo electrónico y te enviaré mi posición actualizada tan pronto como la reciba :)",
|
||||
"subscribed": "Todo está listo. Te enviaré noticias frescas en cuanto las reciba. Prometido..."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"config": "Configuración",
|
||||
"create_success": "Creado",
|
||||
"delete_success": "Eliminado",
|
||||
"save_success": "Guardado",
|
||||
"title": "Administración",
|
||||
"toolbox": "Herramientas",
|
||||
"upload": "Cargar"
|
||||
},
|
||||
"credits": {
|
||||
"git": "Repositorio de Git",
|
||||
"license": "bajo licencia GPLv3",
|
||||
"project": "Proyecto $0"
|
||||
},
|
||||
"email": {
|
||||
"confirmation": {
|
||||
"body_1": "Te agradezco mucho que sigas mi proyecto y que te intereses por su evolución. Te prometo que te mantendré informado sobre mi progreso.",
|
||||
"body_2": "Normalmente envío un mensaje una vez al día. Cuando voy a lugares especiales, envío uno extra (cimas, ese tipo de cosas). Uso un dispositivo GPS para enviar la señal, así que no necesito cobertura telefónica para que funcione. Sin embargo, puede haber ocasiones en las que no presione el botón. Por lo tanto, no te preocupes si no recibes mensajes durante uno o dos días.",
|
||||
"body_3": "Cuando añada fotos a la página, también las encontrarás en este correo electrónico.",
|
||||
"conclusion": "¡Nos vemos en el camino!",
|
||||
"preheader": "¡Gracias por mantenerte en contacto!",
|
||||
"signature": "--François",
|
||||
"subject": "Confirmación",
|
||||
"thanks_subject": "¡Hecho!"
|
||||
},
|
||||
"failure": "No se pudo enviar el correo de actualización",
|
||||
"sent": "Correo de actualización enviado",
|
||||
"unsubscribe": "PD: ¿Demasiados correos electrónicos?",
|
||||
"unsubscribe_button": "Darse de baja",
|
||||
"update": {
|
||||
"latest_news": "Últimas noticias:",
|
||||
"preheader": "¡Nueva posición!",
|
||||
"subject": "Nueva posición recibida",
|
||||
"title": "Mensaje"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"commit_db": "Error SQL",
|
||||
"file_missing": "Falta el archivo $0 \"$1\"",
|
||||
"impossible_value": "El valor \"$0\" no es posible para el campo \"$1\"",
|
||||
"no_auth": "Sin autorización",
|
||||
"not_found": "Acción desconocida",
|
||||
"unknown_field": "Campo \"$0\" desconocido",
|
||||
"unknown_type": "Tipo \"$0\" desconocido"
|
||||
},
|
||||
"feed": {
|
||||
"counter": "N.º $0",
|
||||
"id": "ID Feed",
|
||||
"last_update": "Última actualización de SPOT",
|
||||
"name": "Descripción",
|
||||
"new": "Nuevo feed",
|
||||
"plural": "Feeds",
|
||||
"ref_id": "ID Feed ref.",
|
||||
"status": "Estado"
|
||||
},
|
||||
"map": {
|
||||
"ign_france": "IGN (Francia)",
|
||||
"ign_spain": "IGN (España)",
|
||||
"linz": "LINZ",
|
||||
"natgeo": "National Geographic",
|
||||
"otm": "Open Topo Map",
|
||||
"outdoors": "Mapbox Topo",
|
||||
"satellite": "Satélite",
|
||||
"see_on_google": "Ver la posición en Google Maps",
|
||||
"terrain": "Relieve 3D",
|
||||
"title": "Mapas de base",
|
||||
"usgs": "USGS"
|
||||
},
|
||||
"media": {
|
||||
"click_watch": "Haz clic para ver el vídeo",
|
||||
"click_zoom": "Haz clic para ampliar",
|
||||
"comment_update": "Comentario \"$0\" actualizado",
|
||||
"image": "Foto",
|
||||
"image_taken_on": "Foto tomada el $0",
|
||||
"images": "Fotos",
|
||||
"nearby": "Fotos cercanas",
|
||||
"no_id": "Falta el ID del archivo multimedia",
|
||||
"posted_on": "Añadido el $0",
|
||||
"video": "Vídeo",
|
||||
"video_taken_on": "Vídeo grabado el $0"
|
||||
},
|
||||
"meta": {
|
||||
"locale": "es_ES",
|
||||
"page_og_desc": "Mantente en contacto conmigo durante mis aventuras en la montaña."
|
||||
},
|
||||
"post": {
|
||||
"copy_to_clipboard": "Copiar el enlace",
|
||||
"link_copied": "¡Enlace copiado!",
|
||||
"message": "Mensaje",
|
||||
"name": "Nombre",
|
||||
"new_message": "Mensaje nuevo",
|
||||
"reply": "Responder a este mensaje"
|
||||
},
|
||||
"project": {
|
||||
"wip": "En curso",
|
||||
"code_name": "Nombre clave",
|
||||
"end": "Fin",
|
||||
"hikes": "Senderos",
|
||||
"id": "ID del proyecto",
|
||||
"mode": "Modo",
|
||||
"modes": {
|
||||
"blog": "Proyecto activo",
|
||||
"histo": "Proyecto archivado",
|
||||
"previz": "Proyecto en preparación"
|
||||
},
|
||||
"new": "Nuevo proyecto",
|
||||
"overview": "Vista general",
|
||||
"plural": "Proyectos",
|
||||
"single": "Proyecto",
|
||||
"start": "Inicio",
|
||||
"update_messages": "Actualizar los mensajes del proyecto"
|
||||
},
|
||||
"spot": {
|
||||
"id": "ID de Spot",
|
||||
"model": "Modelo",
|
||||
"name": "Spot",
|
||||
"no_new_msg": "No hay mensajes nuevos",
|
||||
"plural": "Spots",
|
||||
"ref_id": "ID de referencia de Spot"
|
||||
},
|
||||
"stats": {
|
||||
"duration": "Duración",
|
||||
"distance": "Distancia",
|
||||
"elevation": "Elevación",
|
||||
"elevation_gain": "Ascenso acumulado",
|
||||
"elevation_loss": "Descenso acumulado",
|
||||
"from": "Inicio",
|
||||
"legend": "Leyenda",
|
||||
"segment_length": "Tamaño del segmento",
|
||||
"to": "Fin",
|
||||
"type": "Tipo de sendero"
|
||||
},
|
||||
"time": {
|
||||
"city": "Hora en $0",
|
||||
"date_time": "$0 a las $1",
|
||||
"local": "$0 hora local",
|
||||
"user": "$0 en tu zona horaria",
|
||||
"zone": "Huso horario"
|
||||
},
|
||||
"track": {
|
||||
"download": "Descargar la ruta GPX",
|
||||
"hitchhiking": "Autostop",
|
||||
"main": "Camino principal",
|
||||
"off-track": "Variante"
|
||||
},
|
||||
"unit": {
|
||||
"day": "día",
|
||||
"day_short": "D",
|
||||
"days": "días",
|
||||
"hour": "h"
|
||||
},
|
||||
"upload": {
|
||||
"error": "Error al subir el archivo",
|
||||
"media": {
|
||||
"exists": "La imagen $0 ya existe",
|
||||
"title": "Cargar fotos y vídeos"
|
||||
},
|
||||
"mode_archived": "El proyecto \"$0\" está archivado. No se puede cargar.",
|
||||
"position": {
|
||||
"determining": "Determinando la posición…",
|
||||
"error": "No se puede determinar la posición",
|
||||
"new": "Nueva posición",
|
||||
"permission_denied": "Se ha denegado el permiso de ubicación",
|
||||
"sending": "Enviando la posición…",
|
||||
"success": "Posición subida correctamente",
|
||||
"timeout": "La solicitud de posición ha agotado el tiempo de espera",
|
||||
"title": "Subir posición",
|
||||
"unavailable": "La posición no está disponible",
|
||||
"unsupported": "Este navegador no admite la geolocalización"
|
||||
},
|
||||
"success": "$0 se ha subido correctamente."
|
||||
},
|
||||
"user": {
|
||||
"subscribed": "Usuarios suscritos",
|
||||
"clearance": "Nivel de autorización",
|
||||
"email": "Correo electrónico",
|
||||
"id": "ID del usuario",
|
||||
"language": "Idioma",
|
||||
"name": "Nombre"
|
||||
},
|
||||
"weather": {
|
||||
"clear-day": "La nubosidad es inferior al 20 % durante el día",
|
||||
"clear-night": "La nubosidad es inferior al 20 % durante la noche",
|
||||
"cloudy": "La nubosidad es superior al 90 %",
|
||||
"fog": "La visibilidad es baja (menos de 1km)",
|
||||
"hail": "Chubascos de granizo",
|
||||
"partly-cloudy-day": "La nubosidad es superior al 20 % durante el día",
|
||||
"partly-cloudy-night": "La nubosidad es superior al 20 % durante la noche",
|
||||
"rain": "La cantidad de lluvia es superior a cero",
|
||||
"rain-snow": "Chubascos de nieve y lluvia",
|
||||
"rain-snow-showers-day": "Posible lluvia/nieve durante todo el día",
|
||||
"rain-snow-showers-night": "Posible lluvia/nieve durante toda la noche",
|
||||
"showers-day": "Chubascos de lluvia durante el día",
|
||||
"showers-night": "Chubascos de lluvia durante la noche",
|
||||
"sleet": "Aguanieve",
|
||||
"snow": "La cantidad de nieve es superior a cero",
|
||||
"snow-showers-day": "Periodos de nieve durante el día",
|
||||
"snow-showers-night": "Periodos de nieve durante la noche",
|
||||
"thunder": "Tormentas",
|
||||
"thunder-rain": "Tormentas durante el día o la noche",
|
||||
"thunder-showers-day": "Posibles tormentas durante todo el día",
|
||||
"thunder-showers-night": "Posibles tormentas durante toda la noche",
|
||||
"wind": "La velocidad del viento es alta (más de 30 km/h)"
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
{
|
||||
"action": {
|
||||
"back": "Retour",
|
||||
"delete": "Supprimer",
|
||||
"save": "Sauvegarder",
|
||||
"send": "Envoyer"
|
||||
},
|
||||
"account": {
|
||||
"login": "Se connecter",
|
||||
"logout": "Se déconnecter",
|
||||
"confirm_password": "Confirmer le mot de passe",
|
||||
"email_placeholder": "mon@email.com",
|
||||
"invalid_credentials": "Mot de passe incorrect",
|
||||
"invalid_email": "Ceci ne ressemble pas à une adresse e-mail",
|
||||
"logged_in": "Connexion réussie",
|
||||
"logged_out": "Déconnexion réussie",
|
||||
"newsletter": "M'envoyer les nouvelles mises à jour par e-mail",
|
||||
"password": "Mot de passe",
|
||||
"password_mismatch": "Les mots de passe ne correspondent pas",
|
||||
"password_required": "Saisis ton mot de passe pour continuer",
|
||||
"password_set": "Ton mot de passe a bien été défini.",
|
||||
"set_password": "Définis un mot de passe pour ce compte administrateur",
|
||||
"subscribed": "Merci ! Tu vas recevoir un e-mail de confirmation très bientôt.",
|
||||
"unsubscribed": "C'est fait. Fini le spam !",
|
||||
"title": "Compte",
|
||||
"status": {
|
||||
"logged_in": "Tu es connecté.",
|
||||
"logged_in_named": "Bonjour $0, tu es connecté.",
|
||||
"logged_out": "Ajoute ton adresse e-mail et je t'enverrai ma nouvelle position dès que je la trouve :)",
|
||||
"subscribed": "C'est tout bon. Je t'enverrai des nouvelles fraîches. Parole de scout."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"config": "Paramètres",
|
||||
"create_success": "Créé",
|
||||
"delete_success": "Supprimé",
|
||||
"save_success": "Sauvegardé",
|
||||
"title": "Administration",
|
||||
"toolbox": "Boîte à outils",
|
||||
"upload": "Téléverser"
|
||||
},
|
||||
"credits": {
|
||||
"git": "Dépôt Git",
|
||||
"license": "sous licence GPLv3",
|
||||
"project": "Projet $0"
|
||||
},
|
||||
"email": {
|
||||
"confirmation": {
|
||||
"body_1": "C'est gentil de venir voir où j'en suis. Promis, je te tiendrai au courant de mon avancée.",
|
||||
"body_2": "En général, j'envoie un message une fois par jour. Lorsque je passe par des endroits sympas, j'en envoie un supplémentaire (sommets, ce genre de choses). J'utilise une balise GPS pour envoyer le signal, je n'ai donc pas besoin de réseau téléphonique pour que cela fonctionne. Cependant, il peut m'arriver d'oublier d'appuyer sur le bouton. Donc pas de raison de t'inquiéter si tu ne reçois pas de messages pendant une journée ou deux.",
|
||||
"body_3": "Si j'ai ajouté des photos sur le site récemment, tu devrais aussi les retrouver dans cet e-mail.",
|
||||
"conclusion": "À bientôt sur les chemins !",
|
||||
"preheader": "Merci de rester en contact !",
|
||||
"signature": "--François",
|
||||
"subject": "Confirmation",
|
||||
"thanks_subject": "C'est tout bon !"
|
||||
},
|
||||
"failure": "Échec de l’envoi de l’e-mail de mise à jour",
|
||||
"sent": "E-mail de mise à jour envoyé",
|
||||
"unsubscribe": "PS : Trop d'e-mails ?",
|
||||
"unsubscribe_button": "Se désinscrire",
|
||||
"update": {
|
||||
"latest_news": "Dernières nouvelles :",
|
||||
"preheader": "Nouvelle position !",
|
||||
"subject": "Nouvelle position reçue",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"commit_db": "Erreur lors de la requête SQL",
|
||||
"file_missing": "Le fichier $0 \"$1\" est manquant",
|
||||
"impossible_value": "La valeur \"$0\" n'est pas possible pour le champ \"$1\"",
|
||||
"no_auth": "Pas d'autorisation",
|
||||
"not_found": "Action inconnue",
|
||||
"unknown_field": "Champ \"$0\" inconnu",
|
||||
"unknown_type": "Type \"$0\" inconnu"
|
||||
},
|
||||
"feed": {
|
||||
"counter": "N°$0",
|
||||
"id": "ID feed",
|
||||
"last_update": "Dernière vérification de SPOT",
|
||||
"name": "Description",
|
||||
"new": "Nouveau feed",
|
||||
"plural": "Feeds",
|
||||
"ref_id": "ID Feed ref.",
|
||||
"status": "Statut"
|
||||
},
|
||||
"map": {
|
||||
"ign_france": "IGN (France)",
|
||||
"ign_spain": "IGN (Espagne)",
|
||||
"linz": "LINZ",
|
||||
"natgeo": "National Geographic",
|
||||
"otm": "Open Topo Map",
|
||||
"outdoors": "Mapbox Topo",
|
||||
"satellite": "Satellite",
|
||||
"see_on_google": "Voir la position sur Google Maps",
|
||||
"terrain": "Relief 3D",
|
||||
"title": "Fonds de carte",
|
||||
"usgs": "USGS"
|
||||
},
|
||||
"media": {
|
||||
"click_watch": "Cliquer pour voir la vidéo",
|
||||
"click_zoom": "Cliquer pour zoomer",
|
||||
"comment_update": "Commentaire du média \"$0\" mis à jour",
|
||||
"image": "Photo",
|
||||
"image_taken_on": "Prise le $0",
|
||||
"images": "Photos",
|
||||
"nearby": "Photos prises dans le coin",
|
||||
"no_id": "ID du média manquant",
|
||||
"posted_on": "Ajouté le $0",
|
||||
"video": "Vidéo",
|
||||
"video_taken_on": "Filmé le $0"
|
||||
},
|
||||
"meta": {
|
||||
"locale": "fr_CH",
|
||||
"page_og_desc": "Garde le contact lorsque je suis sur les chemins."
|
||||
},
|
||||
"post": {
|
||||
"copy_to_clipboard": "Copier le lien dans le presse-papiers",
|
||||
"link_copied": "Lien copié !",
|
||||
"message": "Message",
|
||||
"name": "Nom",
|
||||
"new_message": "Nouveau message",
|
||||
"reply": "Répondre à ce message"
|
||||
},
|
||||
"project": {
|
||||
"wip": "En cours",
|
||||
"code_name": "Nom de code",
|
||||
"end": "Arrivée",
|
||||
"hikes": "Randonnées",
|
||||
"id": "ID projet",
|
||||
"mode": "Mode",
|
||||
"modes": {
|
||||
"blog": "Projet actif",
|
||||
"histo": "Projet archivé",
|
||||
"previz": "Projet en cours de préparation"
|
||||
},
|
||||
"new": "Nouveau projet",
|
||||
"overview": "Vue d'ensemble",
|
||||
"plural": "Projets",
|
||||
"single": "Projet",
|
||||
"start": "Départ",
|
||||
"update_messages": "Mettre à jour les messages du projet"
|
||||
},
|
||||
"spot": {
|
||||
"id": "ID Spot",
|
||||
"model": "Modèle",
|
||||
"name": "Spot",
|
||||
"no_new_msg": "Aucun nouveau message",
|
||||
"plural": "Spots",
|
||||
"ref_id": "ID Spot ref."
|
||||
},
|
||||
"stats": {
|
||||
"duration": "Durée",
|
||||
"distance": "Distance",
|
||||
"elevation": "Dénivelé",
|
||||
"elevation_gain": "Dénivelé positif",
|
||||
"elevation_loss": "Dénivelé négatif",
|
||||
"from": "Départ",
|
||||
"legend": "Légende",
|
||||
"segment_length": "Taille du segment",
|
||||
"to": "Arrivée",
|
||||
"type": "Type de rando"
|
||||
},
|
||||
"time": {
|
||||
"city": "Heure à $0",
|
||||
"date_time": "$0 à $1",
|
||||
"local": "$0 heure locale",
|
||||
"user": "$0 dans ton fuseau horaire",
|
||||
"zone": "Fuseau horaire"
|
||||
},
|
||||
"track": {
|
||||
"download": "Télécharger la trace GPX",
|
||||
"hitchhiking": "Auto-stop",
|
||||
"main": "Itinéraire",
|
||||
"off-track": "Variante"
|
||||
},
|
||||
"unit": {
|
||||
"day": "jour",
|
||||
"day_short": "J",
|
||||
"days": "jours",
|
||||
"hour": "h"
|
||||
},
|
||||
"upload": {
|
||||
"error": "Erreur lors du téléversement",
|
||||
"media": {
|
||||
"exists": "L'image $0 existe déjà",
|
||||
"title": "Téléverser des photos et vidéos"
|
||||
},
|
||||
"mode_archived": "Le projet \"$0\" a été archivé. Aucun téléversement possible.",
|
||||
"position": {
|
||||
"determining": "Détermination de la position…",
|
||||
"error": "Impossible de déterminer la position",
|
||||
"new": "Nouvelle position",
|
||||
"permission_denied": "L’autorisation d’accéder à la position a été refusée",
|
||||
"sending": "Envoi de la position…",
|
||||
"success": "Position téléversée avec succès",
|
||||
"timeout": "La demande de position a expiré",
|
||||
"title": "Position supplémentaire",
|
||||
"unavailable": "La position n’est pas disponible",
|
||||
"unsupported": "Ce navigateur ne prend pas en charge la géolocalisation"
|
||||
},
|
||||
"success": "$0 a été téléversé"
|
||||
},
|
||||
"user": {
|
||||
"subscribed": "Utilisateurs abonnés",
|
||||
"clearance": "Niveau d'autorisation",
|
||||
"email": "E-mail",
|
||||
"id": "ID utilisateur",
|
||||
"language": "Langue",
|
||||
"name": "Nom"
|
||||
},
|
||||
"weather": {
|
||||
"clear-day": "La couverture nuageuse est inférieure à 20 % pendant la journée",
|
||||
"clear-night": "La couverture nuageuse est inférieure à 20 % pendant la nuit",
|
||||
"cloudy": "La couverture nuageuse est supérieure à 90 %",
|
||||
"fog": "La visibilité est faible (moins de 1km)",
|
||||
"hail": "Averses de grêle",
|
||||
"partly-cloudy-day": "La couverture nuageuse est supérieure à 20 % pendant la journée",
|
||||
"partly-cloudy-night": "La couverture nuageuse est supérieure à 20 % pendant la nuit",
|
||||
"rain": "La quantité de pluie est supérieure à zéro",
|
||||
"rain-snow": "Averses de neige et de pluie",
|
||||
"rain-snow-showers-day": "Pluie/neige possible tout au long de la journée",
|
||||
"rain-snow-showers-night": "Pluie/neige possible tout au long de la nuit",
|
||||
"showers-day": "Averses de pluie pendant la journée",
|
||||
"showers-night": "Averses de pluie pendant la nuit",
|
||||
"sleet": "Grésil",
|
||||
"snow": "La quantité de neige est supérieure à zéro",
|
||||
"snow-showers-day": "Périodes de neige pendant la journée",
|
||||
"snow-showers-night": "Périodes de neige pendant la nuit",
|
||||
"thunder": "Orages",
|
||||
"thunder-rain": "Orages tout au long de la journée ou de la nuit",
|
||||
"thunder-showers-day": "Orages possibles tout au long de la journée",
|
||||
"thunder-showers-night": "Orages possibles tout au long de la nuit",
|
||||
"wind": "La vitesse du vent est élevée (plus de 30 km/h)"
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
<title>[#]lang:email.confirmation.subject[#]</title>
|
||||
</head>
|
||||
<body>
|
||||
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:email.confirmation.preheader[#]</span>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
|
||||
<tr>
|
||||
<td width="20%"><img src="[#]local_server[#]assets/images/icons/favicon-96x96.png" width="90%" border="0" alt="logo" /></td>
|
||||
<td><h1>[#]lang:email.confirmation.thanks_subject[#]</h1></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p align="justify">[#]lang:email.confirmation.body_1[#]</p>
|
||||
<p align="justify">[#]lang:email.confirmation.body_2[#]</p>
|
||||
<p align="justify">[#]lang:email.confirmation.body_3[#]</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p>[#]lang:email.confirmation.conclusion[#]<br />[#]lang:email.confirmation.signature[#]</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p>[#]lang:email.unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email.unsubscribe_button[#]</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="[#]language[#]">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="[#]lang:meta.page_og_desc[#]">
|
||||
<meta property="og:title" content="[#]title[#]" />
|
||||
<meta property="og:description" content="[#]lang:meta.page_og_desc[#]" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="[#]server[#]" />
|
||||
<meta property="og:image" content="assets/images/icons/ogp.svg?v=20260528" />
|
||||
<meta property="og:locale" content="[#]lang:meta.locale[#]" />
|
||||
<link rel="icon" type="image/png" href="assets/images/icons/favicon-96x96.png?v=20260528" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="assets/images/icons/favicon.svg?v=20260528" />
|
||||
<link rel="shortcut icon" href="assets/images/icons/favicon.ico?v=20260528" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/icons/apple-touch-icon.png?v=20260528" />
|
||||
<meta name="apple-mobile-web-app-title" content="[#]title[#]" />
|
||||
<link rel="manifest" href="assets/images/icons/site.webmanifest?v=20260528" />
|
||||
<meta name="theme-color" content="#081B19">
|
||||
<script id="app-config" type="application/json">[#]app_config[#]</script>
|
||||
<title>[#]title[#]</title>
|
||||
<!-- [PART] css [START] -->
|
||||
<link rel="stylesheet" href="[#]filename[#]" />
|
||||
<!-- [PART] css [END] -->
|
||||
<!-- [PART] module [START] -->
|
||||
<link rel="modulepreload" href="[#]filename[#]" />
|
||||
<!-- [PART] module [END] -->
|
||||
<script type="module" src="[#]app_entry[#]"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
// https://d3js.org v7.3.0 Copyright 2010-2021 Mike Bostock
|
||||
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";function n(t,n){return null==t||null==n?NaN:t<n?-1:t>n?1:t>=n?0:NaN}function e(t){let e=t,r=t,i=t;function o(t,n,e=0,o=t.length){if(e<o){if(0!==r(n,n))return o;do{const r=e+o>>>1;i(t[r],n)<0?e=r+1:o=r}while(e<o)}return e}return 2!==t.length&&(e=(n,e)=>t(n)-e,r=n,i=(e,r)=>n(t(e),r)),{left:o,center:function(t,n,r=0,i=t.length){const a=o(t,n,r,i-1);return a>r&&e(t[a-1],n)>-e(t[a],n)?a-1:a},right:function(t,n,e=0,o=t.length){if(e<o){if(0!==r(n,n))return o;do{const r=e+o>>>1;i(t[r],n)<=0?e=r+1:o=r}while(e<o)}return e}}}function r(t){return null===t?NaN:+t}const i=e(n),o=i.right,a=i.left,u=e(r).center;var c=o;function f(t,n){let e=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function s(t){return 0|t.length}function l(t){return!(t>0)}function h(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function d(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function p(t,n){const e=d(t,n);return e?Math.sqrt(e):e}function g(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r<n&&(r=n)));else{let i=-1;for(let o of t)null!=(o=n(o,++i,t))&&(void 0===e?o>=o&&(e=r=o):(e>o&&(e=o),r<o&&(r=o)))}return[e,r]}class y{constructor(){this._partials=new Float64Array(32),this._n=0}add(t){const n=this._partials;let e=0;for(let r=0;r<this._n&&r<32;r++){const i=n[r],o=t+i,a=Math.abs(t)<Math.abs(i)?t-(o-i):i-(o-t);a&&(n[e++]=a),t=o}return n[e]=t,this._n=e+1,this}valueOf(){const t=this._partials;let n,e,r,i=this._n,o=0;if(i>0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=m){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(v(this,t))}has(t){return super.has(v(this,t))}set(t,n){return super.set(_(this,t),n)}delete(t){return super.delete(b(this,t))}}class InternSet extends Set{constructor(t,n=m){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(v(this,t))}add(t){return super.add(_(this,t))}delete(t){return super.delete(b(this,t))}}function v({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):e}function _({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):(t.set(r,e),e)}function b({_intern:t,_key:n},e){const r=n(e);return t.has(r)&&(e=t.get(r),t.delete(r)),e}function m(t){return null!==t&&"object"==typeof t?t.valueOf():t}function x(t){return t}function w(t,...n){return k(t,x,x,n)}function M(t,...n){return k(t,Array.from,x,n)}function A(t,n){for(let e=1,r=n.length;e<r;++e)t=t.flatMap((t=>t.pop().map((([n,e])=>[...t,n,e]))));return t}function T(t,n,...e){return k(t,x,n,e)}function S(t,n,...e){return k(t,Array.from,n,e)}function E(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function k(t,n,e,r){return function t(i,o){if(o>=r.length)return e(i);const a=new InternMap,u=r[o++];let c=-1;for(const t of i){const n=u(t,++c,i),e=a.get(n);e?e.push(t):a.set(n,[t])}for(const[n,e]of a)a.set(n,t(e,o));return n(a)}(t,0)}function N(t,n){return Array.from(n,(n=>t[n]))}function C(t,...n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");t=Array.from(t);let[e]=n;if(e&&2!==e.length||n.length>1){const r=Uint32Array.from(t,((t,n)=>n));return n.length>1?(n=n.map((n=>t.map(n))),r.sort(((t,e)=>{for(const r of n){const n=z(r[t],r[e]);if(n)return n}}))):(e=t.map(e),r.sort(((t,n)=>z(e[t],e[n])))),N(t,r)}return t.sort(P(e))}function P(t=n){if(t===n)return z;if("function"!=typeof t)throw new TypeError("compare is not a function");return(n,e)=>{const r=t(n,e);return r||0===r?r:(0===t(e,e))-(0===t(n,n))}}function z(t,n){return(null==t||!(t>=t))-(null==n||!(n>=n))||(t<n?-1:t>n?1:0)}var D=Array.prototype.slice;function R(t){return()=>t}var F=Math.sqrt(50),q=Math.sqrt(10),O=Math.sqrt(2);function U(t,n,e){var r,i,o,a,u=-1;if(e=+e,(t=+t)===(n=+n)&&e>0)return[t];if((r=n<t)&&(i=t,t=n,n=i),0===(a=I(t,n,e))||!isFinite(a))return[];if(a>0){let e=Math.round(t/a),r=Math.round(n/a);for(e*a<t&&++e,r*a>n&&--r,o=new Array(i=r-e+1);++u<i;)o[u]=(e+u)*a}else{a=-a;let e=Math.round(t*a),r=Math.round(n*a);for(e/a<t&&++e,r/a>n&&--r,o=new Array(i=r-e+1);++u<i;)o[u]=(e+u)/a}return r&&o.reverse(),o}function I(t,n,e){var r=(n-t)/Math.max(0,e),i=Math.floor(Math.log(r)/Math.LN10),o=r/Math.pow(10,i);return i>=0?(o>=F?10:o>=q?5:o>=O?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=F?10:o>=q?5:o>=O?2:1)}funLine truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */
|
||||
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),j=function(e,t){return e===t&&(l=!0),0},D={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^Line truncated
|
||||
@@ -0,0 +1,20 @@
|
||||
/* jQuery UI Widget - v1.13.1 - https://jqueryui.com/download/#!version=1.13.1&components=100000000000000000000000000000000000000000000000 */
|
||||
!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)}(function(t){"use strict";t.ui=t.ui||{};t.ui.version="1.13.1";var e,i=0,s=Array.prototype.hasOwnProperty,n=Array.prototype.slice;t.cleanData=(e=t.cleanData,function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)(s=t._data(n,"events"))&&s.remove&&t(n).triggerHandler("remove");e(i)}),t.widget=function(e,i,s){var n,o,a,r={},l=e.split(".")[0],u=l+"-"+(e=e.split(".")[1]);return s||(s=i,i=t.Widget),Array.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr.pseudos[u.toLowerCase()]=function(e){return!!t.data(e,u)},t[l]=t[l]||{},n=t[l][e],o=t[l][e]=function(t,e){if(!this||!this._createWidget)return new o(t,e);arguments.length&&this._createWidget(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),(a=new i).options=t.widget.extend({},a.options),t.each(s,function(t,e){r[t]="function"==typeof e?function(){function s(){return i.prototype[t].apply(this,arguments)}function n(e){return i.prototype[t].apply(this,e)}return function(){var t,i=this._super,o=this._superApply;return this._super=s,this._superApply=n,t=e.apply(this,arguments),this._super=i,this._superApply=o,t}}():e}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n&&a.widgetEventPrefix||e},r,{constructor:o,namespace:l,widgetName:e,widgetFullName:u}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,o,a=n.call(arguments,1),r=0,l=a.length;r<l;r++)for(i in a[r])o=a[r][i],s.call(a[r],i)&&void 0!==o&&(t.isPlainObject(o)?e[i]=t.isPlainObject(e[i])?t.widget.extend({},e[i],o):t.widget.extend({},o):e[i]=o);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(o){var a="string"==typeof o,r=n.call(arguments,1),l=this;return a?this.length||"instance"!==o?this.each(function(){var i,n=t.data(this,s);return"instance"===o?(l=n,!1):n?"function"!=typeof n[o]||"_"===o.charAt(0)?t.error("no such method '"+o+"' for "+e+" widget instance"):(i=n[o].apply(n,r))!==n&&void 0!==i?(l=i&&i.jquery?l.pushStack(i.get()):i,!1):void 0:t.error("cannot call methods on "+e+" prior to initialization; attempted to call method '"+o+"'")}):l=void 0:(r.length&&(o=t.widget.extend.apply(null,[o].concat(r))),this.each(function(){var e=t.data(this,s);e?(e.option(o||{}),e._init&&e._init()):t.data(this,s,new i(o,this))})),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;o<s.length-1;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){reLine truncated
|
||||
|
||||
/* jQuery Iframe Transport Plugin - https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js */
|
||||
!function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?e(require("jquery")):e(window.jQuery)}(function(e){"use strict";var t=0,r=e,n="parseJSON";"JSON"in window&&"parse"in JSON&&(r=JSON,n="parse"),e.ajaxTransport("iframe",function(r){if(r.async){var n,a,o,i=r.initialIframeSrc||"javascript:false;";return{send:function(p,f){(n=e('<form style="display:none;"></form>')).attr("accept-charset",r.formAcceptCharset),o=/\?/.test(r.url)?"&":"?","DELETE"===r.type?(r.url=r.url+o+"_method=DELETE",r.type="POST"):"PUT"===r.type?(r.url=r.url+o+"_method=PUT",r.type="POST"):"PATCH"===r.type&&(r.url=r.url+o+"_method=PATCH",r.type="POST"),a=e('<iframe src="'+i+'" name="iframe-transport-'+(t+=1)+'"></iframe>').on("load",function(){var t,o=e.isArray(r.paramName)?r.paramName:[r.paramName];a.off("load").on("load",function(){var t;try{if(!(t=a.contents()).length||!t[0].firstChild)throw new Error}catch(e){t=void 0}f(200,"success",{iframe:t}),e('<iframe src="'+i+'"></iframe>').appendTo(n),window.setTimeout(function(){n.remove()},0)}),n.prop("target",a.prop("name")).prop("action",r.url).prop("method",r.type),r.formData&&e.each(r.formData,function(t,r){e('<input type="hidden"/>').prop("name",r.name).val(r.value).appendTo(n)}),r.fileInput&&r.fileInput.length&&"POST"===r.type&&(t=r.fileInput.clone(),r.fileInput.after(function(e){return t[e]}),r.paramName&&r.fileInput.each(function(t){e(this).prop("name",o[t]||r.paramName)}),n.append(r.fileInput).prop("enctype","multipart/form-data").prop("encoding","multipart/form-data"),r.fileInput.removeAttr("form")),window.setTimeout(function(){n.submit(),t&&t.length&&r.fileInput.each(function(r,n){var a=e(t[r]);e(n).prop("name",a.prop("name")).attr("form",a.attr("form")),a.replaceWith(n)})},0)}),n.append(a).appendTo(document.body)},abort:function(){a&&a.off("load").prop("src",i),n&&n.remove()}}}}),e.ajaxSetup({converters:{"iframe text":function(t){return t&&e(t[0].body).text()},"iframe json":function(t){return t&&r[n](e(t[0].body).text())},"iframe html":function(t){return t&&e(t[0].body).html()},"iframe xml":function(t){var r=t&&t[0];return r&&e.isXMLDoc(r)?r:e.parseXML(r.XMLDocument&&r.XMLDocument.xml||e(r.body).html())},"iframe script":function(t){return t&&e.globalEval(e(t[0].body).text())}}})});
|
||||
|
||||
/* jQuery-File-Upload - v10.32.0 - https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.fileupload.js */
|
||||
!function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery","jquery-ui/ui/widget"],e):"object"==typeof exports?e(require("jquery"),require("./vendor/jquery.ui.widget")):e(window.jQuery)}(function(e){"use strict";function t(t){var i="dragover"===t;return function(r){r.dataTransfer=r.originalEvent&&r.originalEvent.dataTransfer;var n=r.dataTransfer;n&&-1!==e.inArray("Files",n.types)&&!1!==this._trigger(t,e.Event(t,{delegatedEvent:r}))&&(r.preventDefault(),i&&(n.dropEffect="copy"))}}var i;e.support.fileInput=!(new RegExp("(Android (1\\.[0156]|2\\.[01]))|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)|(w(eb)?OSBrowser)|(webOS)|(Kindle/(1\\.0|2\\.[05]|3\\.0))").test(window.navigator.userAgent)||e('<input type="file"/>').prop("disabled")),e.support.xhrFileUpload=!(!window.ProgressEvent||!window.FileReader),e.support.xhrFormDataFileUpload=!!window.FormData,e.support.blobSlice=window.Blob&&(Blob.prototype.slice||Blob.prototype.webkitSlice||Blob.prototype.mozSlice),e.widget("blueimp.fileupload",{options:{dropZone:e(document),pasteZone:void 0,fileInput:void 0,replaceFileInput:!0,paramName:void 0,singleFileUploads:!0,limitMultiFileUploads:void 0,limitMultiFileUploadSize:void 0,limitMultiFileUploadSizeOverhead:512,sequentialUploads:!1,limitConcurrentUploads:void 0,forceIframeTransport:!1,redirect:void 0,redirectParamName:void 0,postMessage:void 0,multipart:!0,maxChunkSize:void 0,uploadedBytes:void 0,recalculateProgress:!0,progressInterval:100,bitrateInterval:500,autoUpload:!0,uniqueFilenames:void 0,messages:{uploadedBytes:"Uploaded bytes exceed file size"},i18n:function(t,i){return t=this.messages[t]||t.toString(),i&&e.each(i,function(e,i){t=t.replace("{"+e+"}",i)}),t},formData:function(e){return e.serializeArray()},add:function(t,i){if(t.isDefaultPrevented())return!1;(i.autoUpload||!1!==i.autoUpload&&e(this).fileupload("option","autoUpload"))&&i.process().done(function(){i.submit()})},processData:!1,contentType:!1,cache:!1,timeout:0},_promisePipe:(i=e.fn.jquery.split("."),Number(i[0])>1||Number(i[1])>7?"then":"pipe"),_specialOptions:["fileInput","dropZone","pasteZone","multipart","forceIframeTransport"],_blobSlice:e.support.blobSlice&&function(){return(this.slice||this.webkitSlice||this.mozSlice).apply(this,arguments)},_BitrateTimer:function(){this.timestamp=Date.now?Date.now():(new Date).getTime(),this.loaded=0,this.bitrate=0,this.getBitrate=function(e,t,i){var r=e-this.timestamp;return(!this.bitrate||!i||r>i)&&(this.bitrate=(t-this.loaded)*(1e3/r)*8,this.loaded=t,this.timestamp=e),this.bitrate}},_isXHRUpload:function(t){return!t.forceIframeTransport&&(!t.multipart&&e.support.xhrFileUpload||e.support.xhrFormDataFileUpload)},_getFormData:function(t){var i;return"function"===e.type(t.formData)?t.formData(t.form):e.isArray(t.formData)?t.formData:"object"===e.type(t.formData)?(i=[],e.each(t.formData,function(e,t){i.push({name:e,value:t})}),i):[]},_getTotal:function(t){var i=0;return e.each(t,function(e,t){i+=t.size||1}),i},_initProgressObject:function(t){var i={loaded:0,total:0,bitrate:0};t._progress?e.extend(t._progress,i):t._progress=i},_initResponseObject:function(e){var t;if(e._response)for(t in e._response)Object.prototype.hasOwnProperty.call(e._response,t)&&delete e._response[t];else e._response={}},_onProgress:function(t,i){if(t.lengthComputable){var r,n=Date.now?Date.now():(new Date).getTime();if(i._time&&i.progressInterval&&n-i._time<i.progressInterval&&t.loaded!==t.total)return;i._time=n,r=Math.floor(t.loaded/t.total*(i.chunkSize||i._progress.total))+(i.uploadedBytes||0),this._progress.loaded+=r-i._progress.loaded,this._progress.bitrate=this._bitrateTimer.getBitrate(n,this._progress.loaded,i.bitrateInterval),i._progress.loaded=i.loaded=r,i._progress.bitrate=i.bitrate=i._bitrateTimer.getBitrate(n,r,i.bitrateInterval),this._trigger("progress",e.Event("progress",{delegatedEvent:t}),i),this._trigger("progressall",e.Event("progressall",{delegatedEvent:t}),this._progress)}},_initProgressListener:function(t){var i=this,r=t.xhr?t.xhr():e.ajaxSettings.xhr();r.upload&&(e(r.upload).on("progress",function(e){var r=e.originalEvent;e.lengthComputable=r.lengthComputable,e.loaded=r.loaded,e.total=r.total,i._onProgress(e,t)}),t.xhr=function(){return r})},_deinitProgressListener:function(t){var i=t.xhr?t.xhr():e.ajaxSettings.xhr();i.upload&&e(i.upload).off("progress")},_isInstanceOf:function(e,t){return Object.prototype.toString.call(t)==="[object "+e+"]"},_getUniqueFilename:function(e,t){return t[e=String(e)]?(e=e.replace(/(?: \(([\d]+)\))?(\.[^.]+)?$/,function(e,t,i){return" ("+(t?Number(t)+1:1)+")"+(i||"")}),this._getUniqueFilename(e,t)):(t[e]=!0,e)},_initXHRData:function(t){var i,r=this,n=t.files[0],o=t.multipart||!e.support.xhrFileUpload,s="array"===e.type(t.paramName)?t.paramName[0]:t.paramName;t.headers=e.extend({},t.headers),t.contentRange&&(t.headers["Content-Range"]=t.contentRange),o&&!t.blob&&this._isInstanceOf("File",n)||(t.headers["Content-Disposition"]='attachment; filename="'+encodeULine truncated
|
||||
|
||||
/* SimpleBar - v5.3.6 - https://unpkg.com/simplebar@latest/dist/simplebar.min.js - Under MIT License */
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).SimpleBar=e()}(this,(function(){"use strict";var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function e(t,e){return t(e={exports:{}},e.exports),e.exports}var r,i,n,o="object",s=function(t){return t&&t.Math==Math&&t},a=s(typeof globalThis==o&&globalThis)||s(typeof window==o&&window)||s(typeof self==o&&self)||s(typeof t==o&&t)||Function("return this")(),c=function(t){try{return!!t()}catch(t){return!0}},l=!c((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})),u={}.propertyIsEnumerable,f=Object.getOwnPropertyDescriptor,h={f:f&&!u.call({1:2},1)?function(t){var e=f(this,t);return!!e&&e.enumerable}:u},d=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},p={}.toString,v=function(t){return p.call(t).slice(8,-1)},g="".split,b=c((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==v(t)?g.call(t,""):Object(t)}:Object,y=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},m=function(t){return b(y(t))},x=function(t){return"object"==typeof t?null!==t:"function"==typeof t},E=function(t,e){if(!x(t))return t;var r,i;if(e&&"function"==typeof(r=t.toString)&&!x(i=r.call(t)))return i;if("function"==typeof(r=t.valueOf)&&!x(i=r.call(t)))return i;if(!e&&"function"==typeof(r=t.toString)&&!x(i=r.call(t)))return i;throw TypeError("Can't convert object to primitive value")},w={}.hasOwnProperty,S=function(t,e){return w.call(t,e)},O=a.document,k=x(O)&&x(O.createElement),A=function(t){return k?O.createElement(t):{}},T=!l&&!c((function(){return 7!=Object.defineProperty(A("div"),"a",{get:function(){return 7}}).a})),L=Object.getOwnPropertyDescriptor,z={f:l?L:function(t,e){if(t=m(t),e=E(e,!0),T)try{return L(t,e)}catch(t){}if(S(t,e))return d(!h.f.call(t,e),t[e])}},R=function(t){if(!x(t))throw TypeError(String(t)+" is not an object");return t},_=Object.defineProperty,M={f:l?_:function(t,e,r){if(R(t),e=E(e,!0),R(r),T)try{return _(t,e,r)}catch(t){}if("get"in r||"set"in r)throw TypeError("Accessors not supported");return"value"in r&&(t[e]=r.value),t}},C=l?function(t,e,r){return M.f(t,e,d(1,r))}:function(t,e,r){return t[e]=r,t},W=function(t,e){try{C(a,t,e)}catch(r){a[t]=e}return e},j=e((function(t){var e=a["__core-js_shared__"]||W("__core-js_shared__",{});(t.exports=function(t,r){return e[t]||(e[t]=void 0!==r?r:{})})("versions",[]).push({version:"3.2.1",mode:"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})})),N=j("native-function-to-string",Function.toString),I=a.WeakMap,B="function"==typeof I&&/native code/.test(N.call(I)),D=0,P=Math.random(),F=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++D+P).toString(36)},V=j("keys"),X=function(t){return V[t]||(V[t]=F(t))},H={},q=a.WeakMap;if(B){var $=new q,Y=$.get,G=$.has,U=$.set;r=function(t,e){return U.call($,t,e),e},i=function(t){return Y.call($,t)||{}},n=function(t){return G.call($,t)}}else{var Q=X("state");H[Q]=!0,r=function(t,e){return C(t,Q,e),e},i=function(t){return S(t,Q)?t[Q]:{}},n=function(t){return S(t,Q)}}var K={set:r,get:i,has:n,enforce:function(t){return n(t)?i(t):r(t,{})},getterFor:function(t){return function(e){var r;if(!x(e)||(r=i(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return r}}},J=e((function(t){var e=K.get,r=K.enforce,i=String(N).split("toString");j("inspectSource",(function(t){return N.call(t)})),(t.exports=function(t,e,n,o){var s=!!o&&!!o.unsafe,c=!!o&&!!o.enumerable,l=!!o&&!!o.noTargetGet;"function"==typeof n&&("string"!=typeof e||S(n,"name")||C(n,"name",e),r(n).source=i.join("string"==typeof e?e:"")),t!==a?(s?!l&&t[e]&&(c=!0):delete t[e],c?t[e]=n:C(t,e,n)):c?t[e]=n:W(e,n)})(Function.prototype,"toString",(function(){return"function"==typeof this&&e(this).source||N.call(this)}))})),Z=a,tt=function(t){return"function"==typeof t?t:void 0},et=function(t,e){return arguments.length<2?tt(Z[t])||tt(a[t]):Z[t]&&Z[t][e]||a[t]&&a[t][e]},rt=Math.ceil,it=Math.floor,nt=function(t){return isNaN(t=+t)?0:(t>0?it:rt)(t)},ot=Math.min,st=function(t){return t>0?ot(nt(t),9007199254740991):0},at=Math.max,ct=Math.min,lt=function(t){return function(e,r,i){var n,o=m(e),s=st(o.length),a=function(t,e){var r=nt(t);return r<0?at(r+e,0):ct(r,e)}(i,s);if(t&&r!=r){for(;s>a;)if((n=o[a++])!=n)return!0}else for(;s>a;a++)if((t||a in o)&&o[a]===r)return t||a||0;return!t&&-1}},ut={includes:lt(!0),indexOf:lt(!1)}.indexOf,ft=function(t,e){var r,i=m(t),n=0,o=[];for(r in i)!S(H,r)&&S(i,r)&&o.push(r);for(;e.length>n;)S(i,r=e[n++])&&(~ut(o,r)||o.push(r));return o},ht=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],dt=ht.concat("length","prototype"),pt={f:Object.getOwnPropertyNames||function(t){return ft(t,dt)}},vt={f:Object.getOwnPropertySymbols},gt=Line truncated
|
||||
|
||||
/* Autosize - Jun 30, 2021 - https://github.com/jackmoore/autosize/blob/master/dist/autosize.min.js - MIT license */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e||self).autosize=t()}(this,function(){var e,t,n="function"==typeof Map?new Map:(e=[],t=[],{has:function(t){return e.indexOf(t)>-1},get:function(n){return t[e.indexOf(n)]},set:function(n,o){-1===e.indexOf(n)&&(e.push(n),t.push(o))},delete:function(n){var o=e.indexOf(n);o>-1&&(e.splice(o,1),t.splice(o,1))}}),o=function(e){return new Event(e,{bubbles:!0})};try{new Event("test")}catch(e){o=function(e){var t=document.createEvent("Event");return t.initEvent(e,!0,!1),t}}function r(e){var t=n.get(e);t&&t.destroy()}function i(e){var t=n.get(e);t&&t.update()}var l=null;return"undefined"==typeof window||"function"!=typeof window.getComputedStyle?((l=function(e){return e}).destroy=function(e){return e},l.update=function(e){return e}):((l=function(e,t){return e&&Array.prototype.forEach.call(e.length?e:[e],function(e){return function(e){if(e&&e.nodeName&&"TEXTAREA"===e.nodeName&&!n.has(e)){var t,r=null,i=null,l=null,d=function(){e.clientWidth!==i&&c()},u=function(t){window.removeEventListener("resize",d,!1),e.removeEventListener("input",c,!1),e.removeEventListener("keyup",c,!1),e.removeEventListener("autosize:destroy",u,!1),e.removeEventListener("autosize:update",c,!1),Object.keys(t).forEach(function(n){e.style[n]=t[n]}),n.delete(e)}.bind(e,{height:e.style.height,resize:e.style.resize,overflowY:e.style.overflowY,overflowX:e.style.overflowX,wordWrap:e.style.wordWrap});e.addEventListener("autosize:destroy",u,!1),"onpropertychange"in e&&"oninput"in e&&e.addEventListener("keyup",c,!1),window.addEventListener("resize",d,!1),e.addEventListener("input",c,!1),e.addEventListener("autosize:update",c,!1),e.style.overflowX="hidden",e.style.wordWrap="break-word",n.set(e,{destroy:u,update:c}),"vertical"===(t=window.getComputedStyle(e,null)).resize?e.style.resize="none":"both"===t.resize&&(e.style.resize="horizontal"),r="content-box"===t.boxSizing?-(parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)):parseFloat(t.borderTopWidth)+parseFloat(t.borderBottomWidth),isNaN(r)&&(r=0),c()}function a(t){var n=e.style.width;e.style.width="0px",e.style.width=n,e.style.overflowY=t}function s(){if(0!==e.scrollHeight){var t=function(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push({node:e.parentNode,scrollTop:e.parentNode.scrollTop}),e=e.parentNode;return t}(e),n=document.documentElement&&document.documentElement.scrollTop;e.style.height="",e.style.height=e.scrollHeight+r+"px",i=e.clientWidth,t.forEach(function(e){e.node.scrollTop=e.scrollTop}),n&&(document.documentElement.scrollTop=n)}}function c(){s();var t=Math.round(parseFloat(e.style.height)),n=window.getComputedStyle(e,null),r="content-box"===n.boxSizing?Math.round(parseFloat(n.height)):e.offsetHeight;if(r<t?"hidden"===n.overflowY&&(a("scroll"),s(),r="content-box"===n.boxSizing?Math.round(parseFloat(window.getComputedStyle(e,null).height)):e.offsetHeight):"hidden"!==n.overflowY&&(a("hidden"),s(),r="content-box"===n.boxSizing?Math.round(parseFloat(window.getComputedStyle(e,null).height)):e.offsetHeight),l!==r){l=r;var i=o("autosize:resized");try{e.dispatchEvent(i)}catch(e){}}}}(e)}),e}).destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],r),e},l.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],i),e}),l});
|
||||
|
||||
/* Mousewheel - v3.1.13 - https://github.com/jquery/jquery-mousewheel/blob/master/jquery.mousewheel.min.js - Copyright OpenJS Foundation and other contributors */
|
||||
!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(u){var f,d,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],w=Array.prototype.slice;if(u.event.fixHooks)for(var i=e.length;i;)u.event.fixHooks[e[--i]]=u.event.mouseHooks;var c=u.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],n,!1);else this.onmousewheel=n;u.data(this,"mousewheel-line-height",c.getLineHeight(this)),u.data(this,"mousewheel-page-height",c.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],n,!1);else this.onmousewheel=null;u.removeData(this,"mousewheel-line-height"),u.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=u(e),i=t["offsetParent"in u.fn?"offsetParent":"parent"]();return i.length||(i=u("body")),parseInt(i.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return u(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function n(e){var t,i=e||window.event,n=w.call(arguments,1),o=0,l=0,s=0;if((e=u.event.fix(i)).type="mousewheel","detail"in i&&(s=-1*i.detail),"wheelDelta"in i&&(s=i.wheelDelta),"wheelDeltaY"in i&&(s=i.wheelDeltaY),"wheelDeltaX"in i&&(l=-1*i.wheelDeltaX),"axis"in i&&i.axis===i.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in i&&(o=s=-1*i.deltaY),"deltaX"in i&&(l=i.deltaX,0===s&&(o=-1*l)),0!==s||0!==l){if(1===i.deltaMode){var a=u.data(this,"mousewheel-line-height");o*=a,s*=a,l*=a}else if(2===i.deltaMode){var h=u.data(this,"mousewheel-page-height");o*=h,s*=h,l*=h}if(t=Math.max(Math.abs(s),Math.abs(l)),(!d||t<d)&&g(i,d=t)&&(d/=40),g(i,t)&&(o/=40,l/=40,s/=40),o=Math[1<=o?"floor":"ceil"](o/d),l=Math[1<=l?"floor":"ceil"](l/d),s=Math[1<=s?"floor":"ceil"](s/d),c.settings.normalizeOffset&&this.getBoundingClientRect){var r=this.getBoundingClientRect();e.offsetX=e.clientX-r.left,e.offsetY=e.clientY-r.top}return e.deltaX=l,e.deltaY=s,e.deltaFactor=d,e.deltaMode=0,n.unshift(e,o,l,s),f&&window.clearTimeout(f),f=window.setTimeout(m,200),(u.event.dispatch||u.event.handle).apply(this,n)}}function m(){d=null}function g(e,t){return c.settings.adjustOldDeltas&&"mousewheel"===e.type&&t%120==0}u.fn.extend({mousewheel:function(e){return e?this.on("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.off("mousewheel",e)}})});
|
||||
|
||||
/* waitForImages - v2.4.0 - https://github.com/alexanderdickson/waitForImages/blob/master/dist/jquery.waitforimages.min.js - MIT license */
|
||||
!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){var b="waitForImages",c=function(a){return a.srcset&&a.sizes}(new Image);a.waitForImages={hasImageProperties:["backgroundImage","listStyleImage","borderImage","borderCornerImage","cursor"],hasImageAttributes:["srcset"]},a.expr.pseudos["has-src"]=function(b){return a(b).is('img[src][src!=""]')},a.expr.pseudos.uncached=function(b){return!!a(b).is(":has-src")&&!b.complete},a.fn.waitForImages=function(){var d,e,f,g=0,h=0,i=a.Deferred(),j=this,k=[],l=a.waitForImages.hasImageProperties||[],m=a.waitForImages.hasImageAttributes||[],n=/url\(\s*(['"]?)(.*?)\1\s*\)/g;if(a.isPlainObject(arguments[0])?(f=arguments[0].waitForAll,e=arguments[0].each,d=arguments[0].finished):1===arguments.length&&"boolean"===a.type(arguments[0])?f=arguments[0]:(d=arguments[0],e=arguments[1],f=arguments[2]),d=d||a.noop,e=e||a.noop,f=!!f,!a.isFunction(d)||!a.isFunction(e))throw new TypeError("An invalid callback was supplied.");return this.each(function(){var b=a(this);f?b.find("*").addBack().each(function(){var b=a(this);b.is("img:has-src")&&!b.is("[srcset]")&&k.push({src:b.attr("src"),element:b[0]}),a.each(l,function(a,c){var d,e=b.css(c);if(!e)return!0;for(;d=n.exec(e);)k.push({src:d[2],element:b[0]})}),a.each(m,function(a,c){var d=b.attr(c);return!d||void k.push({src:b.attr("src"),srcset:b.attr("srcset"),element:b[0]})})}):b.find("img:has-src").each(function(){k.push({src:this.src,element:this})})}),g=k.length,h=0,0===g&&(d.call(j),i.resolveWith(j)),a.each(k,function(f,k){var l=new Image,m="load."+b+" error."+b;a(l).one(m,function b(c){var f=[h,g,"load"==c.type];if(h++,e.apply(k.element,f),i.notifyWith(k.element,f),a(this).off(m,b),h==g)return d.call(j[0]),i.resolveWith(j[0]),!1}),c&&k.srcset&&(l.srcset=k.srcset,l.sizes=k.sizes),l.src=k.src}),i.promise()}});
|
||||
@@ -0,0 +1,25 @@
|
||||
/* @preserve
|
||||
* Leaflet 1.8.0-beta.3+main.58eea06, a JS library for interactive maps. https://leafletjs.com
|
||||
* (c) 2010-2022 Vladimir Agafonkin, (c) 2010-2011 CloudMade
|
||||
*/
|
||||
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var i,e,n=1,o=arguments.length;n<o;n++)for(i in e=arguments[n])t[i]=e[i];return t}var R=Object.create||function(t){return N.prototype=t,new N};function N(){}function a(t,i){var e=Array.prototype.slice;if(t.bind)return t.bind.apply(t,e.call(arguments,1));var n=e.call(arguments,2);return function(){return t.apply(i,n.length?n.concat(e.call(arguments)):arguments)}}var D=0;function h(t){return"_leaflet_id"in t||(t._leaflet_id=++D),t._leaflet_id}function j(t,i,e){var n,o,s=function(){n=!1,o&&(r.apply(e,o),o=!1)},r=function(){n?o=arguments:(t.apply(e,arguments),setTimeout(s,i),n=!0)};return r}function H(t,i,e){var n=i[1],i=i[0],o=n-i;return t===n&&e?t:((t-i)%o+o)%o+i}function u(){return!1}function e(t,i){if(!1===i)return t;i=Math.pow(10,void 0===i?6:i);return Math.round(t*i)/i}function W(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function F(t){return W(t).split(/\s+/)}function c(t,i){for(var e in Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?R(t.options):{}),i)t.options[e]=i[e];return t.options}function U(t,i,e){var n,o=[];for(n in t)o.push(encodeURIComponent(e?n.toUpperCase():n)+"="+encodeURIComponent(t[n]));return(i&&-1!==i.indexOf("?")?"&":"?")+o.join("&")}var V=/\{ *([\w_ -]+) *\}/g;function q(t,e){return t.replace(V,function(t,i){i=e[i];if(void 0===i)throw new Error("No value provided for variable "+t);return i="function"==typeof i?i(e):i})}var d=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function G(t,i){for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1}var K="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function Y(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var X=0;function J(t){var i=+new Date,e=Math.max(0,16-(i-X));return X=i+e,window.setTimeout(t,e)}var $=window.requestAnimationFrame||Y("RequestAnimationFrame")||J,Q=window.cancelAnimationFrame||Y("CancelAnimationFrame")||Y("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function x(t,i,e){if(!e||$!==J)return $.call(window,a(t,i));t.call(i)}function r(t){t&&Q.call(window,t)}var tt={__proto__:null,extend:l,create:R,bind:a,get lastId(){return D},stamp:h,throttle:j,wrapNum:H,falseFn:u,formatNum:e,trim:W,splitWords:F,setOptions:c,getParamString:U,template:q,isArray:d,indexOf:G,emptyImageUrl:K,requestFn:$,cancelFn:Q,requestAnimFrame:x,cancelAnimFrame:r};function it(){}it.extend=function(t){function i(){c(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()}var e,n=i.__super__=this.prototype,o=R(n);for(e in(o.constructor=i).prototype=o,this)Object.prototype.hasOwnProperty.call(this,e)&&"prototype"!==e&&"__super__"!==e&&(i[e]=this[e]);if(t.statics&&l(i,t.statics),t.includes){var s=t.includes;if("undefined"!=typeof L&&L&&L.Mixin){s=d(s)?s:[s];for(var r=0;r<s.length;r++)s[r]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",(new Error).stack)}l.apply(null,[o].concat(t.includes))}return l(o,t),delete o.statics,delete o.includes,o.options&&(o.options=n.options?R(n.options):{},l(o.options,t.options)),o._initHooks=[],o.callInitHooks=function(){if(!this._initHooksCalled){n.callInitHooks&&n.callInitHooks.call(this),this._initHooksCalled=!0;for(var t=0,i=o._initHooks.length;t<i;t++)o._initHooks[t].call(this)}},i},it.include=function(t){var i=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=i,this.mergeOptions(t.options)),this},it.mergeOptions=function(t){return l(this.prototype.options,t),this},it.addInitHook=function(t){var i=Array.prototype.slice.call(arguments,1),e="function"==typeof t?t:function(){this[t].apply(this,i)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(e),this};var i={on:function(t,i,e){if("object"==typeof t)for(var n in t)this._on(n,t[n],i);else for(var o=0,s=(t=F(t)).length;o<s;o++)this._on(t[o],i,e);return this},off:function(t,i,e){if(arguments.length)if("object"==typeof t)for(var n in t)this._off(n,t[n],i);else{t=F(t);for(var o=1===arguments.length,s=0,r=t.length;s<r;s++)o?this._off(t[s]):this._off(t[s],i,e)}else delete this._events;return this},_on:function(t,i,e){if("function"!=typeof i)console.warn("wrong listener type: "+typeof i);else{this._events=this._events||{};for(var n=this._events[t],t=(n||(this._events[t]=n=[]),{fn:i,ctx:e=e===this?void 0:e}),o=n,s=0,r=o.length;s<r;s++)if(o[s].fn===i&&o[s].ctx===e)return;o.push(t)}},_off:function(t,i,e){var n,o,s;if(this._events&&(n=this._events[t]))if(1===arguments.length){if(this._firingCount)for(o=0,s=n.length;o<s;o++)n[o].fn=u;delete this._events[t]}else if(e===this&&(e=void 0),"function"!=typeLine truncated
|
||||
|
||||
/* Leaflet.GeometryUtil - v0.10.1 - https://github.com/makinacorpus/Leaflet.GeometryUtil/blob/master/src/leaflet.geometryutil.js - BSD 3-Clause "New" or "Revised" License */
|
||||
!function(t){var e;if("function"==typeof define&&define.amd)define(["leaflet"],t);else if("undefined"!=typeof module)e=require("leaflet"),module.exports=t(e);else{if(void 0===window.L)throw"Leaflet must be loaded first";t(window.L)}}(function(t){"use strict";return t.Polyline._flat=t.LineUtil.isFlat||t.Polyline._flat||function(e){return!t.Util.isArray(e[0])||"object"!=typeof e[0][0]&&void 0!==e[0][0]},t.GeometryUtil=t.extend(t.GeometryUtil||{},{distance:function(t,e,n){return t.latLngToLayerPoint(e).distanceTo(t.latLngToLayerPoint(n))},distanceSegment:function(e,n,r,a){var o=e.latLngToLayerPoint(n),i=e.latLngToLayerPoint(r),l=e.latLngToLayerPoint(a);return t.LineUtil.pointToSegmentDistance(o,i,l)},readableDistance:function(t,e){return"imperial"!==e?t>1e3?(t/1e3).toFixed(2)+" km":t.toFixed(1)+" m":(t*=1.09361)>1760?(t/1760).toFixed(2)+" miles":t.toFixed(1)+" yd"},belongsSegment:function(t,e,n,r){r=void 0===r?.2:r;var a=e.distanceTo(n);return(e.distanceTo(t)+t.distanceTo(n)-a)/a<r},length:function(e){var n=t.GeometryUtil.accumulatedLengths(e);return n.length>0?n[n.length-1]:0},accumulatedLengths:function(t){if("function"==typeof t.getLatLngs&&(t=t.getLatLngs()),0===t.length)return[];for(var e=0,n=[0],r=0,a=t.length-1;r<a;r++)e+=t[r].distanceTo(t[r+1]),n.push(e);return n},closestOnSegment:function(e,n,r,a){var o=e.getMaxZoom();o===1/0&&(o=e.getZoom());var i=e.project(n,o),l=e.project(r,o),s=e.project(a,o),c=t.LineUtil.closestPointOnSegment(i,l,s);return e.unproject(c,o)},closest:function(e,n,r,a){var o,i,l,s,c,u=1/0,g=null;if(n instanceof Array){if(n[0]instanceof Array&&"number"!=typeof n[0][0]){for(i=0;i<n.length;i++)(c=t.GeometryUtil.closest(e,n[i],r,a))&&c.distance<u&&(u=c.distance,g=c);return g}if(!(n[0]instanceof t.LatLng||"number"==typeof n[0][0]||"number"==typeof n[0].lat))return g;n=t.polyline(n)}if(!(n instanceof t.Polyline))return g;if(o=JSON.parse(JSON.stringify(n.getLatLngs().slice(0))),n instanceof t.Polygon){var f=function(e){if(t.Polyline._flat(e))e.push(e[0]);else for(var n=0;n<e.length;n++)f(e[n])};f(o)}if(t.Polyline._flat(o)){if(a){for(i=0,l=o.length;i<l;i++){var y=o[i];(s=t.GeometryUtil.distance(e,r,y))<u&&(u=s,(g=y).distance=s)}return g}for(i=0,l=o.length;i<l-1;i++){var L=o[i],h=o[i+1];(s=t.GeometryUtil.distanceSegment(e,r,L,h))<=u&&(u=s,(g=t.GeometryUtil.closestOnSegment(e,r,L,h)).distance=s)}return g}for(i=0;i<o.length;i++)(c=t.GeometryUtil.closest(e,o[i],r,a)).distance<u&&(u=c.distance,g=c);return g},closestLayer:function(e,n,r){for(var a=1/0,o=null,i=null,l=1/0,s=0,c=n.length;s<c;s++){var u=n[s];if(u instanceof t.LayerGroup){var g=t.GeometryUtil.closestLayer(e,u.getLayers(),r);g.distance<a&&(a=g.distance,o=g)}else"function"==typeof u.getLatLng?(i=u.getLatLng(),l=t.GeometryUtil.distance(e,r,i)):(i=t.GeometryUtil.closest(e,u,r))&&(l=i.distance),l<a&&(a=l,o={layer:u,latlng:i,distance:l})}return o},nClosestLayers:function(e,n,r,a){if((a="number"==typeof a?a:n.length)<1||n.length<1)return null;for(var o,i,l=[],s=0,c=n.length;s<c;s++){var u=n[s];if(u instanceof t.LayerGroup){var g=t.GeometryUtil.closestLayer(e,u.getLayers(),r);l.push(g)}else"function"==typeof u.getLatLng?(i=u.getLatLng(),o=t.GeometryUtil.distance(e,r,i)):(i=t.GeometryUtil.closest(e,u,r))&&(o=i.distance),l.push({layer:u,latlng:i,distance:o})}return l.sort(function(t,e){return t.distance-e.distance}),l.length>a?l.slice(0,a):l},layersWithin:function(e,n,r,a){a="number"==typeof a?a:1/0;for(var o=[],i=null,l=0,s=0,c=n.length;s<c;s++){var u=n[s];"function"==typeof u.getLatLng?(i=u.getLatLng(),l=t.GeometryUtil.distance(e,r,i)):(i=t.GeometryUtil.closest(e,u,r))&&(l=i.distance),i&&l<a&&o.push({layer:u,latlng:i,distance:l})}return o.sort(function(t,e){return t.distance-e.distance})},closestLayerSnap:function(e,n,r,a,o){a="number"==typeof a?a:1/0,o="boolean"!=typeof o||o;var i=t.GeometryUtil.closestLayer(e,n,r);if(!i||i.distance>a)return null;if(o&&"function"==typeof i.layer.getLatLngs){var l=t.GeometryUtil.closest(e,i.layer,i.latlng,!0);l.distance<a&&(i.latlng=l,i.distance=t.GeometryUtil.distance(e,l,r))}return i},interpolateOnPointSegment:function(e,n,r){return t.point(e.x*(1-r)+r*n.x,e.y*(1-r)+r*n.y)},interpolateOnLine:function(e,n,r){var a=(n=n instanceof t.Polyline?n.getLatLngs():n).length;if(a<2)return null;if(0===(r=Math.max(Math.min(r,1),0)))return{latLng:n[0]instanceof t.LatLng?n[0]:t.latLng(n[0]),predecessor:-1};if(1==r)return{latLng:n[n.length-1]instanceof t.LatLng?n[n.length-1]:t.latLng(n[n.length-1]),predecessor:n.length-2};var o=e.getMaxZoom();o===1/0&&(o=e.getZoom());for(var i=[],l=0,s=0;s<a;s++)i[s]=e.project(n[s],o),s>0&&(l+=i[s-1].distanceTo(i[s]));var c=l*r,u=0,g=0;for(s=0;g<c;s++){var f=i[s],y=i[s+1];u=g,g+=f.distanceTo(y)}if(null==f&&null==y)f=i[0],y=i[1],s=1;var L=g-u!=0?(c-u)/(g-u):0,h=t.GeometryUtil.interpolateOnPointSegment(f,y,L);return{latLng:e.unproject(h,o),predecessor:s-1}},locateOnLine:function(e,n,r){var a=n.getLatLngs();if(r.equals(a[0]))return 0;if(r.equals(a[a.length-1]))return 1;for(var o=t.GeometryULine truncated
|
||||
|
||||
/* Leaflet.Heightgraph - v1.3.2 - https://github.com/GIScience/Leaflet.Heightgraph/blob/master/dist/L.Control.Heightgraph.min.js - MIT License */
|
||||
!function(){"use strict";function t(n){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(n)}function n(t,n,e){return n in t?Object.defineProperty(t,n,{value:e,enumerable:!0,configurable:!0,writable:!0}):t[n]=e,t}function e(t,n){var e=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(t,n).enumerable}))),e.push.apply(e,r)}return e}function r(t){for(var r=1;r<arguments.length;r++){var i=null!=arguments[r]?arguments[r]:{};r%2?e(Object(i),!0).forEach((function(e){n(t,e,i[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach((function(n){Object.defineProperty(t,n,Object.getOwnPropertyDescriptor(i,n))}))}return t}function i(t,n){(null==n||n>t.length)&&(n=t.length);for(var e=0,r=new Array(n);e<n;e++)r[e]=t[e];return r}function o(t,n){var e;if("undefined"==typeof Symbol||null==t[Symbol.iterator]){if(Array.isArray(t)||(e=function(t,n){if(t){if("string"==typeof t)return i(t,n);var e=Object.prototype.toString.call(t).slice(8,-1);return"Object"===e&&t.constructor&&(e=t.constructor.name),"Map"===e||"Set"===e?Array.from(t):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?i(t,n):void 0}}(t))||n&&t&&"number"==typeof t.length){e&&(t=e);var r=0,o=function(){};return{s:o,n:function(){return r>=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,u=!1;return{s:function(){e=t[Symbol.iterator]()},n:function(){var t=e.next();return s=t.done,t},e:function(t){u=!0,a=t},f:function(){try{s||null==e.return||e.return()}finally{if(u)throw a}}}}var a="http://www.w3.org/1999/xhtml",s={svg:"http://www.w3.org/2000/svg",xhtml:a,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function u(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),s.hasOwnProperty(n)?{space:s[n],local:t}:t}function l(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===a&&n.documentElement.namespaceURI===a?n.createElement(t):n.createElementNS(e,t)}}function h(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function c(t){var n=u(t);return(n.local?h:l)(n)}function f(){}function d(t){return null==t?f:function(){return this.querySelector(t)}}function p(){return[]}function g(t){return null==t?p:function(){return this.querySelectorAll(t)}}function _(t){return function(){return this.matches(t)}}function v(t){return new Array(t.length)}function y(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}y.prototype={constructor:y,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};function m(t,n,e,r,i,o){for(var a,s=0,u=n.length,l=o.length;s<l;++s)(a=n[s])?(a.__data__=o[s],r[s]=a):e[s]=new y(t,o[s]);for(;s<u;++s)(a=n[s])&&(i[s]=a)}function x(t,n,e,r,i,o,a){var s,u,l,h={},c=n.length,f=o.length,d=new Array(c);for(s=0;s<c;++s)(u=n[s])&&(d[s]=l="$"+a.call(u,u.__data__,s,n),l in h?i[s]=u:h[l]=u);for(s=0;s<f;++s)(u=h[l="$"+a.call(t,o[s],s,o)])?(r[s]=u,u.__data__=o[s],h[l]=null):e[s]=new y(t,o[s]);for(s=0;s<c;++s)(u=n[s])&&h[d[s]]===u&&(i[s]=u)}function w(t,n){return t<n?-1:t>n?1:t>=n?0:NaN}function b(t){return function(){this.removeAttribute(t)}}function M(t){return function(){this.removeAttributeNS(t.space,t.local)}}function k(t,n){return function(){this.setAttribute(t,n)}}function A(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function S(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function T(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function N(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function C(t){return function(){this.style.removeProperty(t)}}function E(t,n,e){return function(){this.style.setProperty(t,n,e)}}function H(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function D(t,n){return t.style.getPropertyValue(n)||N(t).getComputedStyle(t,null).getPropertyValue(n)}function F(t){return function(){delete this[t]}}function P(t,n){return function(){this[t]=n}}function B(t,n){return function(){var e=n.apply(this,arguLine truncated
|
||||
|
||||
/* Additional Leaflet functions */
|
||||
L.Map.include({
|
||||
setOffsetView: function (iOffsetRatioX, oCenter, iZoomLevel) {
|
||||
var oCenter = (typeof oCenter == 'object')?$.extend({}, oCenter):this.getCenter();
|
||||
iZoomLevel = iZoomLevel || this.getZoom();
|
||||
|
||||
var oBounds = this.getBounds();
|
||||
var iOffsetX = (oBounds.getEast() - oBounds.getWest()) * iOffsetRatioX / ( 2 * Math.pow(2, iZoomLevel - this.getZoom()));
|
||||
oCenter.lng = oCenter.lng - iOffsetX;
|
||||
|
||||
this.setView(oCenter, iZoomLevel);
|
||||
}
|
||||
});
|
||||