Merge branch 'next'
@ -10,6 +10,11 @@ chown nginx:nginx /run/nginx
|
|||||||
sed -i "s/^nobody:.*$/nobody:x:1000:50::nobody:\/:\/sbin\/nologin/" /etc/passwd
|
sed -i "s/^nobody:.*$/nobody:x:1000:50::nobody:\/:\/sbin\/nologin/" /etc/passwd
|
||||||
sed -i "s/^nobody:.*$/nobody:x:50:/" /etc/group
|
sed -i "s/^nobody:.*$/nobody:x:50:/" /etc/group
|
||||||
|
|
||||||
|
# Prepare ulogger filesystem
|
||||||
|
grep '^[$<?]' /var/www/html/config.default.php > /var/www/html/config.php
|
||||||
|
chown nobody:nobody /var/www/html/uploads
|
||||||
|
chmod 775 /var/www/html/uploads
|
||||||
|
|
||||||
if [ "$ULOGGER_DB_DRIVER" = "sqlite" ]; then
|
if [ "$ULOGGER_DB_DRIVER" = "sqlite" ]; then
|
||||||
sed -i "s/^\$dbuser = .*$//" /var/www/html/config.php
|
sed -i "s/^\$dbuser = .*$//" /var/www/html/config.php
|
||||||
sed -i "s/^\$dbpass = .*$//" /var/www/html/config.php
|
sed -i "s/^\$dbpass = .*$//" /var/www/html/config.php
|
||||||
@ -49,12 +54,12 @@ else
|
|||||||
mysqld_safe --datadir=/data &
|
mysqld_safe --datadir=/data &
|
||||||
mysqladmin --silent --wait=30 ping
|
mysqladmin --silent --wait=30 ping
|
||||||
mysqladmin -u root password "${DB_ROOT_PASS}"
|
mysqladmin -u root password "${DB_ROOT_PASS}"
|
||||||
mysql -u root -p${DB_ROOT_PASS} < /var/www/html/scripts/ulogger.sql
|
mysql -u root -p"${DB_ROOT_PASS}" < /var/www/html/scripts/ulogger.sql
|
||||||
mysql -u root -p${DB_ROOT_PASS} -e "CREATE USER 'ulogger'@'localhost' IDENTIFIED BY '${DB_USER_PASS}'"
|
mysql -u root -p"${DB_ROOT_PASS}" -e "CREATE USER 'ulogger'@'localhost' IDENTIFIED BY '${DB_USER_PASS}'"
|
||||||
mysql -u root -p${DB_ROOT_PASS} -e "GRANT ALL PRIVILEGES ON ulogger.* TO 'ulogger'@'localhost'"
|
mysql -u root -p"${DB_ROOT_PASS}" -e "GRANT ALL PRIVILEGES ON ulogger.* TO 'ulogger'@'localhost'"
|
||||||
mysql -u root -p${DB_ROOT_PASS} -e "CREATE USER 'ulogger'@'%' IDENTIFIED BY '${DB_USER_PASS}'"
|
mysql -u root -p"${DB_ROOT_PASS}" -e "CREATE USER 'ulogger'@'%' IDENTIFIED BY '${DB_USER_PASS}'"
|
||||||
mysql -u root -p${DB_ROOT_PASS} -e "GRANT ALL PRIVILEGES ON ulogger.* TO 'ulogger'@'%'"
|
mysql -u root -p"${DB_ROOT_PASS}" -e "GRANT ALL PRIVILEGES ON ulogger.* TO 'ulogger'@'%'"
|
||||||
mysql -u root -p${DB_ROOT_PASS} -e "INSERT INTO users (login, password) VALUES ('admin', '\$2y\$10\$7OvZrKgonVZM9lkzrTbiou.CVhO3HjPk5y0W9L68fVwPs/osBRIMq')" ulogger
|
mysql -u root -p"${DB_ROOT_PASS}" -e "INSERT INTO users (login, password) VALUES ('admin', '\$2y\$10\$7OvZrKgonVZM9lkzrTbiou.CVhO3HjPk5y0W9L68fVwPs/osBRIMq')" ulogger
|
||||||
mysqladmin -u root -p${DB_ROOT_PASS} shutdown
|
mysqladmin -u root -p"${DB_ROOT_PASS}" shutdown
|
||||||
sed -i "s/^\$dbdsn = .*$/\$dbdsn = \"mysql:host=localhost;port=3306;dbname=ulogger;charset=utf8\";/" /var/www/html/config.php
|
sed -i "s/^\$dbdsn = .*$/\$dbdsn = \"mysql:host=localhost;port=3306;dbname=ulogger;charset=utf8\";/" /var/www/html/config.php
|
||||||
fi
|
fi
|
||||||
|
343
.eslintrc.js
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"jasmine": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:jasmine/recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly",
|
||||||
|
"google": true
|
||||||
|
},
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"jasmine",
|
||||||
|
"import"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"accessor-pairs": "error",
|
||||||
|
"array-bracket-newline": "error",
|
||||||
|
"array-bracket-spacing": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"array-callback-return": "error",
|
||||||
|
"array-element-newline": "off",
|
||||||
|
"arrow-body-style": "error",
|
||||||
|
"arrow-parens": "error",
|
||||||
|
"arrow-spacing": "error",
|
||||||
|
"block-scoped-var": "off",
|
||||||
|
"block-spacing": "off",
|
||||||
|
"brace-style": [
|
||||||
|
"error",
|
||||||
|
"1tbs",
|
||||||
|
{
|
||||||
|
"allowSingleLine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"callback-return": "error",
|
||||||
|
"camelcase": "off",
|
||||||
|
"capitalized-comments": "off",
|
||||||
|
"class-methods-use-this": "error",
|
||||||
|
"comma-dangle": "error",
|
||||||
|
"comma-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"after": true,
|
||||||
|
"before": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"comma-style": [
|
||||||
|
"error",
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"complexity": "error",
|
||||||
|
"computed-property-spacing": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"consistent-return": "error",
|
||||||
|
"consistent-this": "off",
|
||||||
|
"curly": "error",
|
||||||
|
"default-case": "error",
|
||||||
|
"dot-location": [
|
||||||
|
"error",
|
||||||
|
"property"
|
||||||
|
],
|
||||||
|
"dot-notation": "off",
|
||||||
|
"eol-last": "error",
|
||||||
|
"eqeqeq": "off",
|
||||||
|
"func-call-spacing": "error",
|
||||||
|
"func-name-matching": "error",
|
||||||
|
"func-names": "off",
|
||||||
|
"func-style": [
|
||||||
|
"error",
|
||||||
|
"declaration",
|
||||||
|
{
|
||||||
|
"allowArrowFunctions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"function-paren-newline": "off",
|
||||||
|
"generator-star-spacing": "error",
|
||||||
|
"global-require": "error",
|
||||||
|
"guard-for-in": "off",
|
||||||
|
"handle-callback-err": "error",
|
||||||
|
"id-blacklist": "error",
|
||||||
|
"id-length": "off",
|
||||||
|
"id-match": "error",
|
||||||
|
"implicit-arrow-linebreak": "error",
|
||||||
|
"indent": "off",
|
||||||
|
"indent-legacy": "off",
|
||||||
|
"init-declarations": "off",
|
||||||
|
"jsx-quotes": "error",
|
||||||
|
"key-spacing": "error",
|
||||||
|
"keyword-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"after": true,
|
||||||
|
"before": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"line-comment-position": "off",
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"lines-around-comment": "off",
|
||||||
|
"lines-around-directive": "error",
|
||||||
|
"lines-between-class-members": "error",
|
||||||
|
"max-classes-per-file": "error",
|
||||||
|
"max-depth": "off",
|
||||||
|
"max-len": "off",
|
||||||
|
"max-lines": "off",
|
||||||
|
"max-lines-per-function": "off",
|
||||||
|
"max-nested-callbacks": "error",
|
||||||
|
"max-params": "error",
|
||||||
|
"max-statements": "off",
|
||||||
|
"max-statements-per-line": "off",
|
||||||
|
"multiline-comment-style": "off",
|
||||||
|
"multiline-ternary": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"new-cap": [
|
||||||
|
"error", { "newIsCapExceptionPattern": "^u[A-Z]" }
|
||||||
|
],
|
||||||
|
"new-parens": "error",
|
||||||
|
"newline-after-var": "off",
|
||||||
|
"newline-before-return": "off",
|
||||||
|
"newline-per-chained-call": "off",
|
||||||
|
"no-alert": "off",
|
||||||
|
"no-array-constructor": "error",
|
||||||
|
"no-async-promise-executor": "error",
|
||||||
|
"no-await-in-loop": "error",
|
||||||
|
"no-bitwise": "error",
|
||||||
|
"no-buffer-constructor": "error",
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-catch-shadow": "error",
|
||||||
|
"no-cond-assign": [
|
||||||
|
"error",
|
||||||
|
"except-parens"
|
||||||
|
],
|
||||||
|
"no-confusing-arrow": "error",
|
||||||
|
"no-continue": "error",
|
||||||
|
"no-div-regex": "error",
|
||||||
|
"no-duplicate-imports": "error",
|
||||||
|
"no-else-return": "error",
|
||||||
|
"no-empty-function": "error",
|
||||||
|
"no-eq-null": "off",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-extend-native": "off",
|
||||||
|
"no-extra-bind": "error",
|
||||||
|
"no-extra-label": "error",
|
||||||
|
"no-extra-parens": "off",
|
||||||
|
"no-floating-decimal": "error",
|
||||||
|
"no-implicit-globals": "off",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"no-inline-comments": "off",
|
||||||
|
"no-inner-declarations": [
|
||||||
|
"error",
|
||||||
|
"functions"
|
||||||
|
],
|
||||||
|
"no-invalid-this": "off",
|
||||||
|
"no-iterator": "error",
|
||||||
|
"no-label-var": "error",
|
||||||
|
"no-labels": "error",
|
||||||
|
"no-lone-blocks": "error",
|
||||||
|
"no-lonely-if": "error",
|
||||||
|
"no-loop-func": "error",
|
||||||
|
"no-magic-numbers": "off",
|
||||||
|
"no-misleading-character-class": "error",
|
||||||
|
"no-mixed-operators": "off",
|
||||||
|
"no-mixed-requires": "error",
|
||||||
|
"no-multi-assign": "error",
|
||||||
|
"no-multi-spaces": "error",
|
||||||
|
"no-multi-str": "error",
|
||||||
|
"no-multiple-empty-lines": "error",
|
||||||
|
"no-native-reassign": "error",
|
||||||
|
"no-negated-condition": "off",
|
||||||
|
"no-negated-in-lhs": "error",
|
||||||
|
"no-nested-ternary": "error",
|
||||||
|
"no-new": "error",
|
||||||
|
"no-new-func": "error",
|
||||||
|
"no-new-object": "error",
|
||||||
|
"no-new-require": "error",
|
||||||
|
"no-new-wrappers": "error",
|
||||||
|
"no-octal-escape": "error",
|
||||||
|
"no-param-reassign": "off",
|
||||||
|
"no-path-concat": "error",
|
||||||
|
"no-plusplus": "off",
|
||||||
|
"no-process-env": "error",
|
||||||
|
"no-process-exit": "error",
|
||||||
|
"no-proto": "error",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"no-restricted-globals": "error",
|
||||||
|
"no-restricted-imports": "error",
|
||||||
|
"no-restricted-modules": "error",
|
||||||
|
"no-restricted-properties": "error",
|
||||||
|
"no-restricted-syntax": "error",
|
||||||
|
"no-return-assign": [
|
||||||
|
"error",
|
||||||
|
"except-parens"
|
||||||
|
],
|
||||||
|
"no-return-await": "error",
|
||||||
|
"no-script-url": "error",
|
||||||
|
"no-self-compare": "error",
|
||||||
|
"no-sequences": "error",
|
||||||
|
"no-shadow": "error",
|
||||||
|
"no-shadow-restricted-names": "error",
|
||||||
|
"no-spaced-func": "error",
|
||||||
|
"no-sync": "error",
|
||||||
|
"no-tabs": "error",
|
||||||
|
"no-template-curly-in-string": "error",
|
||||||
|
"no-ternary": "off",
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"no-undef-init": "error",
|
||||||
|
"no-undefined": "error",
|
||||||
|
"no-underscore-dangle": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowAfterThis": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-unmodified-loop-condition": "error",
|
||||||
|
"no-unneeded-ternary": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-use-before-define": "off",
|
||||||
|
"no-useless-call": "error",
|
||||||
|
"no-useless-catch": "error",
|
||||||
|
"no-useless-computed-key": "error",
|
||||||
|
"no-useless-concat": "off",
|
||||||
|
"no-useless-constructor": "error",
|
||||||
|
"no-useless-rename": "error",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"no-void": "error",
|
||||||
|
"no-warning-comments": "warn",
|
||||||
|
"no-whitespace-before-property": "error",
|
||||||
|
"no-with": "error",
|
||||||
|
"nonblock-statement-body-position": "error",
|
||||||
|
"object-curly-newline": "error",
|
||||||
|
"object-curly-spacing": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"object-shorthand": [
|
||||||
|
"error",
|
||||||
|
"consistent-as-needed"
|
||||||
|
],
|
||||||
|
"one-var": "off",
|
||||||
|
"one-var-declaration-per-line": [
|
||||||
|
"error",
|
||||||
|
"initializations"
|
||||||
|
],
|
||||||
|
"operator-assignment": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"operator-linebreak": "error",
|
||||||
|
"padded-blocks": "off",
|
||||||
|
"padding-line-between-statements": "error",
|
||||||
|
"prefer-arrow-callback": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"prefer-destructuring": "off",
|
||||||
|
"prefer-named-capture-group": "off",
|
||||||
|
"prefer-numeric-literals": "error",
|
||||||
|
"prefer-object-spread": "error",
|
||||||
|
"prefer-promise-reject-errors": "error",
|
||||||
|
"prefer-reflect": "off",
|
||||||
|
"prefer-rest-params": "off",
|
||||||
|
"prefer-spread": "error",
|
||||||
|
"prefer-template": "off",
|
||||||
|
"quote-props": "off",
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"single",
|
||||||
|
"avoid-escape"
|
||||||
|
],
|
||||||
|
"radix": [
|
||||||
|
"error",
|
||||||
|
"as-needed"
|
||||||
|
],
|
||||||
|
"require-atomic-updates": "error",
|
||||||
|
"require-await": "error",
|
||||||
|
"require-jsdoc": "off",
|
||||||
|
"require-unicode-regexp": "off",
|
||||||
|
"rest-spread-spacing": "error",
|
||||||
|
"semi": "off",
|
||||||
|
"semi-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"after": true,
|
||||||
|
"before": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"semi-style": [
|
||||||
|
"error",
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"sort-imports": "error",
|
||||||
|
"sort-keys": "off",
|
||||||
|
"sort-vars": "off",
|
||||||
|
"space-before-blocks": "error",
|
||||||
|
"space-before-function-paren": "off",
|
||||||
|
"space-in-parens": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"space-infix-ops": "error",
|
||||||
|
"space-unary-ops": "error",
|
||||||
|
"spaced-comment": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"strict": "off",
|
||||||
|
"switch-colon-spacing": "error",
|
||||||
|
"symbol-description": "error",
|
||||||
|
"template-curly-spacing": "error",
|
||||||
|
"template-tag-spacing": "error",
|
||||||
|
"unicode-bom": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"valid-jsdoc": "off",
|
||||||
|
"vars-on-top": "off",
|
||||||
|
"wrap-regex": "error",
|
||||||
|
"yield-star-spacing": "error",
|
||||||
|
"yoda": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
@ -36,7 +36,7 @@ abstract class BaseDatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
|
|||||||
protected $testAccuracy = 10;
|
protected $testAccuracy = 10;
|
||||||
protected $testProvider = "gps";
|
protected $testProvider = "gps";
|
||||||
protected $testComment = "test comment";
|
protected $testComment = "test comment";
|
||||||
protected $testImageId = 1;
|
protected $testImage = "1234_1502974402_5d1a1960335cf.jpg";
|
||||||
|
|
||||||
// Fixes PostgreSQL: "cannot truncate a table referenced in a foreign key constraint"
|
// Fixes PostgreSQL: "cannot truncate a table referenced in a foreign key constraint"
|
||||||
protected function getSetUpOperation() {
|
protected function getSetUpOperation() {
|
||||||
@ -180,7 +180,11 @@ abstract class BaseDatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
|
|||||||
protected function addTestUser($user = NULL, $pass = NULL) {
|
protected function addTestUser($user = NULL, $pass = NULL) {
|
||||||
if (is_null($user)) { $user = $this->testUser; }
|
if (is_null($user)) { $user = $this->testUser; }
|
||||||
if (is_null($pass)) { $pass = $this->testPass; }
|
if (is_null($pass)) { $pass = $this->testPass; }
|
||||||
return $this->pdoInsert('users', [ 'login' => $user, 'password' => $pass ]);
|
$id = $this->pdoInsert('users', [ 'login' => $user, 'password' => $pass ]);
|
||||||
|
if ($id !== false) {
|
||||||
|
return (int) $id;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -196,7 +200,11 @@ abstract class BaseDatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
|
|||||||
if (is_null($userId)) { $userId = $this->testUserId; }
|
if (is_null($userId)) { $userId = $this->testUserId; }
|
||||||
if (is_null($trackName)) { $trackName = $this->testTrackName; }
|
if (is_null($trackName)) { $trackName = $this->testTrackName; }
|
||||||
if (is_null($comment)) { $comment = $this->testTrackComment; }
|
if (is_null($comment)) { $comment = $this->testTrackComment; }
|
||||||
return $this->pdoInsert('tracks', [ 'user_id' => $userId, 'name' => $trackName, 'comment' => $comment ]);
|
$id = $this->pdoInsert('tracks', [ 'user_id' => $userId, 'name' => $trackName, 'comment' => $comment ]);
|
||||||
|
if ($id !== false) {
|
||||||
|
return (int) $id;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -224,8 +224,7 @@ class ClientAPITest extends UloggerAPITestCase {
|
|||||||
'bearing' => $this->testBearing,
|
'bearing' => $this->testBearing,
|
||||||
'accuracy' => $this->testAccuracy,
|
'accuracy' => $this->testAccuracy,
|
||||||
'provider' => $this->testProvider,
|
'provider' => $this->testProvider,
|
||||||
'comment' => $this->testComment,
|
'comment' => $this->testComment
|
||||||
'imageid' => $this->testImageId
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$response = $this->http->post('/client/index.php', $options);
|
$response = $this->http->post('/client/index.php', $options);
|
||||||
@ -246,15 +245,115 @@ class ClientAPITest extends UloggerAPITestCase {
|
|||||||
"accuracy" => $this->testAccuracy,
|
"accuracy" => $this->testAccuracy,
|
||||||
"provider" => $this->testProvider,
|
"provider" => $this->testProvider,
|
||||||
"comment" => $this->testComment,
|
"comment" => $this->testComment,
|
||||||
"image_id" => $this->testImageId
|
"image" => null
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, user_id, track_id, " . $this->unix_timestamp('time') . " AS time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
"SELECT id, user_id, track_id, " . $this->unix_timestamp('time') . " AS time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAddPositionWithImage() {
|
||||||
|
$this->assertTrue($this->authenticate(), "Authentication failed");
|
||||||
|
|
||||||
|
$trackId = $this->addTestTrack($this->testUserId);
|
||||||
|
$this->assertEquals(1, $this->getConnection()->getRowCount('tracks'), "Wrong row count");
|
||||||
|
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'http_errors' => false,
|
||||||
|
'multipart' => [
|
||||||
|
[
|
||||||
|
'name' => 'action',
|
||||||
|
'contents' => 'addpos',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'trackid',
|
||||||
|
'contents' => $trackId,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'time',
|
||||||
|
'contents' => $this->testTimestamp,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'lat',
|
||||||
|
'contents' => $this->testLat,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'lon',
|
||||||
|
'contents' => $this->testLon,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'altitude',
|
||||||
|
'contents' => $this->testAltitude,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'speed',
|
||||||
|
'contents' => $this->testSpeed,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'bearing',
|
||||||
|
'contents' => $this->testBearing,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'accuracy',
|
||||||
|
'contents' => $this->testAccuracy,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'provider',
|
||||||
|
'contents' => $this->testProvider,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'comment',
|
||||||
|
'contents' => $this->testComment,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'image',
|
||||||
|
'contents' => 'DEADBEEF',
|
||||||
|
'filename' => 'upload',
|
||||||
|
'headers' => [ 'Content-Type' => 'image/jpeg', 'Content-Transfer-Encoding' => 'binary' ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$response = $this->http->post('/client/index.php', $options);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
$json = json_decode((string) $response->getBody());
|
||||||
|
$this->assertFalse($json->{'error'}, "Unexpected error");
|
||||||
|
$this->assertEquals(1, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
|
$expected = [
|
||||||
|
"id" => 1,
|
||||||
|
"user_id" => $this->testUserId,
|
||||||
|
"track_id" => $trackId,
|
||||||
|
"time" => $this->testTimestamp,
|
||||||
|
"latitude" => $this->testLat,
|
||||||
|
"longitude" => $this->testLon,
|
||||||
|
"altitude" => $this->testAltitude,
|
||||||
|
"speed" => $this->testSpeed,
|
||||||
|
"bearing" => $this->testBearing,
|
||||||
|
"accuracy" => $this->testAccuracy,
|
||||||
|
"provider" => $this->testProvider,
|
||||||
|
"comment" => $this->testComment
|
||||||
|
];
|
||||||
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
|
"positions",
|
||||||
|
"SELECT id, user_id, track_id, " . $this->unix_timestamp('time') . " AS time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
|
);
|
||||||
|
$this->assertEquals($expected['id'], $actual->getValue(0, 'id'));
|
||||||
|
$this->assertEquals($expected['user_id'], $actual->getValue(0, 'user_id'));
|
||||||
|
$this->assertEquals($expected['track_id'], $actual->getValue(0, 'track_id'));
|
||||||
|
$this->assertEquals($expected['time'], $actual->getValue(0, 'time'));
|
||||||
|
$this->assertEquals($expected['latitude'], $actual->getValue(0, 'latitude'));
|
||||||
|
$this->assertEquals($expected['longitude'], $actual->getValue(0, 'longitude'));
|
||||||
|
$this->assertEquals($expected['altitude'], $actual->getValue(0, 'altitude'));
|
||||||
|
$this->assertEquals($expected['speed'], $actual->getValue(0, 'speed'));
|
||||||
|
$this->assertEquals($expected['bearing'], $actual->getValue(0, 'bearing'));
|
||||||
|
$this->assertEquals($expected['accuracy'], $actual->getValue(0, 'accuracy'));
|
||||||
|
$this->assertEquals($expected['provider'], $actual->getValue(0, 'provider'));
|
||||||
|
$this->assertEquals($expected['comment'], $actual->getValue(0, 'comment'));
|
||||||
|
$this->assertContains('.jpg', $actual->getValue(0, 'image'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testAddPositionNoexistantTrack() {
|
public function testAddPositionNoexistantTrack() {
|
||||||
$this->assertTrue($this->authenticate(), "Authentication failed");
|
$this->assertTrue($this->authenticate(), "Authentication failed");
|
||||||
|
|
||||||
@ -275,7 +374,7 @@ class ClientAPITest extends UloggerAPITestCase {
|
|||||||
'accuracy' => $this->testAccuracy,
|
'accuracy' => $this->testAccuracy,
|
||||||
'provider' => $this->testProvider,
|
'provider' => $this->testProvider,
|
||||||
'comment' => $this->testComment,
|
'comment' => $this->testComment,
|
||||||
'imageid' => $this->testImageId
|
'imageid' => $this->testImage
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$response = $this->http->post('/client/index.php', $options);
|
$response = $this->http->post('/client/index.php', $options);
|
||||||
@ -306,7 +405,7 @@ class ClientAPITest extends UloggerAPITestCase {
|
|||||||
'accuracy' => $this->testAccuracy,
|
'accuracy' => $this->testAccuracy,
|
||||||
'provider' => $this->testProvider,
|
'provider' => $this->testProvider,
|
||||||
'comment' => $this->testComment,
|
'comment' => $this->testComment,
|
||||||
'imageid' => $this->testImageId
|
'imageid' => $this->testImage
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -343,7 +442,7 @@ class ClientAPITest extends UloggerAPITestCase {
|
|||||||
'accuracy' => $this->testAccuracy,
|
'accuracy' => $this->testAccuracy,
|
||||||
'provider' => $this->testProvider,
|
'provider' => $this->testProvider,
|
||||||
'comment' => $this->testComment,
|
'comment' => $this->testComment,
|
||||||
'imageid' => $this->testImageId
|
'imageid' => $this->testImage
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?php
|
<?php /** @noinspection HtmlUnknownAttribute */
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
@ -54,12 +54,13 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
|
$this->assertEquals(count($json), 1, "Wrong count of tracks");
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$track = $json[0];
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(1, (int) $track->id, "Wrong track id");
|
||||||
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
|
$this->assertEquals($this->testTrackName, $track->name, "Wrong track name");
|
||||||
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
|
|
||||||
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -88,12 +89,12 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
||||||
altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
|
|
||||||
@ -110,7 +111,7 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
}
|
}
|
||||||
@ -170,12 +171,14 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(count($json), 1, "Wrong count of tracks");
|
||||||
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
|
|
||||||
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
|
$track = $json[0];
|
||||||
|
$this->assertEquals(1, (int) $track->id, "Wrong track id");
|
||||||
|
$this->assertEquals($this->testTrackName, $track->name, "Wrong track name");
|
||||||
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -204,12 +207,12 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
||||||
altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
}
|
}
|
||||||
@ -241,7 +244,7 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
<ele>' . $this->testAltitude . '</ele>
|
<ele>' . $this->testAltitude . '</ele>
|
||||||
<time>' . gmdate("Y-m-d\TH:i:s\Z", $this->testTimestamp) . '</time>
|
<time>' . gmdate("Y-m-d\TH:i:s\Z", $this->testTimestamp) . '</time>
|
||||||
<name>1</name>
|
<name>1</name>
|
||||||
<desc><![CDATA[User: demo Track: client_test2 Time: 2017-06-25 00:50:45 (Europe/Warsaw) Speed: 0 km/h Altitude: 15 m Total time: 00:00:00 Average speed: 0 km/h Total distance: 0 km Point 1 of 18]]></desc>
|
<desc><![CDATA[' . $this->testComment . ']]></desc>
|
||||||
<extensions>
|
<extensions>
|
||||||
<ulogger:speed>' . $this->testSpeed . '</ulogger:speed>
|
<ulogger:speed>' . $this->testSpeed . '</ulogger:speed>
|
||||||
<ulogger:bearing>' . $this->testBearing . '</ulogger:bearing>
|
<ulogger:bearing>' . $this->testBearing . '</ulogger:bearing>
|
||||||
@ -270,12 +273,14 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(count($json), 1, "Wrong count of tracks");
|
||||||
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
|
|
||||||
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
|
$track = $json[0];
|
||||||
|
$this->assertEquals(1, (int) $track->id, "Wrong track id");
|
||||||
|
$this->assertEquals($this->testTrackName, $track->name, "Wrong track name");
|
||||||
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -303,13 +308,13 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"bearing" => $this->testBearing,
|
"bearing" => $this->testBearing,
|
||||||
"accuracy" => $this->testAccuracy,
|
"accuracy" => $this->testAccuracy,
|
||||||
"provider" => $this->testProvider,
|
"provider" => $this->testProvider,
|
||||||
"comment" => null,
|
"comment" => $this->testComment,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
||||||
altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
}
|
}
|
||||||
@ -351,12 +356,14 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(count($json), 1, "Wrong count of tracks");
|
||||||
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
|
|
||||||
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
|
$track = $json[0];
|
||||||
|
$this->assertEquals(1, (int) $track->id, "Wrong track id");
|
||||||
|
$this->assertEquals($this->testTrackName, $track->name, "Wrong track name");
|
||||||
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -385,12 +392,12 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
||||||
altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
}
|
}
|
||||||
@ -438,12 +445,14 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status: $xml->message");
|
$this->assertEquals(count($json), 1, "Wrong count of tracks");
|
||||||
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
|
|
||||||
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
|
$track = $json[0];
|
||||||
|
$this->assertEquals(1, (int) $track->id, "Wrong track id");
|
||||||
|
$this->assertEquals($this->testTrackName, $track->name, "Wrong track name");
|
||||||
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -472,12 +481,12 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
||||||
altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
$expected = [
|
$expected = [
|
||||||
@ -493,7 +502,7 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
}
|
}
|
||||||
@ -543,12 +552,18 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status: $xml->message");
|
$this->assertEquals(count($json), 2, "Wrong count of tracks");
|
||||||
$this->assertEquals(2, (int) $xml->trackid, "Wrong error message");
|
|
||||||
$this->assertEquals(2, (int) $xml->trackcnt, "Wrong error message");
|
$track = $json[0];
|
||||||
|
$this->assertEquals(2, (int) $track->id, "Wrong track id");
|
||||||
|
$this->assertEquals($this->testTrackName, $track->name, "Wrong track name");
|
||||||
|
|
||||||
|
$track = $json[1];
|
||||||
|
$this->assertEquals(1, (int) $track->id, "Wrong track id");
|
||||||
|
$this->assertEquals($this->testTrackName, $track->name, "Wrong track name");
|
||||||
|
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -584,12 +599,12 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
"SELECT id, " . $this->unix_timestamp('time') . " AS time, user_id, track_id, latitude, longitude,
|
||||||
altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
$expected = [
|
$expected = [
|
||||||
@ -605,7 +620,7 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
"accuracy" => null,
|
"accuracy" => null,
|
||||||
"provider" => "gps",
|
"provider" => "gps",
|
||||||
"comment" => null,
|
"comment" => null,
|
||||||
"image_id" => null
|
"image" => null
|
||||||
];
|
];
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
}
|
}
|
||||||
@ -647,11 +662,11 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(1, (int) $json->error, "Wrong error status");
|
||||||
$this->assertEquals($lang["iparsefailure"], (string) $xml->message, "Wrong error status");
|
$this->assertEquals($lang["iparsefailure"], (string) $json->message, "Wrong error status");
|
||||||
|
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -694,11 +709,11 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(1, (int) $json->error, "Wrong error status");
|
||||||
$this->assertEquals($lang["iparsefailure"], (string) $xml->message, "Wrong error status");
|
$this->assertEquals($lang["iparsefailure"], (string) $json->message, "Wrong error status");
|
||||||
|
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -735,11 +750,11 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(1, (int) $json->error, "Wrong error status");
|
||||||
$this->assertEquals($lang["iparsefailure"], (string) $xml->message, "Wrong error status");
|
$this->assertEquals($lang["iparsefailure"], (string) $json->message, "Wrong error status");
|
||||||
|
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
@ -780,29 +795,16 @@ class ImportTest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/import.php", $options);
|
$response = $this->http->post("/utils/import.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(1, (int) $json->error, "Wrong error status");
|
||||||
$this->assertEquals(0, strpos((string) $xml->message, $lang["iparsefailure"]), "Wrong error status");
|
$this->assertEquals(0, strpos((string) $json->message, $lang["iparsefailure"]), "Wrong error status");
|
||||||
|
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount("positions"), "Wrong row count");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ResponseInterface $response
|
|
||||||
* @return bool|SimpleXMLElement
|
|
||||||
*/
|
|
||||||
private function getXMLfromResponse($response) {
|
|
||||||
$xml = false;
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
try {
|
|
||||||
$xml = new SimpleXMLElement((string) $response->getBody());
|
|
||||||
} catch (Exception $e) { /* ignore */ }
|
|
||||||
return $xml;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getStream($string) {
|
private function getStream($string) {
|
||||||
$stream = tmpfile();
|
$stream = tmpfile();
|
||||||
fwrite($stream, $string);
|
fwrite($stream, $string);
|
||||||
|
@ -28,20 +28,20 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
|
$this->assertEquals(count($json), 2, "Wrong count of positions");
|
||||||
|
|
||||||
$position = $xml->position[0];
|
$position = $json[0];
|
||||||
$this->assertEquals((int) $position["id"], 1, "Wrong position id");
|
$this->assertEquals((int) $position->id, 1, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
|
||||||
$this->assertEquals((string) $position->username, $this->testAdminUser, "Wrong username");
|
$this->assertEquals((string) $position->username, $this->testAdminUser, "Wrong username");
|
||||||
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
||||||
|
|
||||||
$position = $xml->position[1];
|
$position = $json[1];
|
||||||
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
|
$this->assertEquals((int) $position->id, 2, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 1, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 1, "Wrong timestamp");
|
||||||
@ -67,20 +67,20 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
|
$this->assertEquals(count($json), 2, "Wrong count of positions");
|
||||||
|
|
||||||
$position = $xml->position[0];
|
$position = $json[0];
|
||||||
$this->assertEquals((int) $position["id"], 1, "Wrong position id");
|
$this->assertEquals((int) $position->id, 1, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
|
||||||
$this->assertEquals((string) $position->username, $this->testUser, "Wrong username");
|
$this->assertEquals((string) $position->username, $this->testUser, "Wrong username");
|
||||||
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
||||||
|
|
||||||
$position = $xml->position[1];
|
$position = $json[1];
|
||||||
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
|
$this->assertEquals((int) $position->id, 2, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 1, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 1, "Wrong timestamp");
|
||||||
@ -107,9 +107,9 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->position->count(), 0, "Wrong count of positions");
|
$this->assertEquals(count($json), 0, "Wrong count of positions");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetPositionsOtherUserByAdmin() {
|
public function testGetPositionsOtherUserByAdmin() {
|
||||||
@ -131,20 +131,20 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
|
$this->assertEquals(count($json), 2, "Wrong count of positions");
|
||||||
|
|
||||||
$position = $xml->position[0];
|
$position = $json[0];
|
||||||
$this->assertEquals((int) $position["id"], 1, "Wrong position id");
|
$this->assertEquals((int) $position->id, 1, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
|
||||||
$this->assertEquals((string) $position->username, $this->testUser, "Wrong username");
|
$this->assertEquals((string) $position->username, $this->testUser, "Wrong username");
|
||||||
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
||||||
|
|
||||||
$position = $xml->position[1];
|
$position = $json[1];
|
||||||
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
|
$this->assertEquals((int) $position->id, 2, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 1, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 1, "Wrong timestamp");
|
||||||
@ -175,12 +175,12 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->position->count(), 1, "Wrong count of positions");
|
$this->assertEquals(count($json), 1, "Wrong count of positions");
|
||||||
|
|
||||||
$position = $xml->position[0];
|
$position = $json[0];
|
||||||
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
|
$this->assertEquals((int) $position->id, 2, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 3, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 3, "Wrong timestamp");
|
||||||
@ -212,20 +212,20 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
|
$this->assertEquals(count($json), 2, "Wrong count of positions");
|
||||||
|
|
||||||
$position = $xml->position[0];
|
$position = $json[0];
|
||||||
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
|
$this->assertEquals((int) $position->id, 2, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 3, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 3, "Wrong timestamp");
|
||||||
$this->assertEquals((string) $position->username, $this->testAdminUser, "Wrong username");
|
$this->assertEquals((string) $position->username, $this->testAdminUser, "Wrong username");
|
||||||
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
|
||||||
|
|
||||||
$position = $xml->position[1];
|
$position = $json[1];
|
||||||
$this->assertEquals((int) $position["id"], 3, "Wrong position id");
|
$this->assertEquals((int) $position->id, 3, "Wrong position id");
|
||||||
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
|
||||||
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
|
||||||
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 2, "Wrong timestamp");
|
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 2, "Wrong timestamp");
|
||||||
@ -250,9 +250,9 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, $xml->position->count(), "Wrong count of positions");
|
$this->assertCount(0, $json, "Wrong count of positions");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetPositionsNoUserId() {
|
public function testGetPositionsNoUserId() {
|
||||||
@ -272,10 +272,9 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertCount(0, $json, "Wrong count of positions");
|
||||||
$this->assertEquals(0, $xml->position->count(), "Wrong count of positions");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetPositionsNoAuth() {
|
public function testGetPositionsNoAuth() {
|
||||||
@ -291,10 +290,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/getpositions.php", $options);
|
$response = $this->http->get("/utils/getpositions.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
|
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->position->count(), 0, "Wrong count of positions");
|
$this->assertEquals(count($json), 0, "Wrong count of positions");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -317,17 +316,17 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/gettracks.php", $options);
|
$response = $this->http->get("/utils/gettracks.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->track->count(), 2, "Wrong count of tracks");
|
$this->assertEquals(count($json), 2, "Wrong count of tracks");
|
||||||
|
|
||||||
$track = $xml->track[0];
|
$track = $json[0];
|
||||||
$this->assertEquals((int) $track->trackid, $this->testTrackId2, "Wrong track id");
|
$this->assertEquals((int) $track->id, $this->testTrackId2, "Wrong track id");
|
||||||
$this->assertEquals((string) $track->trackname, $this->testTrackName . "2", "Wrong track name");
|
$this->assertEquals((string) $track->name, $this->testTrackName . "2", "Wrong track name");
|
||||||
|
|
||||||
$track = $xml->track[1];
|
$track = $json[1];
|
||||||
$this->assertEquals((int) $track->trackid, $this->testTrackId, "Wrong track id");
|
$this->assertEquals((int) $track->id, $this->testTrackId, "Wrong track id");
|
||||||
$this->assertEquals((string) $track->trackname, $this->testTrackName, "Wrong track name");
|
$this->assertEquals((string) $track->name, $this->testTrackName, "Wrong track name");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetTracksUser() {
|
public function testGetTracksUser() {
|
||||||
@ -347,18 +346,18 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/gettracks.php", $options);
|
$response = $this->http->get("/utils/gettracks.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->track->count(), 2, "Wrong count of tracks");
|
$this->assertEquals(count($json), 2, "Wrong count of tracks");
|
||||||
|
|
||||||
$track = $xml->track[0];
|
$track = $json[0];
|
||||||
$this->assertEquals((int) $track->trackid, $this->testTrackId2, "Wrong track id");
|
$this->assertEquals((int) $track->id, $this->testTrackId2, "Wrong track id");
|
||||||
$this->assertEquals((string) $track->trackname, $this->testTrackName . "2", "Wrong track name");
|
$this->assertEquals((string) $track->name, $this->testTrackName . "2", "Wrong track name");
|
||||||
|
|
||||||
$track = $xml->track[1];
|
$track = $json[1];
|
||||||
$this->assertEquals((int) $track->trackid, $this->testTrackId, "Wrong track id");
|
$this->assertEquals((int) $track->id, $this->testTrackId, "Wrong track id");
|
||||||
$this->assertEquals((string) $track->trackname, $this->testTrackName, "Wrong track name");
|
$this->assertEquals((string) $track->name, $this->testTrackName, "Wrong track name");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetTracksOtherUser() {
|
public function testGetTracksOtherUser() {
|
||||||
$this->addTestUser($this->testUser, password_hash($this->testPass, PASSWORD_DEFAULT));
|
$this->addTestUser($this->testUser, password_hash($this->testPass, PASSWORD_DEFAULT));
|
||||||
@ -377,9 +376,9 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/gettracks.php", $options);
|
$response = $this->http->get("/utils/gettracks.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->track->count(), 0, "Wrong count of tracks");
|
$this->assertEquals(count($json), 0, "Wrong count of tracks");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetTracksNoUserId() {
|
public function testGetTracksNoUserId() {
|
||||||
@ -397,9 +396,9 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->get("/utils/gettracks.php", $options);
|
$response = $this->http->get("/utils/gettracks.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->track->count(), 0, "Wrong count of tracks");
|
$this->assertEquals(count($json), 0, "Wrong count of tracks");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetTracksNoAuth() {
|
public function testGetTracksNoAuth() {
|
||||||
@ -416,9 +415,9 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->get("/utils/gettracks.php", $options);
|
$response = $this->http->get("/utils/gettracks.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals($xml->track->count(), 0, "Wrong count of tracks");
|
$this->assertEquals(count($json), 0, "Wrong count of tracks");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -428,15 +427,19 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [ "userid" => $this->testUserId ],
|
"form_params" => [
|
||||||
|
"login" => $this->testUser,
|
||||||
|
"pass" => $this->testPass,
|
||||||
|
"oldpass" => $this->testPass
|
||||||
|
],
|
||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(401, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(401, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, "Unauthorized", "Wrong error message");
|
$this->assertEquals((string) $json->message, "Unauthorized", "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testChangePassEmpty() {
|
public function testChangePassEmpty() {
|
||||||
@ -449,13 +452,13 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, "Empty password", "Wrong error message");
|
$this->assertEquals((string) $json->message, "Empty password", "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testChangePassNoUser() {
|
public function testChangePassUserUnknown() {
|
||||||
$this->assertTrue($this->authenticate(), "Authentication failed");
|
$this->assertTrue($this->authenticate(), "Authentication failed");
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
@ -468,10 +471,28 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, "User unknown", "Wrong error message");
|
$this->assertEquals((string) $json->message, "User unknown", "Wrong error message");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testChangePassEmptyLogin() {
|
||||||
|
$this->assertTrue($this->authenticate(), "Authentication failed");
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
"http_errors" => false,
|
||||||
|
"form_params" => [
|
||||||
|
"pass" => $this->testPass,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
|
$json = json_decode($response->getBody());
|
||||||
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
|
$this->assertEquals((string) $json->message, "Empty login", "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testChangePassWrongOldpass() {
|
public function testChangePassWrongOldpass() {
|
||||||
@ -480,6 +501,7 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testAdminUser,
|
||||||
"oldpass" => "badpass",
|
"oldpass" => "badpass",
|
||||||
"pass" => "newpass",
|
"pass" => "newpass",
|
||||||
],
|
],
|
||||||
@ -487,10 +509,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, "Wrong old password", "Wrong error message");
|
$this->assertEquals((string) $json->message, "Wrong old password", "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testChangePassNoOldpass() {
|
public function testChangePassNoOldpass() {
|
||||||
@ -499,16 +521,17 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testAdminUser,
|
||||||
"pass" => "newpass",
|
"pass" => "newpass",
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, "Wrong old password", "Wrong error message");
|
$this->assertEquals((string) $json->message, "Wrong old password", "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testChangePassSelfAdmin() {
|
public function testChangePassSelfAdmin() {
|
||||||
@ -519,6 +542,7 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testAdminUser,
|
||||||
"oldpass" => $this->testAdminPass,
|
"oldpass" => $this->testAdminPass,
|
||||||
"pass" => $newPass,
|
"pass" => $newPass,
|
||||||
],
|
],
|
||||||
@ -526,9 +550,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
|
|
||||||
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users")), "Wrong actual password hash");
|
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users")), "Wrong actual password hash");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,6 +564,7 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testUser,
|
||||||
"oldpass" => $this->testPass,
|
"oldpass" => $this->testPass,
|
||||||
"pass" => $newPass,
|
"pass" => $newPass,
|
||||||
],
|
],
|
||||||
@ -548,9 +572,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
|
|
||||||
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users WHERE id = $userId")), "Wrong actual password hash");
|
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users WHERE id = $userId")), "Wrong actual password hash");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -570,9 +593,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
|
|
||||||
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users WHERE id = $userId")), "Wrong actual password hash");
|
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users WHERE id = $userId")), "Wrong actual password hash");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,10 +615,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
|
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is not false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, "Unauthorized", "Wrong error message");
|
$this->assertEquals((string) $json->message, "Unauthorized", "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* handletrack.php */
|
/* handletrack.php */
|
||||||
@ -617,9 +639,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handletrack.php", $options);
|
$response = $this->http->post("/utils/handletrack.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals($trackId2, $this->pdoGetColumn("SELECT id FROM tracks WHERE id = $trackId2"), "Wrong actual track id");
|
$this->assertEquals($trackId2, $this->pdoGetColumn("SELECT id FROM tracks WHERE id = $trackId2"), "Wrong actual track id");
|
||||||
}
|
}
|
||||||
@ -640,9 +661,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handletrack.php", $options);
|
$response = $this->http->post("/utils/handletrack.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$this->assertEquals($trackId2, $this->pdoGetColumn("SELECT id FROM tracks WHERE id = $trackId2"), "Wrong actual track id");
|
$this->assertEquals($trackId2, $this->pdoGetColumn("SELECT id FROM tracks WHERE id = $trackId2"), "Wrong actual track id");
|
||||||
}
|
}
|
||||||
@ -663,10 +683,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handletrack.php", $options);
|
$response = $this->http->post("/utils/handletrack.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleTrackUpdate() {
|
public function testHandleTrackUpdate() {
|
||||||
@ -686,9 +706,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handletrack.php", $options);
|
$response = $this->http->post("/utils/handletrack.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
|
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
$row1 = [
|
$row1 = [
|
||||||
"id" => $trackId2,
|
"id" => $trackId2,
|
||||||
@ -727,10 +746,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handletrack.php", $options);
|
$response = $this->http->post("/utils/handletrack.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -752,10 +771,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handletrack.php", $options);
|
$response = $this->http->post("/utils/handletrack.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleTrackMissingAction() {
|
public function testHandleTrackMissingAction() {
|
||||||
@ -767,10 +786,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handletrack.php", $options);
|
$response = $this->http->post("/utils/handletrack.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -785,10 +804,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleUserNonAdmin() {
|
public function testHandleUserNonAdmin() {
|
||||||
@ -803,10 +822,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -822,10 +841,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -840,10 +859,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -859,10 +878,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
|
$this->assertEquals((int) $json->error, 1, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -876,9 +895,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
|
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
$expected = [
|
$expected = [
|
||||||
"login" => $this->testUser,
|
"login" => $this->testUser,
|
||||||
@ -903,10 +921,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(1, (int) $json->error, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["userexists"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["userexists"], "Wrong error message");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -922,9 +940,8 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
|
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users WHERE login = '$this->testUser'")), "Wrong actual password hash");
|
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users WHERE login = '$this->testUser'")), "Wrong actual password hash");
|
||||||
}
|
}
|
||||||
@ -941,10 +958,10 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
|
$this->assertEquals(1, (int) $json->error, "Wrong error status");
|
||||||
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
|
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
$this->assertTrue(password_verify($this->testPass, $this->pdoGetColumn("SELECT password FROM users WHERE login = '$this->testUser'")), "Wrong actual password hash");
|
$this->assertTrue(password_verify($this->testPass, $this->pdoGetColumn("SELECT password FROM users WHERE login = '$this->testUser'")), "Wrong actual password hash");
|
||||||
}
|
}
|
||||||
@ -960,25 +977,11 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/handleuser.php", $options);
|
$response = $this->http->post("/utils/handleuser.php", $options);
|
||||||
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
|
||||||
$xml = $this->getXMLfromResponse($response);
|
$json = json_decode($response->getBody());
|
||||||
$this->assertTrue($xml !== false, "XML object is false");
|
$this->assertNotNull($json, "JSON object is null");
|
||||||
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
|
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ResponseInterface $response
|
|
||||||
* @return bool|SimpleXMLElement
|
|
||||||
*/
|
|
||||||
private function getXMLfromResponse($response) {
|
|
||||||
$xml = false;
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
try {
|
|
||||||
$xml = new SimpleXMLElement((string) $response->getBody());
|
|
||||||
} catch (Exception $e) { /* ignore */ }
|
|
||||||
return $xml;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
@ -11,15 +11,15 @@ class PositionTest extends UloggerDatabaseTestCase {
|
|||||||
$trackId = $this->addTestTrack($userId);
|
$trackId = $this->addTestTrack($userId);
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount('tracks'), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount('tracks'), "Wrong row count");
|
||||||
|
|
||||||
$posId = uPosition::add($userId, $trackId + 1, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImageId);
|
$posId = uPosition::add($userId, $trackId + 1, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImage);
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
$this->assertFalse($posId, "Adding position with nonexistant track should fail");
|
$this->assertFalse($posId, "Adding position with nonexistant track should fail");
|
||||||
|
|
||||||
$posId = uPosition::add($userId + 1, $trackId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImageId);
|
$posId = uPosition::add($userId + 1, $trackId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImage);
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
$this->assertFalse($posId, "Adding position with wrong user should fail");
|
$this->assertFalse($posId, "Adding position with wrong user should fail");
|
||||||
|
|
||||||
$posId = uPosition::add($userId, $trackId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImageId);
|
$posId = uPosition::add($userId, $trackId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImage);
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
$expected = [
|
$expected = [
|
||||||
"id" => $posId,
|
"id" => $posId,
|
||||||
@ -34,11 +34,11 @@ class PositionTest extends UloggerDatabaseTestCase {
|
|||||||
"accuracy" => $this->testAccuracy,
|
"accuracy" => $this->testAccuracy,
|
||||||
"provider" => $this->testProvider,
|
"provider" => $this->testProvider,
|
||||||
"comment" => $this->testComment,
|
"comment" => $this->testComment,
|
||||||
"image_id" => $this->testImageId
|
"image" => $this->testImage
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, user_id, track_id, " . $this->unix_timestamp('time') . " AS time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
"SELECT id, user_id, track_id, " . $this->unix_timestamp('time') . " AS time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
|
|
||||||
|
@ -41,16 +41,16 @@ class TrackTest extends UloggerDatabaseTestCase {
|
|||||||
$this->assertEquals(1, $this->getConnection()->getRowCount('tracks'), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount('tracks'), "Wrong row count");
|
||||||
|
|
||||||
$track = new uTrack($trackId + 1);
|
$track = new uTrack($trackId + 1);
|
||||||
$posId = $track->addPosition($userId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImageId);
|
$posId = $track->addPosition($userId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImage);
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
$this->assertFalse($posId, "Adding position with nonexistant track should fail");
|
$this->assertFalse($posId, "Adding position with nonexistant track should fail");
|
||||||
|
|
||||||
$track = new uTrack($trackId);
|
$track = new uTrack($trackId);
|
||||||
$posId = $track->addPosition($userId2, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImageId);
|
$posId = $track->addPosition($userId2, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImage);
|
||||||
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
$this->assertEquals(0, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
$this->assertFalse($posId, "Adding position with wrong user should fail");
|
$this->assertFalse($posId, "Adding position with wrong user should fail");
|
||||||
|
|
||||||
$posId = $track->addPosition($userId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImageId);
|
$posId = $track->addPosition($userId, $this->testTimestamp, $this->testLat, $this->testLon, $this->testAltitude, $this->testSpeed, $this->testBearing, $this->testAccuracy, $this->testProvider, $this->testComment, $this->testImage);
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
$this->assertEquals(1, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||||
$expected = [
|
$expected = [
|
||||||
"id" => $posId,
|
"id" => $posId,
|
||||||
@ -65,11 +65,11 @@ class TrackTest extends UloggerDatabaseTestCase {
|
|||||||
"accuracy" => $this->testAccuracy,
|
"accuracy" => $this->testAccuracy,
|
||||||
"provider" => $this->testProvider,
|
"provider" => $this->testProvider,
|
||||||
"comment" => $this->testComment,
|
"comment" => $this->testComment,
|
||||||
"image_id" => $this->testImageId
|
"image" => $this->testImage
|
||||||
];
|
];
|
||||||
$actual = $this->getConnection()->createQueryTable(
|
$actual = $this->getConnection()->createQueryTable(
|
||||||
"positions",
|
"positions",
|
||||||
"SELECT id, user_id, track_id, " . $this->unix_timestamp('time') . " AS time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image_id FROM positions"
|
"SELECT id, user_id, track_id, " . $this->unix_timestamp('time') . " AS time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image FROM positions"
|
||||||
);
|
);
|
||||||
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
$this->assertTableContains($expected, $actual, "Wrong actual table data");
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ before_install:
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
- composer install
|
- composer install
|
||||||
|
- npm install
|
||||||
- until netstat -atn 2>/dev/null | grep '8080.*LISTEN'; do sleep 1; done
|
- until netstat -atn 2>/dev/null | grep '8080.*LISTEN'; do sleep 1; done
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
@ -56,9 +57,14 @@ after_success:
|
|||||||
tx push -s --no-interactive
|
tx push -s --no-interactive
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
after_failure:
|
||||||
|
- docker logs ulogger
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./vendor/bin/phpunit -c .tests/phpunit.xml
|
- ./vendor/bin/phpunit -c .tests/phpunit.xml
|
||||||
|
- npm test
|
||||||
|
- npm run lint:js
|
||||||
|
- npm run lint:css
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
coverity_scan:
|
coverity_scan:
|
||||||
|
@ -37,7 +37,6 @@ RUN chown nginx.nginx /etc/nginx/conf.d/default.conf
|
|||||||
RUN rm -rf /var/www/html
|
RUN rm -rf /var/www/html
|
||||||
RUN mkdir -p /var/www/html
|
RUN mkdir -p /var/www/html
|
||||||
COPY . /var/www/html
|
COPY . /var/www/html
|
||||||
RUN grep '^[$<?]' /var/www/html/config.default.php > /var/www/html/config.php
|
|
||||||
|
|
||||||
RUN /init.sh "${DB_ROOT_PASS}" "${DB_USER_PASS}"
|
RUN /init.sh "${DB_ROOT_PASS}" "${DB_USER_PASS}"
|
||||||
|
|
||||||
|
16
README.md
@ -21,7 +21,6 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews
|
|||||||
- user authentication
|
- user authentication
|
||||||
- Google Maps
|
- Google Maps
|
||||||
- OpenLayers (OpenStreet and other layers)
|
- OpenLayers (OpenStreet and other layers)
|
||||||
- ajax
|
|
||||||
- user preferences stored in cookies
|
- user preferences stored in cookies
|
||||||
- simple admin menu
|
- simple admin menu
|
||||||
- export tracks to gpx and kml
|
- export tracks to gpx and kml
|
||||||
@ -30,6 +29,8 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews
|
|||||||
## Install
|
## Install
|
||||||
- Download zipped archive or clone the repository on your computer
|
- Download zipped archive or clone the repository on your computer
|
||||||
- Move it to your web server directory (unzip if needed)
|
- Move it to your web server directory (unzip if needed)
|
||||||
|
- Fix folder permissions: `uploads` folder (for uploaded images) should be writeable by PHP scripts
|
||||||
|
- In case of development version it is necessary to build javascript bundle from source files. You will need to install `npm` and run `npm install` and `npm run build` in root folder
|
||||||
- Create database and database user (at least SELECT, INSERT, UPDATE, DELETE privileges, CREATE, DROP for setup script, SEQUENCES for postgreSQL)
|
- Create database and database user (at least SELECT, INSERT, UPDATE, DELETE privileges, CREATE, DROP for setup script, SEQUENCES for postgreSQL)
|
||||||
- Create a copy of `config.default.php` and rename it to `config.php`. Customize it and add database credentials
|
- Create a copy of `config.default.php` and rename it to `config.php`. Customize it and add database credentials
|
||||||
- Edit `scripts/setup.php` script, enable it by setting [$enabled](https://github.com/bfabiszewski/ulogger-server/blob/master/scripts/setup.php#L21) value to `true`
|
- Edit `scripts/setup.php` script, enable it by setting [$enabled](https://github.com/bfabiszewski/ulogger-server/blob/master/scripts/setup.php#L21) value to `true`
|
||||||
@ -51,6 +52,7 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews
|
|||||||
## Tests
|
## Tests
|
||||||
- Install tests dependecies.
|
- Install tests dependecies.
|
||||||
- `composer install`
|
- `composer install`
|
||||||
|
- `npm install`
|
||||||
- Integration tests may be run against docker image. We need exposed http and optionally database ports (eg. mapped to localhost 8080 and 8081). Below example for MySQL setup.
|
- Integration tests may be run against docker image. We need exposed http and optionally database ports (eg. mapped to localhost 8080 and 8081). Below example for MySQL setup.
|
||||||
- `docker build -t ulogger .`
|
- `docker build -t ulogger .`
|
||||||
- `docker run -d --name ulogger -p 8080:80 -p 8081:3306 --expose 3306 -e ULOGGER_ENABLE_SETUP=1 ulogger`
|
- `docker run -d --name ulogger -p 8080:80 -p 8081:3306 --expose 3306 -e ULOGGER_ENABLE_SETUP=1 ulogger`
|
||||||
@ -59,13 +61,13 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews
|
|||||||
- `DB_USER=ulogger`
|
- `DB_USER=ulogger`
|
||||||
- `DB_PASS=secret2`
|
- `DB_PASS=secret2`
|
||||||
- `ULOGGER_URL="http://127.0.0.1:8080"`
|
- `ULOGGER_URL="http://127.0.0.1:8080"`
|
||||||
- Run tests
|
- PHP tests
|
||||||
- `./vendor/bin/phpunit -c .tests/phpunit.xml`
|
- `./vendor/bin/phpunit -c .tests/phpunit.xml`
|
||||||
|
- JS tests
|
||||||
## Todo
|
- `npm test`
|
||||||
- improve track editing
|
- Other tests
|
||||||
- track display filters (accurracy, provider)
|
- `npm run lint:js`
|
||||||
- improve interface on mobile devices
|
- `npm run lint:css`
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
- translations may be contributed via [Transifex](https://www.transifex.com/bfabiszewski/ulogger/).
|
- translations may be contributed via [Transifex](https://www.transifex.com/bfabiszewski/ulogger/).
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
exitWithError("Unauthorized");
|
exitWithError("Unauthorized");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
// action: authorize
|
// action: authorize
|
||||||
case "auth":
|
case "auth":
|
||||||
$login = uUtils::postString('user');
|
$login = uUtils::postString('user');
|
||||||
@ -111,16 +111,21 @@
|
|||||||
$accuracy = uUtils::postInt('accuracy');
|
$accuracy = uUtils::postInt('accuracy');
|
||||||
$provider = uUtils::postString('provider');
|
$provider = uUtils::postString('provider');
|
||||||
$comment = uUtils::postString('comment');
|
$comment = uUtils::postString('comment');
|
||||||
$imageId = uUtils::postInt('imageid');
|
$imageMeta = uUtils::requestFile('image');
|
||||||
$trackId = uUtils::postInt('trackid');
|
$trackId = uUtils::postInt('trackid');
|
||||||
|
|
||||||
if (!is_float($lat) || !is_float($lon) || !is_int($timestamp) || !is_int($trackId)) {
|
if (!is_float($lat) || !is_float($lon) || !is_int($timestamp) || !is_int($trackId)) {
|
||||||
exitWithError("Missing required parameter");
|
exitWithError("Missing required parameter");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$image = null;
|
||||||
|
if (!empty($imageMeta)) {
|
||||||
|
$image = uUpload::add($imageMeta, $trackId);
|
||||||
|
}
|
||||||
|
|
||||||
require_once(ROOT_DIR . "/helpers/position.php");
|
require_once(ROOT_DIR . "/helpers/position.php");
|
||||||
$positionId = uPosition::add($auth->user->id, $trackId,
|
$positionId = uPosition::add($auth->user->id, $trackId,
|
||||||
$timestamp, $lat, $lon, $altitude, $speed, $bearing, $accuracy, $provider, $comment, $imageId);
|
$timestamp, $lat, $lon, $altitude, $speed, $bearing, $accuracy, $provider, $comment, $image);
|
||||||
|
|
||||||
if ($positionId === false) {
|
if ($positionId === false) {
|
||||||
exitWithError("Server error");
|
exitWithError("Server error");
|
||||||
|
40
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "e0409bcb302c1bef7caa031dafc841a9",
|
"content-hash": "ffb1c6d77d755002ea20d1c1c6338b43",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "ulrichsg/getopt-php",
|
"name": "ulrichsg/getopt-php",
|
||||||
@ -1656,16 +1656,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.10.0",
|
"version": "v1.13.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||||
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
|
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
|
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
|
||||||
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
|
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -1677,7 +1677,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.9-dev"
|
"dev-master": "1.13-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -1693,13 +1693,13 @@
|
|||||||
"MIT"
|
"MIT"
|
||||||
],
|
],
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Gert de Pagter",
|
"name": "Gert de Pagter",
|
||||||
"email": "BackEndTea@gmail.com"
|
"email": "BackEndTea@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Symfony polyfill for ctype functions",
|
"description": "Symfony polyfill for ctype functions",
|
||||||
@ -1710,20 +1710,20 @@
|
|||||||
"polyfill",
|
"polyfill",
|
||||||
"portable"
|
"portable"
|
||||||
],
|
],
|
||||||
"time": "2018-08-06T14:22:27+00:00"
|
"time": "2019-11-27T13:56:44+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/yaml",
|
"name": "symfony/yaml",
|
||||||
"version": "v3.4.22",
|
"version": "v3.4.36",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/yaml.git",
|
"url": "https://github.com/symfony/yaml.git",
|
||||||
"reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d"
|
"reference": "dab657db15207879217fc81df4f875947bf68804"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/ba11776e9e6c15ad5759a07bffb15899bac75c2d",
|
"url": "https://api.github.com/repos/symfony/yaml/zipball/dab657db15207879217fc81df4f875947bf68804",
|
||||||
"reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d",
|
"reference": "dab657db15207879217fc81df4f875947bf68804",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -1769,7 +1769,7 @@
|
|||||||
],
|
],
|
||||||
"description": "Symfony Yaml Component",
|
"description": "Symfony Yaml Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-01-16T10:59:17+00:00"
|
"time": "2019-10-24T15:33:53+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vlucas/phpdotenv",
|
"name": "vlucas/phpdotenv",
|
||||||
@ -1880,6 +1880,12 @@
|
|||||||
"stability-flags": [],
|
"stability-flags": [],
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-libxml": "*"
|
||||||
|
},
|
||||||
"platform-dev": []
|
"platform-dev": []
|
||||||
}
|
}
|
||||||
|
1
css/chartist.min.css
vendored
Normal file
224
css/fonts.css
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/mem6YaGs126MiZpBA-UFUK0Udc1GAK6bt6o.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/mem6YaGs126MiZpBA-UFUK0ddc1GAK6bt6o.woff2) format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/mem6YaGs126MiZpBA-UFUK0Vdc1GAK6bt6o.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/mem6YaGs126MiZpBA-UFUK0adc1GAK6bt6o.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/mem6YaGs126MiZpBA-UFUK0Wdc1GAK6bt6o.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/mem6YaGs126MiZpBA-UFUK0Xdc1GAK6bt6o.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/mem6YaGs126MiZpBA-UFUK0Zdc1GAK6b.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(../fonts/memnYaGs126MiZpBA-UFUKWiUNhmIqOxjaPXZSk.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(../fonts/memnYaGs126MiZpBA-UFUKWiUNhvIqOxjaPXZSk.woff2) format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(../fonts/memnYaGs126MiZpBA-UFUKWiUNhnIqOxjaPXZSk.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(../fonts/memnYaGs126MiZpBA-UFUKWiUNhoIqOxjaPXZSk.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(../fonts/memnYaGs126MiZpBA-UFUKWiUNhkIqOxjaPXZSk.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(../fonts/memnYaGs126MiZpBA-UFUKWiUNhlIqOxjaPXZSk.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(../fonts/memnYaGs126MiZpBA-UFUKWiUNhrIqOxjaPX.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(../fonts/mem8YaGs126MiZpBA-UFWJ0bf8pkAp6a.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(../fonts/mem8YaGs126MiZpBA-UFUZ0bf8pkAp6a.woff2) format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(../fonts/mem8YaGs126MiZpBA-UFWZ0bf8pkAp6a.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(../fonts/mem8YaGs126MiZpBA-UFVp0bf8pkAp6a.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(../fonts/mem8YaGs126MiZpBA-UFWp0bf8pkAp6a.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(../fonts/mem8YaGs126MiZpBA-UFW50bf8pkAp6a.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(../fonts/mem8YaGs126MiZpBA-UFVZ0bf8pkAg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/mem5YaGs126MiZpBA-UN7rgOX-hpKKSTj5PW.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/mem5YaGs126MiZpBA-UN7rgOVuhpKKSTj5PW.woff2) format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/mem5YaGs126MiZpBA-UN7rgOXuhpKKSTj5PW.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/mem5YaGs126MiZpBA-UN7rgOUehpKKSTj5PW.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/mem5YaGs126MiZpBA-UN7rgOXehpKKSTj5PW.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/mem5YaGs126MiZpBA-UN7rgOXOhpKKSTj5PW.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/mem5YaGs126MiZpBA-UN7rgOUuhpKKSTjw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
606
css/main.css
@ -19,238 +19,360 @@
|
|||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #666;
|
background-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #bce;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
color: #bce;
|
||||||
}
|
}
|
||||||
|
|
||||||
:link, :visited {
|
:link, :visited {
|
||||||
color: #bce;
|
color: #bce;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
width: 150px;
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
width: 150px;
|
||||||
padding-top: 0.2em;
|
padding-top: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
flex-grow: 1;
|
||||||
|
order: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map-canvas {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
font-family: "Open Sans", Verdana, sans-serif;
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-weight: bold;
|
||||||
|
float: right;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
order: 2;
|
||||||
|
width: 165px;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
background-color: #666;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu-content {
|
||||||
|
padding: 10px 0 3em 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
line-height: 3em;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 165px;
|
||||||
|
padding-left: 10px;
|
||||||
|
color: lightgray;
|
||||||
|
background-color: rgba(102, 102, 102, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu-button {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 28px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1900;
|
||||||
|
top: 5px;
|
||||||
|
right: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 35px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
border-width: 1px 0 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #bce;
|
||||||
|
border-radius: 11px 0 0 11px;
|
||||||
|
background-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu-button a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu-button a::after {
|
||||||
|
content: "»";
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu.menu-hidden {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu.menu-hidden #menu-button {
|
||||||
|
font-weight: normal;
|
||||||
|
border-color: white;
|
||||||
|
background-color: rgba(0, 60, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu.menu-hidden #menu-button a::after {
|
||||||
|
content: "«";
|
||||||
|
}
|
||||||
|
|
||||||
#menu input,
|
#menu input,
|
||||||
#login input {
|
#login input {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
#menu input[type = "submit"],
|
|
||||||
#login input[type = "submit"] {
|
#menu input[type="submit"],
|
||||||
background-color: black;
|
#login input[type="submit"] {
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
}
|
background-color: black;
|
||||||
#menu input[type = "checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.menulink {
|
|
||||||
display: block;
|
|
||||||
margin-top: .2em;
|
|
||||||
}
|
|
||||||
#main {
|
|
||||||
height: 100%;
|
|
||||||
margin-right: 165px;
|
|
||||||
}
|
|
||||||
#map-canvas {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
#menu {
|
|
||||||
font-family: 'Open Sans', Verdana, sans-serif;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
font-size: 0.7em;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
float: right;
|
|
||||||
width: 165px;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #666;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
#menu-content {
|
|
||||||
padding: 10px 0 3em 10px;
|
|
||||||
}
|
|
||||||
#footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom:0;
|
|
||||||
width: 165px;
|
|
||||||
line-height: 3em;
|
|
||||||
padding-left: 10px;
|
|
||||||
background-color:rgba(102, 102, 102, 0.9);
|
|
||||||
color: lightgray;
|
|
||||||
}
|
|
||||||
#menu-close {
|
|
||||||
background-color: #666;
|
|
||||||
opacity: 0.9;
|
|
||||||
position: absolute;
|
|
||||||
top: 55px;
|
|
||||||
right: 165px;
|
|
||||||
z-index: 1900;
|
|
||||||
width: 18px;
|
|
||||||
height: 20px;
|
|
||||||
line-height: 18px;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bolder;
|
|
||||||
border-radius: 11px 0 0 11px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#user, #track, #summary, #export, #import, #other, #units {
|
#menu input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for=user] {
|
||||||
|
display: block;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: block;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
#summary div {
|
|
||||||
padding-top: .3em;
|
.section:first-child {
|
||||||
|
padding-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#input-file {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#summary div {
|
||||||
|
padding-top: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
#summary div img {
|
#summary div img {
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login {
|
#login {
|
||||||
font-family: 'Open Sans', Verdana, sans-serif;
|
font-family: "Open Sans", Verdana, sans-serif;
|
||||||
|
font-size: 0.8em;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 10%;
|
top: 10%;
|
||||||
background-color: #444;
|
|
||||||
width: 30%;
|
width: 30%;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
font-size: 0.8em;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
#title {
|
#title {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
padding-bottom: 0.5em;
|
|
||||||
padding-top: 0.6em;
|
padding-top: 0.6em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitle {
|
#subtitle {
|
||||||
padding-bottom: 2em;
|
padding-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#error {
|
#error {
|
||||||
padding-top: 1.2em;
|
padding-top: 1.2em;
|
||||||
color: yellow;
|
color: yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popup {
|
#popup {
|
||||||
font-family: 'Open Sans', Verdana, sans-serif;
|
font-family: "Open Sans", Verdana, sans-serif;
|
||||||
|
max-width: 25em;
|
||||||
|
background-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pheader {
|
#pheader {
|
||||||
|
font-size: 0.9rem;
|
||||||
float: left;
|
float: left;
|
||||||
font-size: .9rem;
|
padding-bottom: 0.5rem;
|
||||||
color: #297b9a;
|
color: #bce;
|
||||||
padding-bottom: .5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pheader div {
|
#pheader div {
|
||||||
float: left;
|
float: left;
|
||||||
padding-right: 2em;
|
padding-right: 2em;
|
||||||
}
|
}
|
||||||
#pbody {
|
|
||||||
clear: both;
|
#pheader div img {
|
||||||
padding-top: .2rem;
|
background-image: radial-gradient(circle closest-side, #bfbfbc, #666);
|
||||||
border-top: 1px solid #6cdae7;;
|
|
||||||
font-size: .8rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pbody {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.3rem;
|
||||||
|
clear: both;
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #e6e2e2;
|
||||||
|
border-top: 1px solid #bce;
|
||||||
|
}
|
||||||
|
|
||||||
#pcomments {
|
#pcomments {
|
||||||
clear: both;
|
clear: both;
|
||||||
color: #903;
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
color: #e6e6e6;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #777676;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pimage {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pimage img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 25em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pimage img:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
#pleft, #pright {
|
#pleft, #pright {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
#pbody .smaller {
|
|
||||||
color: gray;
|
#pleft img {
|
||||||
font-size: .9em;
|
background-image: radial-gradient(circle closest-side, #bfbfbc, #666);
|
||||||
}
|
|
||||||
#pfooter {
|
|
||||||
font-size: .6rem;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
#bottom {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10000;
|
|
||||||
}
|
|
||||||
#chart {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0; left:0; right: 0;
|
|
||||||
height: 200px;
|
|
||||||
margin-right: 165px;
|
|
||||||
background-color: white;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
#close {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 175px;
|
|
||||||
right: 175px;
|
|
||||||
z-index: 10001;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#close a, #close:link, #close:visited {
|
#pbody .smaller {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #cacaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pfooter {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
padding-top: 20px;
|
||||||
|
color: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pfooter div:first-child {
|
||||||
|
width: 40%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pfooter div:last-child {
|
||||||
|
width: 40%;
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottom {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chart {
|
||||||
|
font-family: "Open Sans", Verdana, sans-serif;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: -15px;
|
||||||
|
left: 0;
|
||||||
|
height: 200px;
|
||||||
|
padding: 0 10px;
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chart-close {
|
||||||
|
font-size: 0.8em;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10001;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 160px;
|
||||||
|
cursor: pointer;
|
||||||
color: #5070af;
|
color: #5070af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mi {
|
.mi {
|
||||||
color:white;
|
font-style: italic;
|
||||||
padding-right:0.1em;
|
padding-right: 0.1em;
|
||||||
font-style:italic;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal {
|
#modal {
|
||||||
font-family: 'Open Sans', Verdana, sans-serif;
|
font-family: "Open Sans", Verdana, sans-serif;
|
||||||
display: block;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10010;
|
z-index: 10010;
|
||||||
left: 0;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
|
||||||
background-color: black; /* fallback */
|
|
||||||
background-color: rgba(0,0,0,0.4);
|
|
||||||
padding-top: 10%;
|
padding-top: 10%;
|
||||||
|
background-color: black; /* fallback */
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal-header {
|
#modal-header {
|
||||||
top: 20px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: right;
|
top: 20px;
|
||||||
margin: 0 auto;
|
|
||||||
width: 40%;
|
width: 40%;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal-header button {
|
#modal-header button {
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
border: none;
|
border: none;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal-body {
|
#modal-body {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
color: white;
|
|
||||||
background-color: rgba(102, 102, 102, 0.9);
|
|
||||||
margin: 0 auto 15% auto;
|
|
||||||
border: 1px solid #888;
|
|
||||||
width: 40%;
|
width: 40%;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
|
margin: 0 auto 15% auto;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 10px;
|
color: white;
|
||||||
|
border: 1px solid #888;
|
||||||
-moz-border-radius: 10px;
|
-moz-border-radius: 10px;
|
||||||
-webkit-border-radius: 10px;
|
-webkit-border-radius: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgba(102, 102, 102, 0.9);
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal-body .buttons {
|
#modal-body .buttons {
|
||||||
@ -258,27 +380,53 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#modal input[type=text], #modal input[type=password] {
|
#modal input[type=text], #modal input[type=password] {
|
||||||
width: 100%;
|
|
||||||
padding: 0.4em;
|
|
||||||
margin: 0.8em 0;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid #ccc;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 5px;
|
width: 100%;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding: 0.4em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
-moz-border-radius: 5px;
|
-moz-border-radius: 5px;
|
||||||
-webkit-border-radius: 5px;
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal.image {
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 0;
|
||||||
|
background-color: rgba(45, 45, 45, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal.image #modal-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 87vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal.image #modal-body {
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgb(45, 45, 45);
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal.image #modal-header {
|
||||||
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
color: white;
|
|
||||||
background-color: #434343;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid white;
|
|
||||||
border-radius: 5px;
|
|
||||||
-moz-border-radius: 5px;
|
|
||||||
-webkit-border-radius: 5px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid white;
|
||||||
|
-moz-border-radius: 5px;
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #434343;
|
||||||
|
}
|
||||||
|
|
||||||
|
button > * {
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cancel {
|
#cancel {
|
||||||
@ -286,131 +434,205 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.red-button {
|
.red-button {
|
||||||
color: white;
|
|
||||||
float: right;
|
float: right;
|
||||||
background-color: red;
|
padding: 0.1em 0.4em;
|
||||||
padding: .1em .4em;
|
color: white;
|
||||||
border-radius: 10px;
|
|
||||||
-moz-border-radius: 10px;
|
-moz-border-radius: 10px;
|
||||||
-webkit-border-radius: 10px;
|
-webkit-border-radius: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
#user-menu {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: gray;
|
|
||||||
padding: 1em;
|
|
||||||
width: 130px;
|
|
||||||
border: 1px solid #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown a {
|
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: .5em;
|
width: 130px;
|
||||||
padding-top: .5em;
|
padding: 1em;
|
||||||
|
border: 1px solid #888;
|
||||||
|
background-color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show { display: block; }
|
#user-menu.menu-hidden, a.menu-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.icon { height: 1.4em; }
|
#user-menu a {
|
||||||
|
display: block;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.u { text-decoration: underline; }
|
.icon {
|
||||||
|
height: 1.4em;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-title {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
animation: blink 1s linear infinite;
|
animation: blink 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
50% { opacity: 0; }
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* chart */
|
||||||
|
.ct-point {
|
||||||
|
transition: 0.3s;
|
||||||
|
stroke-width: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-point:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
stroke-width: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-point-hilight {
|
||||||
|
stroke-width: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-point-selected {
|
||||||
|
stroke-width: 10px !important;
|
||||||
|
stroke: #f4c63d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-line {
|
||||||
|
stroke-width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-axis-title {
|
||||||
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* openlayers 3 popup */
|
/* openlayers 3 popup */
|
||||||
.ol-popup {
|
.ol-popup {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: white;
|
|
||||||
-webkit-filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2));
|
|
||||||
filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2));
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid #cccccc;
|
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
left: -50px;
|
left: -50px;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #666;
|
||||||
|
-webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
|
||||||
|
filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
|
||||||
}
|
}
|
||||||
.ol-popup:after, .ol-popup:before {
|
|
||||||
top: 100%;
|
.ol-popup::after, .ol-popup::before {
|
||||||
border: solid transparent;
|
|
||||||
content: " ";
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
content: " ";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
border: solid transparent;
|
||||||
}
|
}
|
||||||
.ol-popup:after {
|
|
||||||
border-top-color: white;
|
.ol-popup::after {
|
||||||
border-width: 10px;
|
|
||||||
left: 48px;
|
left: 48px;
|
||||||
margin-left: -10px;
|
margin-left: -10px;
|
||||||
|
border-width: 10px;
|
||||||
|
border-top-color: #666;
|
||||||
}
|
}
|
||||||
.ol-popup:before {
|
|
||||||
border-top-color: #cccccc;
|
.ol-popup::before {
|
||||||
border-width: 11px;
|
|
||||||
left: 48px;
|
left: 48px;
|
||||||
margin-left: -11px;
|
margin-left: -11px;
|
||||||
|
border-width: 11px;
|
||||||
|
border-top-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-popup-closer {
|
.ol-popup-closer {
|
||||||
text-decoration: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: -5px;
|
||||||
right: 8px;
|
right: -10px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-image: url(../images/close.svg) !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
}
|
}
|
||||||
.ol-popup-closer:after {
|
|
||||||
content: "✖";
|
.ol-overlay-container {
|
||||||
|
background-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google Maps InfoWindow */
|
||||||
|
.gm-style .gm-style-iw-c {
|
||||||
|
background-color: #666 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-style .gm-style-iw-t::after {
|
||||||
|
background: linear-gradient(45deg, rgb(102, 102, 102) 50%, rgba(255, 255, 255, 0) 51%, rgba(255, 255, 255, 0) 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-style-iw button {
|
||||||
|
background-image: url(../images/close.svg) !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-style-iw button img {
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#switcher {
|
#switcher {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
|
display: none;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-layerswitcher {
|
.ol-layerswitcher {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
|
padding: 0.5em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: rgba(0, 60, 136, .5);
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
font-family: sans-serif;
|
background-color: rgba(0, 60, 136, 0.5);
|
||||||
font-weight: bold;
|
|
||||||
font-size: .9em;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-layerswitcher:hover {
|
.ol-layerswitcher:hover {
|
||||||
background-color: rgba(0, 60, 136, .7)
|
background-color: rgba(0, 60, 136, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-layerswitcher label {
|
.ol-layerswitcher label {
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
margin: .5em 0;
|
margin: 0.5em 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-layerswitcher label:hover {
|
.ol-layerswitcher label:hover {
|
||||||
color: #c8dcf2;
|
color: #c8dcf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-layerswitcher input {
|
.ol-layerswitcher input {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
label.ol-datalayer {
|
label.ol-datalayer {
|
||||||
margin-top: 1.5em;
|
margin-top: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-datalayer ~ .ol-datalayer {
|
.ol-datalayer ~ .ol-datalayer {
|
||||||
margin-top: .5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-switcher-button {
|
.ol-switcher-button {
|
||||||
top: 6.6em;
|
top: 6.6em;
|
||||||
left: .5em;
|
left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-touch .ol-switcher-button {
|
.ol-touch .ol-switcher-button {
|
||||||
top: 10em;
|
top: 10em;
|
||||||
}
|
}
|
||||||
|
202
fonts/OpenSans.LICENSE
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
5
fonts/OpenSans.README
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Open Sans is a humanist sans serif typeface designed by Steve Matteson,
|
||||||
|
Type Director of Ascender Corp. This version contains the complete 897 character set,
|
||||||
|
which includes the standard ISO Latin 1, Latin CE, Greek and Cyrillic character sets.
|
||||||
|
Open Sans was designed with an upright stress, open forms and a neutral, yet friendly appearance.
|
||||||
|
It was optimized for print, web, and mobile interfaces, and has excellent legibility characteristics in its letterforms.
|
BIN
fonts/mem5YaGs126MiZpBA-UN7rgOUehpKKSTj5PW.woff2
Normal file
BIN
fonts/mem5YaGs126MiZpBA-UN7rgOUuhpKKSTjw.woff2
Normal file
BIN
fonts/mem5YaGs126MiZpBA-UN7rgOVuhpKKSTj5PW.woff2
Normal file
BIN
fonts/mem5YaGs126MiZpBA-UN7rgOX-hpKKSTj5PW.woff2
Normal file
BIN
fonts/mem5YaGs126MiZpBA-UN7rgOXOhpKKSTj5PW.woff2
Normal file
BIN
fonts/mem5YaGs126MiZpBA-UN7rgOXehpKKSTj5PW.woff2
Normal file
BIN
fonts/mem5YaGs126MiZpBA-UN7rgOXuhpKKSTj5PW.woff2
Normal file
BIN
fonts/mem6YaGs126MiZpBA-UFUK0Udc1GAK6bt6o.woff2
Normal file
BIN
fonts/mem6YaGs126MiZpBA-UFUK0Vdc1GAK6bt6o.woff2
Normal file
BIN
fonts/mem6YaGs126MiZpBA-UFUK0Wdc1GAK6bt6o.woff2
Normal file
BIN
fonts/mem6YaGs126MiZpBA-UFUK0Xdc1GAK6bt6o.woff2
Normal file
BIN
fonts/mem6YaGs126MiZpBA-UFUK0Zdc1GAK6b.woff2
Normal file
BIN
fonts/mem6YaGs126MiZpBA-UFUK0adc1GAK6bt6o.woff2
Normal file
BIN
fonts/mem6YaGs126MiZpBA-UFUK0ddc1GAK6bt6o.woff2
Normal file
BIN
fonts/mem8YaGs126MiZpBA-UFUZ0bf8pkAp6a.woff2
Normal file
BIN
fonts/mem8YaGs126MiZpBA-UFVZ0bf8pkAg.woff2
Normal file
BIN
fonts/mem8YaGs126MiZpBA-UFVp0bf8pkAp6a.woff2
Normal file
BIN
fonts/mem8YaGs126MiZpBA-UFW50bf8pkAp6a.woff2
Normal file
BIN
fonts/mem8YaGs126MiZpBA-UFWJ0bf8pkAp6a.woff2
Normal file
BIN
fonts/mem8YaGs126MiZpBA-UFWZ0bf8pkAp6a.woff2
Normal file
BIN
fonts/mem8YaGs126MiZpBA-UFWp0bf8pkAp6a.woff2
Normal file
BIN
fonts/memnYaGs126MiZpBA-UFUKWiUNhkIqOxjaPXZSk.woff2
Normal file
BIN
fonts/memnYaGs126MiZpBA-UFUKWiUNhlIqOxjaPXZSk.woff2
Normal file
BIN
fonts/memnYaGs126MiZpBA-UFUKWiUNhmIqOxjaPXZSk.woff2
Normal file
BIN
fonts/memnYaGs126MiZpBA-UFUKWiUNhnIqOxjaPXZSk.woff2
Normal file
BIN
fonts/memnYaGs126MiZpBA-UFUKWiUNhoIqOxjaPXZSk.woff2
Normal file
BIN
fonts/memnYaGs126MiZpBA-UFUKWiUNhrIqOxjaPX.woff2
Normal file
BIN
fonts/memnYaGs126MiZpBA-UFUKWiUNhvIqOxjaPXZSk.woff2
Normal file
@ -27,6 +27,7 @@
|
|||||||
*/
|
*/
|
||||||
class uAuth {
|
class uAuth {
|
||||||
|
|
||||||
|
/** @var bool Is user authenticated */
|
||||||
private $isAuthenticated = false;
|
private $isAuthenticated = false;
|
||||||
/** @var null|uUser */
|
/** @var null|uUser */
|
||||||
public $user = null;
|
public $user = null;
|
||||||
@ -40,6 +41,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user instance stored in session
|
||||||
|
*/
|
||||||
|
public function updateSession() {
|
||||||
|
if ($this->isAuthenticated()) {
|
||||||
|
$this->user->storeInSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is user authenticated
|
* Is user authenticated
|
||||||
*
|
*
|
||||||
|
@ -27,59 +27,108 @@
|
|||||||
* Handles config values
|
* Handles config values
|
||||||
*/
|
*/
|
||||||
class uConfig {
|
class uConfig {
|
||||||
// version number
|
/**
|
||||||
static $version = "0.6";
|
* @var string Version number
|
||||||
|
*/
|
||||||
|
static $version = "1.0-beta";
|
||||||
|
|
||||||
// default map drawing framework
|
/**
|
||||||
|
* @var string Default map drawing framework
|
||||||
|
*/
|
||||||
static $mapapi = "openlayers";
|
static $mapapi = "openlayers";
|
||||||
|
|
||||||
// gmaps key
|
/**
|
||||||
|
* @var string|null Google maps key
|
||||||
|
*/
|
||||||
static $gkey = null;
|
static $gkey = null;
|
||||||
|
|
||||||
// openlayers additional map layers
|
/**
|
||||||
|
* @var array Openlayers additional map layers
|
||||||
|
*/
|
||||||
static $ol_layers = [];
|
static $ol_layers = [];
|
||||||
|
|
||||||
// default coordinates for initial map
|
/**
|
||||||
|
* @var float Default latitude for initial map
|
||||||
|
*/
|
||||||
static $init_latitude = 52.23;
|
static $init_latitude = 52.23;
|
||||||
|
/**
|
||||||
|
* @var float Default longitude for initial map
|
||||||
|
*/
|
||||||
static $init_longitude = 21.01;
|
static $init_longitude = 21.01;
|
||||||
|
|
||||||
// MySQL config
|
/**
|
||||||
static $dbdsn = ""; // database dsn
|
* @var string Database dsn
|
||||||
static $dbuser = ""; // database user
|
*/
|
||||||
static $dbpass = ""; // database pass
|
static $dbdsn = "";
|
||||||
static $dbprefix = ""; // optional table names prefix, eg. "ulogger_"
|
/**
|
||||||
|
* @var string Database user
|
||||||
|
*/
|
||||||
|
static $dbuser = "";
|
||||||
|
/**
|
||||||
|
* @var string Database pass
|
||||||
|
*/
|
||||||
|
static $dbpass = "";
|
||||||
|
/**
|
||||||
|
* @var string Optional table names prefix, eg. "ulogger_"
|
||||||
|
*/
|
||||||
|
static $dbprefix = "";
|
||||||
|
|
||||||
// require login/password authentication
|
/**
|
||||||
|
* @var bool Require login/password authentication
|
||||||
|
*/
|
||||||
static $require_authentication = true;
|
static $require_authentication = true;
|
||||||
|
|
||||||
// all users tracks are visible to authenticated user
|
/**
|
||||||
|
* @var bool All users tracks are visible to authenticated user
|
||||||
|
*/
|
||||||
static $public_tracks = false;
|
static $public_tracks = false;
|
||||||
|
|
||||||
// admin user who has access to all users locations
|
/**
|
||||||
// none if empty
|
* @var string Admin user who has access to all users locations
|
||||||
|
* none if empty
|
||||||
|
*/
|
||||||
static $admin_user = "";
|
static $admin_user = "";
|
||||||
|
|
||||||
// miniumum required length of user password
|
/**
|
||||||
|
* @var int Miniumum required length of user password
|
||||||
|
*/
|
||||||
static $pass_lenmin = 12;
|
static $pass_lenmin = 12;
|
||||||
|
|
||||||
// required strength of user password
|
/**
|
||||||
// 0 = no requirements,
|
* @var int Required strength of user password
|
||||||
// 1 = require mixed case letters (lower and upper),
|
* 0 = no requirements,
|
||||||
// 2 = require mixed case and numbers
|
* 1 = require mixed case letters (lower and upper),
|
||||||
// 3 = require mixed case, numbers and non-alphanumeric characters
|
* 2 = require mixed case and numbers
|
||||||
|
* 3 = require mixed case, numbers and non-alphanumeric characters
|
||||||
|
*/
|
||||||
static $pass_strength = 2;
|
static $pass_strength = 2;
|
||||||
|
|
||||||
// Default interval in seconds for live auto reload
|
/**
|
||||||
|
* @var int Default interval in seconds for live auto reload
|
||||||
|
*/
|
||||||
static $interval = 10;
|
static $interval = 10;
|
||||||
|
|
||||||
// Default language
|
/**
|
||||||
|
* @var string Default language code
|
||||||
|
*/
|
||||||
static $lang = "en";
|
static $lang = "en";
|
||||||
|
|
||||||
// units
|
/**
|
||||||
|
* @var string Default units
|
||||||
|
*/
|
||||||
static $units = "metric";
|
static $units = "metric";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Stroke weight
|
||||||
|
*/
|
||||||
static $strokeWeight = 2;
|
static $strokeWeight = 2;
|
||||||
|
/**
|
||||||
|
* @var string Stroke color
|
||||||
|
*/
|
||||||
static $strokeColor = '#ff0000';
|
static $strokeColor = '#ff0000';
|
||||||
|
/**
|
||||||
|
* @var int Stroke opacity
|
||||||
|
*/
|
||||||
static $strokeOpacity = 1;
|
static $strokeOpacity = 1;
|
||||||
|
|
||||||
private static $fileLoaded = false;
|
private static $fileLoaded = false;
|
||||||
@ -109,7 +158,7 @@
|
|||||||
include_once($configFile);
|
include_once($configFile);
|
||||||
|
|
||||||
if (isset($mapapi)) { self::$mapapi = $mapapi; }
|
if (isset($mapapi)) { self::$mapapi = $mapapi; }
|
||||||
if (isset($gkey)) { self::$gkey = $gkey; }
|
if (isset($gkey) && !empty($gkey)) { self::$gkey = $gkey; }
|
||||||
if (isset($ol_layers)) { self::$ol_layers = $ol_layers; }
|
if (isset($ol_layers)) { self::$ol_layers = $ol_layers; }
|
||||||
if (isset($init_latitude)) { self::$init_latitude = $init_latitude; }
|
if (isset($init_latitude)) { self::$init_latitude = $init_latitude; }
|
||||||
if (isset($init_longitude)) { self::$init_longitude = $init_longitude; }
|
if (isset($init_longitude)) { self::$init_longitude = $init_longitude; }
|
||||||
|
@ -17,8 +17,9 @@
|
|||||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once(ROOT_DIR . "/helpers/db.php");
|
require_once(ROOT_DIR . "/helpers/db.php");
|
||||||
require_once(ROOT_DIR . "/helpers/track.php");
|
require_once(ROOT_DIR . "/helpers/track.php");
|
||||||
|
require_once(ROOT_DIR . "/helpers/upload.php");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Positions handling
|
* Positions handling
|
||||||
@ -51,9 +52,9 @@
|
|||||||
/** @param String Provider */
|
/** @param String Provider */
|
||||||
public $provider;
|
public $provider;
|
||||||
/** @param String Comment */
|
/** @param String Comment */
|
||||||
public $comment; // not used yet
|
public $comment;
|
||||||
/** @param int Image id */
|
/** @param String Image path */
|
||||||
public $imageId; // not used yet
|
public $image;
|
||||||
|
|
||||||
public $isValid = false;
|
public $isValid = false;
|
||||||
|
|
||||||
@ -66,11 +67,11 @@
|
|||||||
if (!empty($positionId)) {
|
if (!empty($positionId)) {
|
||||||
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
||||||
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
||||||
p.comment, p.image_id, u.login, t.name
|
p.comment, p.image, u.login, t.name
|
||||||
FROM " . self::db()->table('positions') . " p
|
FROM " . self::db()->table('positions') . " p
|
||||||
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
||||||
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
||||||
WHERE id = ? LIMIT 1";
|
WHERE p.id = ? LIMIT 1";
|
||||||
$params = [ $positionId ];
|
$params = [ $positionId ];
|
||||||
try {
|
try {
|
||||||
$this->loadWithQuery($query, $params);
|
$this->loadWithQuery($query, $params);
|
||||||
@ -90,6 +91,15 @@
|
|||||||
return uDb::getInstance();
|
return uDb::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has image
|
||||||
|
*
|
||||||
|
* @return bool True if has image
|
||||||
|
*/
|
||||||
|
public function hasImage() {
|
||||||
|
return !empty($this->image);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add position
|
* Add position
|
||||||
*
|
*
|
||||||
@ -104,27 +114,27 @@
|
|||||||
* @param int $accuracy Optional
|
* @param int $accuracy Optional
|
||||||
* @param string $provider Optional
|
* @param string $provider Optional
|
||||||
* @param string $comment Optional
|
* @param string $comment Optional
|
||||||
* @param int $imageId Optional
|
* @param int $image Optional
|
||||||
* @return int|bool New position id in database, false on error
|
* @return int|bool New position id in database, false on error
|
||||||
*/
|
*/
|
||||||
public static function add($userId, $trackId, $timestamp, $lat, $lon,
|
public static function add($userId, $trackId, $timestamp, $lat, $lon,
|
||||||
$altitude = NULL, $speed = NULL, $bearing = NULL, $accuracy = NULL,
|
$altitude = NULL, $speed = NULL, $bearing = NULL, $accuracy = NULL,
|
||||||
$provider = NULL, $comment = NULL, $imageId = NULL) {
|
$provider = NULL, $comment = NULL, $image = NULL) {
|
||||||
$positionId = false;
|
$positionId = false;
|
||||||
if (is_numeric($lat) && is_numeric($lon) && is_numeric($timestamp) && is_numeric($userId) && is_numeric($trackId)) {
|
if (is_numeric($lat) && is_numeric($lon) && is_numeric($timestamp) && is_numeric($userId) && is_numeric($trackId)) {
|
||||||
$track = new uTrack($trackId);
|
$track = new uTrack($trackId);
|
||||||
if ($track->isValid && $track->userId == $userId) {
|
if ($track->isValid && $track->userId === $userId) {
|
||||||
try {
|
try {
|
||||||
$table = self::db()->table('positions');
|
$table = self::db()->table('positions');
|
||||||
$query = "INSERT INTO $table
|
$query = "INSERT INTO $table
|
||||||
(user_id, track_id,
|
(user_id, track_id,
|
||||||
time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image_id)
|
time, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, image)
|
||||||
VALUES (?, ?, " . self::db()->from_unixtime('?') . ", ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
VALUES (?, ?, " . self::db()->from_unixtime('?') . ", ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
$stmt = self::db()->prepare($query);
|
$stmt = self::db()->prepare($query);
|
||||||
$params = [ $userId, $trackId,
|
$params = [ $userId, $trackId,
|
||||||
$timestamp, $lat, $lon, $altitude, $speed, $bearing, $accuracy, $provider, $comment, $imageId ];
|
$timestamp, $lat, $lon, $altitude, $speed, $bearing, $accuracy, $provider, $comment, $image ];
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$positionId = self::db()->lastInsertId("${table}_id_seq");
|
$positionId = (int) self::db()->lastInsertId("${table}_id_seq");
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
syslog(LOG_ERR, $e->getMessage());
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
@ -134,6 +144,70 @@
|
|||||||
return $positionId;
|
return $positionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save position to database
|
||||||
|
*
|
||||||
|
* @return bool True if success, false otherwise
|
||||||
|
*/
|
||||||
|
public function update() {
|
||||||
|
$ret = false;
|
||||||
|
if ($this->isValid) {
|
||||||
|
try {
|
||||||
|
$query = "UPDATE " . self::db()->table('positions') . " SET
|
||||||
|
time = " . self::db()->from_unixtime('?') . ", user_id = ?, track_id = ?, latitude = ?, longitude = ?, altitude = ?,
|
||||||
|
speed = ?, bearing = ?, accuracy = ?, provider = ?, comment = ?, image = ? WHERE id = ?";
|
||||||
|
$stmt = self::db()->prepare($query);
|
||||||
|
$params = [
|
||||||
|
$this->timestamp,
|
||||||
|
$this->userId,
|
||||||
|
$this->trackId,
|
||||||
|
$this->latitude,
|
||||||
|
$this->longitude,
|
||||||
|
$this->altitude,
|
||||||
|
$this->speed,
|
||||||
|
$this->bearing,
|
||||||
|
$this->accuracy,
|
||||||
|
$this->provider,
|
||||||
|
$this->comment,
|
||||||
|
$this->image,
|
||||||
|
$this->id
|
||||||
|
];
|
||||||
|
$stmt->execute($params);
|
||||||
|
$ret = true;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// TODO: handle exception
|
||||||
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete positions
|
||||||
|
*
|
||||||
|
* @return bool True if success, false otherwise
|
||||||
|
*/
|
||||||
|
public function delete() {
|
||||||
|
$ret = false;
|
||||||
|
if ($this->isValid) {
|
||||||
|
try {
|
||||||
|
$query = "DELETE FROM " . self::db()->table('positions') . " WHERE id = ?";
|
||||||
|
$stmt = self::db()->prepare($query);
|
||||||
|
$stmt->execute([ $this->id ]);
|
||||||
|
if ($this->hasImage()) {
|
||||||
|
uUpload::delete($this->image);
|
||||||
|
}
|
||||||
|
$ret = true;
|
||||||
|
$this->id = NULL;
|
||||||
|
$this->isValid = false;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// TODO: handle exception
|
||||||
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all user's positions, optionally limit to given track
|
* Delete all user's positions, optionally limit to given track
|
||||||
*
|
*
|
||||||
@ -151,6 +225,7 @@
|
|||||||
$where .= " AND track_id = ?";
|
$where .= " AND track_id = ?";
|
||||||
$args[] = $trackId;
|
$args[] = $trackId;
|
||||||
}
|
}
|
||||||
|
self::removeImages($userId, $trackId);
|
||||||
try {
|
try {
|
||||||
$query = "DELETE FROM " . self::db()->table('positions') . " $where";
|
$query = "DELETE FROM " . self::db()->table('positions') . " $where";
|
||||||
$stmt = self::db()->prepare($query);
|
$stmt = self::db()->prepare($query);
|
||||||
@ -181,7 +256,7 @@
|
|||||||
}
|
}
|
||||||
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
||||||
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
||||||
p.comment, p.image_id, u.login, t.name
|
p.comment, p.image, u.login, t.name
|
||||||
FROM " . self::db()->table('positions') . " p
|
FROM " . self::db()->table('positions') . " p
|
||||||
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
||||||
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
||||||
@ -205,7 +280,7 @@
|
|||||||
public static function getLastAllUsers() {
|
public static function getLastAllUsers() {
|
||||||
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
||||||
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
||||||
p.comment, p.image_id, u.login, t.name
|
p.comment, p.image, u.login, t.name
|
||||||
FROM " . self::db()->table('positions') . " p
|
FROM " . self::db()->table('positions') . " p
|
||||||
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
||||||
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
||||||
@ -224,25 +299,30 @@
|
|||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// TODO: handle exception
|
// TODO: handle exception
|
||||||
syslog(LOG_ERR, $e->getMessage());
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
|
$positionsArr = false;
|
||||||
}
|
}
|
||||||
return $positionsArr;
|
return $positionsArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get array of all positions
|
* Get array of all positions
|
||||||
*
|
*
|
||||||
* @param int $userId Optional limit to given user id
|
* @param int $userId Optional limit to given user id
|
||||||
* @param int $trackId Optional limit to given track id
|
* @param int $trackId Optional limit to given track id
|
||||||
* @return uPosition[]|bool Array of uPosition positions, false on error
|
* @param int $afterId Optional limit to positions with id greater then given id
|
||||||
*/
|
* @param array $rules Optional rules
|
||||||
public static function getAll($userId = NULL, $trackId = NULL) {
|
* @return uPosition[]|bool Array of uPosition positions, false on error
|
||||||
$rules = [];
|
*/
|
||||||
|
public static function getAll($userId = NULL, $trackId = NULL, $afterId = NULL, $rules = []) {
|
||||||
if (!empty($userId)) {
|
if (!empty($userId)) {
|
||||||
$rules[] = "p.user_id = " . self::db()->quote($userId);
|
$rules[] = "p.user_id = " . self::db()->quote($userId);
|
||||||
}
|
}
|
||||||
if (!empty($trackId)) {
|
if (!empty($trackId)) {
|
||||||
$rules[] = "p.track_id = " . self::db()->quote($trackId);
|
$rules[] = "p.track_id = " . self::db()->quote($trackId);
|
||||||
}
|
}
|
||||||
|
if (!empty($afterId)) {
|
||||||
|
$rules[] = "p.id > " . self::db()->quote($afterId);
|
||||||
|
}
|
||||||
if (!empty($rules)) {
|
if (!empty($rules)) {
|
||||||
$where = "WHERE " . implode(" AND ", $rules);
|
$where = "WHERE " . implode(" AND ", $rules);
|
||||||
} else {
|
} else {
|
||||||
@ -250,7 +330,7 @@
|
|||||||
}
|
}
|
||||||
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
$query = "SELECT p.id, " . self::db()->unix_timestamp('p.time') . " AS tstamp, p.user_id, p.track_id,
|
||||||
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
p.latitude, p.longitude, p.altitude, p.speed, p.bearing, p.accuracy, p.provider,
|
||||||
p.comment, p.image_id, u.login, t.name
|
p.comment, p.image, u.login, t.name
|
||||||
FROM " . self::db()->table('positions') . " p
|
FROM " . self::db()->table('positions') . " p
|
||||||
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
LEFT JOIN " . self::db()->table('users') . " u ON (p.user_id = u.id)
|
||||||
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
LEFT JOIN " . self::db()->table('tracks') . " t ON (p.track_id = t.id)
|
||||||
@ -265,10 +345,53 @@
|
|||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// TODO: handle exception
|
// TODO: handle exception
|
||||||
syslog(LOG_ERR, $e->getMessage());
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
|
$positionsArr = false;
|
||||||
}
|
}
|
||||||
return $positionsArr;
|
return $positionsArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get array of all positions with image
|
||||||
|
*
|
||||||
|
* @param int $userId Optional limit to given user id
|
||||||
|
* @param int $trackId Optional limit to given track id
|
||||||
|
* @param int $afterId Optional limit to positions with id greater then given id
|
||||||
|
* @param array $rules Optional rules
|
||||||
|
* @return uPosition[]|bool Array of uPosition positions, false on error
|
||||||
|
*/
|
||||||
|
public static function getAllWithImage($userId = NULL, $trackId = NULL, $afterId = NULL, $rules = []) {
|
||||||
|
$rules[] = "p.image IS NOT NULL";
|
||||||
|
return self::getAll($userId, $trackId, $afterId, $rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all user's uploads, optionally limit to given track
|
||||||
|
*
|
||||||
|
* @param int $userId User id
|
||||||
|
* @param int $trackId Optional track id
|
||||||
|
* @return bool True if success, false otherwise
|
||||||
|
*/
|
||||||
|
public static function removeImages($userId, $trackId = NULL) {
|
||||||
|
if (($positions = self::getAllWithImage($userId, $trackId)) !== false) {
|
||||||
|
/** @var uUpload $position */
|
||||||
|
foreach ($positions as $position) {
|
||||||
|
try {
|
||||||
|
$query = "UPDATE " . self::db()->table('positions') . "
|
||||||
|
SET image = NULL WHERE id = ?";
|
||||||
|
$stmt = self::db()->prepare($query);
|
||||||
|
$stmt->execute([ $position->id ]);
|
||||||
|
// ignore unlink errors
|
||||||
|
uUpload::delete($position->image);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// TODO: handle exception
|
||||||
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate distance to target point using haversine formula
|
* Calculate distance to target point using haversine formula
|
||||||
*
|
*
|
||||||
@ -282,7 +405,7 @@
|
|||||||
$lon2 = deg2rad($target->longitude);
|
$lon2 = deg2rad($target->longitude);
|
||||||
$latD = $lat2 - $lat1;
|
$latD = $lat2 - $lat1;
|
||||||
$lonD = $lon2 - $lon1;
|
$lonD = $lon2 - $lon1;
|
||||||
$bearing = 2 * asin(sqrt(pow(sin($latD / 2), 2) + cos($lat1) * cos($lat2) * pow(sin($lonD / 2), 2)));
|
$bearing = 2 * asin(sqrt((sin($latD / 2) ** 2) + cos($lat1) * cos($lat2) * (sin($lonD / 2) ** 2)));
|
||||||
return $bearing * 6371000;
|
return $bearing * 6371000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +441,7 @@
|
|||||||
$position->accuracy = $row['accuracy'];
|
$position->accuracy = $row['accuracy'];
|
||||||
$position->provider = $row['provider'];
|
$position->provider = $row['provider'];
|
||||||
$position->comment = $row['comment'];
|
$position->comment = $row['comment'];
|
||||||
$position->imageId = $row['image_id'];
|
$position->image = $row['image'];
|
||||||
$position->isValid = true;
|
$position->isValid = true;
|
||||||
return $position;
|
return $position;
|
||||||
}
|
}
|
||||||
@ -346,7 +469,7 @@
|
|||||||
$stmt->bindColumn('accuracy', $this->accuracy, PDO::PARAM_INT);
|
$stmt->bindColumn('accuracy', $this->accuracy, PDO::PARAM_INT);
|
||||||
$stmt->bindColumn('provider', $this->provider);
|
$stmt->bindColumn('provider', $this->provider);
|
||||||
$stmt->bindColumn('comment', $this->comment);
|
$stmt->bindColumn('comment', $this->comment);
|
||||||
$stmt->bindColumn('image_id', $this->imageId, PDO::PARAM_INT);
|
$stmt->bindColumn('image', $this->image);
|
||||||
$stmt->bindColumn('login', $this->userLogin);
|
$stmt->bindColumn('login', $this->userLogin);
|
||||||
$stmt->bindColumn('name', $this->trackName);
|
$stmt->bindColumn('name', $this->trackName);
|
||||||
if ($stmt->fetch(PDO::FETCH_BOUND)) {
|
if ($stmt->fetch(PDO::FETCH_BOUND)) {
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
$stmt = self::db()->prepare($query);
|
$stmt = self::db()->prepare($query);
|
||||||
$params = [ $userId, $name, $comment ];
|
$params = [ $userId, $name, $comment ];
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$trackId = self::db()->lastInsertId("${table}_id_seq");
|
$trackId = (int) self::db()->lastInsertId("${table}_id_seq");
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// TODO: handle exception
|
// TODO: handle exception
|
||||||
syslog(LOG_ERR, $e->getMessage());
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
@ -158,7 +158,7 @@
|
|||||||
$ret = false;
|
$ret = false;
|
||||||
if (empty($name)) { $name = $this->name; }
|
if (empty($name)) { $name = $this->name; }
|
||||||
if (is_null($comment)) { $comment = $this->comment; }
|
if (is_null($comment)) { $comment = $this->comment; }
|
||||||
if ($comment == "") { $comment = NULL; }
|
if ($comment === "") { $comment = NULL; }
|
||||||
if ($this->isValid) {
|
if ($this->isValid) {
|
||||||
try {
|
try {
|
||||||
$query = "UPDATE " . self::db()->table('tracks') . " SET name = ?, comment = ? WHERE id = ?";
|
$query = "UPDATE " . self::db()->table('tracks') . " SET name = ?, comment = ? WHERE id = ?";
|
||||||
@ -184,21 +184,17 @@
|
|||||||
*/
|
*/
|
||||||
public static function deleteAll($userId) {
|
public static function deleteAll($userId) {
|
||||||
$ret = false;
|
$ret = false;
|
||||||
if (!empty($userId)) {
|
if (!empty($userId) && uPosition::deleteAll($userId) === true) {
|
||||||
// remove all positions
|
// remove all tracks
|
||||||
if (uPosition::deleteAll($userId) === true) {
|
try {
|
||||||
// remove all tracks
|
$query = "DELETE FROM " . self::db()->table('tracks') . " WHERE user_id = ?";
|
||||||
try {
|
$stmt = self::db()->prepare($query);
|
||||||
$query = "DELETE FROM " . self::db()->table('tracks') . " WHERE user_id = ?";
|
$stmt->execute([ $userId ]);
|
||||||
$stmt = self::db()->prepare($query);
|
$ret = true;
|
||||||
$stmt->execute([ $userId ]);
|
} catch (PDOException $e) {
|
||||||
$ret = true;
|
// TODO: handle exception
|
||||||
} catch (PDOException $e) {
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
// TODO: handle exception
|
|
||||||
syslog(LOG_ERR, $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
166
helpers/upload.php
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once(ROOT_DIR . "/helpers/db.php");
|
||||||
|
require_once(ROOT_DIR . "/helpers/utils.php");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploaded files
|
||||||
|
*/
|
||||||
|
class uUpload {
|
||||||
|
|
||||||
|
const META_TYPE = "type";
|
||||||
|
const META_NAME = "name";
|
||||||
|
const META_TMP_NAME = "tmp_name";
|
||||||
|
const META_ERROR = "error";
|
||||||
|
const META_SIZE = "size";
|
||||||
|
public static $uploadDir = ROOT_DIR . "/uploads/";
|
||||||
|
private static $filePattern = "[a-z0-9_.]{20,}";
|
||||||
|
private static $mimeMap = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[] Mime to extension mapping
|
||||||
|
*/
|
||||||
|
private static function getMimeMap() {
|
||||||
|
if (empty(self::$mimeMap)) {
|
||||||
|
self::$mimeMap["image/jpeg"] = "jpg";
|
||||||
|
self::$mimeMap["image/x-ms-bmp"] = "bmp";
|
||||||
|
self::$mimeMap["image/gif"] = "gif";
|
||||||
|
self::$mimeMap["image/png"] = "png";
|
||||||
|
}
|
||||||
|
return self::$mimeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is mime accepted type
|
||||||
|
* @param string $mime Mime type
|
||||||
|
* @return bool True if known
|
||||||
|
*/
|
||||||
|
private static function isKnownMime($mime) {
|
||||||
|
return array_key_exists($mime, self::getMimeMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file extension for given mime
|
||||||
|
* @param $mime
|
||||||
|
* @return string|null Extension or NULL if not found
|
||||||
|
*/
|
||||||
|
private static function getExtension($mime) {
|
||||||
|
if (self::isKnownMime($mime)) {
|
||||||
|
return self::getMimeMap()[$mime];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save file to uploads, basic sanitizing
|
||||||
|
* @param array $uploaded File meta array from $_FILES[]
|
||||||
|
* @param int $trackId
|
||||||
|
* @return string|NULL Unique file name, null on error
|
||||||
|
*/
|
||||||
|
public static function add($uploaded, $trackId) {
|
||||||
|
try {
|
||||||
|
$fileMeta = self::sanitizeUpload($uploaded);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
|
// save exception to txt file as image replacement?
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = self::getExtension($fileMeta[self::META_TYPE]);
|
||||||
|
|
||||||
|
do {
|
||||||
|
$fileName = uniqid("{$trackId}_") . ".$extension";
|
||||||
|
} while (file_exists(self::$uploadDir . $fileName));
|
||||||
|
if (move_uploaded_file($fileMeta[self::META_TMP_NAME], self::$uploadDir . $fileName)) {
|
||||||
|
return $fileName;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete upload from database and filesystem
|
||||||
|
* @param String $path File relative path
|
||||||
|
* @return bool False if file exists but can't be unlinked
|
||||||
|
*/
|
||||||
|
public static function delete($path) {
|
||||||
|
$ret = true;
|
||||||
|
if (preg_match(self::$filePattern, $path)) {
|
||||||
|
$path = self::$uploadDir . $path;
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$ret = unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $fileMeta File meta array from $_FILES[]
|
||||||
|
* @param boolean $checkMime Check with known mime types
|
||||||
|
* @return array File metadata array
|
||||||
|
* @throws ErrorException Internal server exception
|
||||||
|
* @throws Exception File upload exception
|
||||||
|
*/
|
||||||
|
public static function sanitizeUpload($fileMeta, $checkMime = true) {
|
||||||
|
if (!isset($fileMeta) ||
|
||||||
|
!isset($fileMeta[self::META_NAME]) || !isset($fileMeta[self::META_TYPE]) ||
|
||||||
|
!isset($fileMeta[self::META_SIZE]) || !isset($fileMeta[self::META_TMP_NAME])) {
|
||||||
|
$message = "no uploaded file";
|
||||||
|
$lastErr = error_get_last();
|
||||||
|
if (!empty($lastErr)) {
|
||||||
|
$message = $lastErr["message"];
|
||||||
|
}
|
||||||
|
throw new ErrorException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadErrors = [];
|
||||||
|
$uploadErrors[UPLOAD_ERR_INI_SIZE] = "Uploaded file exceeds the upload_max_filesize directive in php.ini";
|
||||||
|
$uploadErrors[UPLOAD_ERR_FORM_SIZE] = "Uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form";
|
||||||
|
$uploadErrors[UPLOAD_ERR_PARTIAL] = "File was only partially uploaded";
|
||||||
|
$uploadErrors[UPLOAD_ERR_NO_FILE] = "No file was uploaded";
|
||||||
|
$uploadErrors[UPLOAD_ERR_NO_TMP_DIR] = "Missing a temporary folder";
|
||||||
|
$uploadErrors[UPLOAD_ERR_CANT_WRITE] = "Failed to write file to disk";
|
||||||
|
$uploadErrors[UPLOAD_ERR_EXTENSION] = "A PHP extension stopped file upload";
|
||||||
|
|
||||||
|
$file = NULL;
|
||||||
|
$fileError = isset($fileMeta[self::META_ERROR]) ? $fileMeta[self::META_ERROR] : UPLOAD_ERR_OK;
|
||||||
|
if ($fileMeta[self::META_SIZE] > uUtils::getUploadMaxSize() && $fileError == UPLOAD_ERR_OK) {
|
||||||
|
$fileError = UPLOAD_ERR_FORM_SIZE;
|
||||||
|
}
|
||||||
|
if ($fileError == UPLOAD_ERR_OK) {
|
||||||
|
$file = $fileMeta[self::META_TMP_NAME];
|
||||||
|
} else {
|
||||||
|
$message = "Unknown error";
|
||||||
|
if (isset($uploadErrors[$fileError])) {
|
||||||
|
$message = $uploadErrors[$fileError];
|
||||||
|
}
|
||||||
|
$message .= " ($fileError)";
|
||||||
|
throw new Exception($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$file || !file_exists($file)) {
|
||||||
|
throw new ErrorException("File not found");
|
||||||
|
}
|
||||||
|
if ($checkMime && !self::isKnownMime($fileMeta[self::META_TYPE])) {
|
||||||
|
throw new Exception("Unsupported mime type");
|
||||||
|
}
|
||||||
|
return $fileMeta;
|
||||||
|
}
|
||||||
|
}
|
@ -84,7 +84,7 @@
|
|||||||
$query = "INSERT INTO $table (login, password) VALUES (?, ?)";
|
$query = "INSERT INTO $table (login, password) VALUES (?, ?)";
|
||||||
$stmt = self::db()->prepare($query);
|
$stmt = self::db()->prepare($query);
|
||||||
$stmt->execute([ $login, $hash ]);
|
$stmt->execute([ $login, $hash ]);
|
||||||
$userid = self::db()->lastInsertId("${table}_id_seq");
|
$userid = (int) self::db()->lastInsertId("${table}_id_seq");
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// TODO: handle exception
|
// TODO: handle exception
|
||||||
syslog(LOG_ERR, $e->getMessage());
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
@ -140,6 +140,7 @@
|
|||||||
$stmt = self::db()->prepare($query);
|
$stmt = self::db()->prepare($query);
|
||||||
$stmt->execute([ $hash, $this->login ]);
|
$stmt->execute([ $hash, $this->login ]);
|
||||||
$ret = true;
|
$ret = true;
|
||||||
|
$this->hash = $hash;
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// TODO: handle exception
|
// TODO: handle exception
|
||||||
syslog(LOG_ERR, $e->getMessage());
|
syslog(LOG_ERR, $e->getMessage());
|
||||||
@ -194,7 +195,7 @@
|
|||||||
/**
|
/**
|
||||||
* Get all users
|
* Get all users
|
||||||
*
|
*
|
||||||
* @return array|bool Array of uUser users, false on error
|
* @return uUser[]|bool Array of uUser users, false on error
|
||||||
*/
|
*/
|
||||||
public static function getAll() {
|
public static function getAll() {
|
||||||
try {
|
try {
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
$upload_max_filesize = self::iniGetBytes('upload_max_filesize');
|
$upload_max_filesize = self::iniGetBytes('upload_max_filesize');
|
||||||
$post_max_size = self::iniGetBytes('post_max_size');
|
$post_max_size = self::iniGetBytes('post_max_size');
|
||||||
// post_max_size = 0 means unlimited size
|
// post_max_size = 0 means unlimited size
|
||||||
if ($post_max_size == 0) { $post_max_size = $upload_max_filesize; }
|
if ($post_max_size === 0) { $post_max_size = $upload_max_filesize; }
|
||||||
$memory_limit = self::iniGetBytes('memory_limit');
|
$memory_limit = self::iniGetBytes('memory_limit');
|
||||||
// memory_limit = -1 means no limit
|
// memory_limit = -1 means no limit
|
||||||
if ($memory_limit < 0) { $memory_limit = $post_max_size; }
|
if ($memory_limit < 0) { $memory_limit = $post_max_size; }
|
||||||
@ -45,10 +45,11 @@
|
|||||||
*
|
*
|
||||||
* @param string $iniParam Ini parameter name
|
* @param string $iniParam Ini parameter name
|
||||||
* @return int Bytes
|
* @return int Bytes
|
||||||
|
* @noinspection PhpMissingBreakStatementInspection
|
||||||
*/
|
*/
|
||||||
private static function iniGetBytes($iniParam) {
|
private static function iniGetBytes($iniParam) {
|
||||||
$iniStr = ini_get($iniParam);
|
$iniStr = ini_get($iniParam);
|
||||||
$val = floatval($iniStr);
|
$val = (float) $iniStr;
|
||||||
$suffix = substr(trim($iniStr), -1);
|
$suffix = substr(trim($iniStr), -1);
|
||||||
if (ctype_alpha($suffix)) {
|
if (ctype_alpha($suffix)) {
|
||||||
switch (strtolower($suffix)) {
|
switch (strtolower($suffix)) {
|
||||||
@ -89,22 +90,17 @@
|
|||||||
* @param array|null $extra Optional array of extra parameters
|
* @param array|null $extra Optional array of extra parameters
|
||||||
*/
|
*/
|
||||||
private static function exitWithStatus($isError, $extra = NULL) {
|
private static function exitWithStatus($isError, $extra = NULL) {
|
||||||
header("Content-type: text/xml");
|
$output = [];
|
||||||
$xml = new XMLWriter();
|
if ($isError) {
|
||||||
$xml->openURI("php://output");
|
$output["error"] = true;
|
||||||
$xml->startDocument("1.0");
|
}
|
||||||
$xml->setIndent(true);
|
|
||||||
$xml->startElement("root");
|
|
||||||
$xml->writeElement("error", (int) $isError);
|
|
||||||
if (!empty($extra)) {
|
if (!empty($extra)) {
|
||||||
foreach ($extra as $key => $value) {
|
foreach ($extra as $key => $value) {
|
||||||
$xml->writeElement($key, $value);
|
$output[$key] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
header("Content-type: application/json");
|
||||||
$xml->endElement();
|
echo json_encode($output);
|
||||||
$xml->endDocument();
|
|
||||||
$xml->flush();
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,9 +111,9 @@
|
|||||||
* @return string URL
|
* @return string URL
|
||||||
*/
|
*/
|
||||||
public static function getBaseUrl() {
|
public static function getBaseUrl() {
|
||||||
$proto = (!isset($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] == "" || $_SERVER["HTTPS"] == "off") ? "http://" : "https://";
|
$proto = (!isset($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] === "" || $_SERVER["HTTPS"] === "off") ? "http://" : "https://";
|
||||||
// Check if we are behind an https proxy
|
// Check if we are behind an https proxy
|
||||||
if (isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] == "https") {
|
if (isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] === "https") {
|
||||||
$proto = "https://";
|
$proto = "https://";
|
||||||
}
|
}
|
||||||
$host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : "";
|
$host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : "";
|
||||||
@ -165,29 +161,47 @@
|
|||||||
return self::requestInt($name, $default, INPUT_GET);
|
return self::requestInt($name, $default, INPUT_GET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function requestFile($name, $default = NULL) {
|
||||||
|
if (isset($_FILES[$name])) {
|
||||||
|
$files = $_FILES[$name];
|
||||||
|
if (isset($files["name"], $files["type"], $files["size"], $files["tmp_name"])) {
|
||||||
|
return $_FILES[$name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name Input name
|
||||||
|
* @param boolean $checkMime Optionally check mime with known types
|
||||||
|
* @return array File metadata array
|
||||||
|
* @throws Exception Upload exception
|
||||||
|
* @throws ErrorException Internal server exception
|
||||||
|
*/
|
||||||
|
public static function requireFile($name, $checkMime = false) {
|
||||||
|
return uUpload::sanitizeUpload($_FILES[$name], $checkMime);
|
||||||
|
}
|
||||||
|
|
||||||
private static function requestString($name, $default, $type) {
|
private static function requestString($name, $default, $type) {
|
||||||
if (is_string(($val = self::requestValue($name, $default, $type)))) {
|
if (is_string(($val = self::requestValue($name, $default, $type)))) {
|
||||||
return trim($val);
|
return trim($val);
|
||||||
} else {
|
|
||||||
return $val;
|
|
||||||
}
|
}
|
||||||
|
return $val;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function requestInt($name, $default, $type) {
|
private static function requestInt($name, $default, $type) {
|
||||||
if (is_float(($val = self::requestValue($name, $default, $type, FILTER_VALIDATE_FLOAT)))) {
|
if (is_float(($val = self::requestValue($name, $default, $type, FILTER_VALIDATE_FLOAT)))) {
|
||||||
return (int) round($val);
|
return (int) round($val);
|
||||||
} else {
|
|
||||||
return self::requestValue($name, $default, $type, FILTER_VALIDATE_INT);
|
|
||||||
}
|
}
|
||||||
|
return self::requestValue($name, $default, $type, FILTER_VALIDATE_INT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function requestValue($name, $default, $type, $filters = FILTER_DEFAULT, $flags = NULL) {
|
private static function requestValue($name, $default, $type, $filters = FILTER_DEFAULT, $flags = NULL) {
|
||||||
$input = filter_input($type, $name, $filters, $flags);
|
$input = filter_input($type, $name, $filters, $flags);
|
||||||
if ($input !== false && !is_null($input)) {
|
if ($input !== false && $input !== null) {
|
||||||
return $input;
|
return $input;
|
||||||
} else {
|
|
||||||
return $default;
|
|
||||||
}
|
}
|
||||||
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
1
images/bearing.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.72 12L12 .152 16.279 12z"/><path d="M14.854 13L12 20.904 9.144 13h5.71m1.425-1H7.72L12 23.848 16.279 12z"/></svg>
|
After Width: | Height: | Size: 208 B |
@ -1,4 +1,4 @@
|
|||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path fill="#297b9a" d="M24 24v-8h-24v8h24zm-22-6h2v2h1v-2h2v3h1v-3h2v2h1v-2h2v2h1v-2h2v3h1v-3h2v2h1v-2h2v4h-20v-4zm14-10h-8v4l-8-6 8-6v4h8v-4l8 6-8 6v-4z"/>
|
<path fill="#52f6ff" d="M24 24v-8h-24v8h24zm-22-6h2v2h1v-2h2v3h1v-3h2v2h1v-2h2v2h1v-2h2v3h1v-3h2v2h1v-2h2v4h-20v-4zm14-10h-8v4l-8-6 8-6v4h8v-4l8 6-8 6v-4z"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 253 B After Width: | Height: | Size: 253 B |
Before Width: | Height: | Size: 724 B |
Before Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 677 B |
Before Width: | Height: | Size: 359 B |
1
images/position.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z"/></svg>
|
After Width: | Height: | Size: 279 B |
@ -1,4 +1,4 @@
|
|||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path fill="#297b9a" d="M20.043 11.76c-.141-.427-.314-.844-.516-1.242l-2.454 1.106c.217.393.39.81.517 1.242l2.453-1.106zm-12.572-.904c.271-.354.579-.674.918-.957l-1.89-1.968c-.328.293-.637.614-.919.957l1.891 1.968zm1.714-1.514c.38-.221.781-.396 1.198-.523l-1.033-2.569c-.412.142-.813.317-1.2.524l1.035 2.568zm-2.759 3.615c.121-.435.287-.854.498-1.25l-2.47-1.066c-.196.403-.364.823-.498 1.25l2.47 1.066zm9.434-6.2c-.387-.205-.79-.379-1.2-.519l-1.023 2.573c.418.125.82.299 1.2.519l1.023-2.573zm2.601 2.131c-.281-.342-.59-.664-.918-.957l-1.891 1.968c.34.283.648.604.919.957l1.89-1.968zm-5.791-3.06c-.219-.017-.437-.026-.648-.026-.213 0-.432.009-.65.026v2.784c.216-.025.434-.038.65-.038.215 0 .434.013.648.038v-2.784zm11.33 8.172c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 2.583.816 5.042 2.205 7h19.59c1.389-1.958 2.205-4.417 2.205-7zm-9.08 5c-.007-1.086-.606-2.031-1.496-2.522l-1.402-6.571-1.402 6.571c-.889.491-1.489 1.436-1.496 2.522h-5.821c-.845-1.5-1.303-3.242-1.303-5 0-5.514 4.486-10 10-10s10 4.486 10 10c0 1.758-.458 3.5-1.303 5h-5.777z"/>
|
<path fill="#52f6ff" d="M20.043 11.76c-.141-.427-.314-.844-.516-1.242l-2.454 1.106c.217.393.39.81.517 1.242l2.453-1.106zm-12.572-.904c.271-.354.579-.674.918-.957l-1.89-1.968c-.328.293-.637.614-.919.957l1.891 1.968zm1.714-1.514c.38-.221.781-.396 1.198-.523l-1.033-2.569c-.412.142-.813.317-1.2.524l1.035 2.568zm-2.759 3.615c.121-.435.287-.854.498-1.25l-2.47-1.066c-.196.403-.364.823-.498 1.25l2.47 1.066zm9.434-6.2c-.387-.205-.79-.379-1.2-.519l-1.023 2.573c.418.125.82.299 1.2.519l1.023-2.573zm2.601 2.131c-.281-.342-.59-.664-.918-.957l-1.891 1.968c.34.283.648.604.919.957l1.89-1.968zm-5.791-3.06c-.219-.017-.437-.026-.648-.026-.213 0-.432.009-.65.026v2.784c.216-.025.434-.038.65-.038.215 0 .434.013.648.038v-2.784zm11.33 8.172c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 2.583.816 5.042 2.205 7h19.59c1.389-1.958 2.205-4.417 2.205-7zm-9.08 5c-.007-1.086-.606-2.031-1.496-2.522l-1.402-6.571-1.402 6.571c-.889.491-1.489 1.436-1.496 2.522h-5.821c-.845-1.5-1.303-3.242-1.303-5 0-5.514 4.486-10 10-10s10 4.486 10 10c0 1.758-.458 3.5-1.303 5h-5.777z"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@ -1,4 +1,4 @@
|
|||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path fill="#297b9a" d="M7 24h-6v-6h6v6zm8-9h-6v9h6v-9zm8-4h-6v13h6v-13zm0-11l-6 1.221 1.716 1.708-6.85 6.733-3.001-3.002-7.841 7.797 1.41 1.418 6.427-6.39 2.991 2.993 8.28-8.137 1.667 1.66 1.201-6.001z"/>
|
<path fill="#52f6ff" d="M7 24h-6v-6h6v6zm8-9h-6v9h6v-9zm8-4h-6v13h6v-13zm0-11l-6 1.221 1.716 1.708-6.85 6.733-3.001-3.002-7.841 7.797 1.41 1.418 6.427-6.39 2.991 2.993 8.28-8.137 1.667 1.66 1.201-6.001z"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 301 B After Width: | Height: | Size: 301 B |
@ -1,4 +1,4 @@
|
|||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path fill="#297b9a" d="M11 6v8h7v-2h-5v-6h-2zm10.854 7.683l1.998.159c-.132.854-.351 1.676-.652 2.46l-1.8-.905c.2-.551.353-1.123.454-1.714zm-2.548 7.826l-1.413-1.443c-.486.356-1.006.668-1.555.933l.669 1.899c.821-.377 1.591-.844 2.299-1.389zm1.226-4.309c-.335.546-.719 1.057-1.149 1.528l1.404 1.433c.583-.627 1.099-1.316 1.539-2.058l-1.794-.903zm-20.532-5.2c0 6.627 5.375 12 12.004 12 1.081 0 2.124-.156 3.12-.424l-.665-1.894c-.787.2-1.607.318-2.455.318-5.516 0-10.003-4.486-10.003-10s4.487-10 10.003-10c2.235 0 4.293.744 5.959 1.989l-2.05 2.049 7.015 1.354-1.355-7.013-2.184 2.183c-2.036-1.598-4.595-2.562-7.385-2.562-6.629 0-12.004 5.373-12.004 12zm23.773-2.359h-2.076c.163.661.261 1.344.288 2.047l2.015.161c-.01-.755-.085-1.494-.227-2.208z"/>
|
<path fill="#52f6ff" d="M11 6v8h7v-2h-5v-6h-2zm10.854 7.683l1.998.159c-.132.854-.351 1.676-.652 2.46l-1.8-.905c.2-.551.353-1.123.454-1.714zm-2.548 7.826l-1.413-1.443c-.486.356-1.006.668-1.555.933l.669 1.899c.821-.377 1.591-.844 2.299-1.389zm1.226-4.309c-.335.546-.719 1.057-1.149 1.528l1.404 1.433c.583-.627 1.099-1.316 1.539-2.058l-1.794-.903zm-20.532-5.2c0 6.627 5.375 12 12.004 12 1.081 0 2.124-.156 3.12-.424l-.665-1.894c-.787.2-1.607.318-2.455.318-5.516 0-10.003-4.486-10.003-10s4.487-10 10.003-10c2.235 0 4.293.744 5.959 1.989l-2.05 2.049 7.015 1.354-1.355-7.013-2.184 2.183c-2.036-1.598-4.595-2.562-7.385-2.562-6.629 0-12.004 5.373-12.004 12zm23.773-2.359h-2.076c.163.661.261 1.344.288 2.047l2.015.161c-.01-.755-.085-1.494-.227-2.208z"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 840 B |
271
index.php
@ -17,12 +17,12 @@
|
|||||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once(__DIR__ . "/helpers/auth.php");
|
require_once(__DIR__ . '/helpers/auth.php');
|
||||||
require_once(ROOT_DIR . "/helpers/config.php");
|
require_once(ROOT_DIR . '/helpers/config.php');
|
||||||
require_once(ROOT_DIR . "/helpers/position.php");
|
require_once(ROOT_DIR . '/helpers/position.php');
|
||||||
require_once(ROOT_DIR . "/helpers/track.php");
|
require_once(ROOT_DIR . '/helpers/track.php');
|
||||||
require_once(ROOT_DIR . "/helpers/utils.php");
|
require_once(ROOT_DIR . '/helpers/utils.php');
|
||||||
require_once(ROOT_DIR . "/helpers/lang.php");
|
require_once(ROOT_DIR . '/helpers/lang.php');
|
||||||
|
|
||||||
$login = uUtils::postString('user');
|
$login = uUtils::postString('user');
|
||||||
$pass = uUtils::postPass('pass');
|
$pass = uUtils::postPass('pass');
|
||||||
@ -32,202 +32,127 @@
|
|||||||
$langsArr = uLang::getLanguages();
|
$langsArr = uLang::getLanguages();
|
||||||
|
|
||||||
$auth = new uAuth();
|
$auth = new uAuth();
|
||||||
if ($action == "auth") {
|
if ($action === 'auth') {
|
||||||
$auth->checkLogin($login, $pass);
|
$auth->checkLogin($login, $pass);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$auth->isAuthenticated() && $action == "auth") {
|
if ($action === 'auth' && !$auth->isAuthenticated()) {
|
||||||
$auth->exitWithRedirect("login.php?auth_error=1");
|
$auth->exitWithRedirect('login.php?auth_error=1');
|
||||||
}
|
}
|
||||||
if (!$auth->isAuthenticated() && uConfig::$require_authentication) {
|
if (uConfig::$require_authentication && !$auth->isAuthenticated()) {
|
||||||
$auth->exitWithRedirect("login.php");
|
$auth->exitWithRedirect('login.php');
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$displayUserId = NULL;
|
|
||||||
$usersArr = [];
|
|
||||||
if ($auth->isAdmin() || uConfig::$public_tracks) {
|
|
||||||
// public access or admin user
|
|
||||||
// get last position user
|
|
||||||
$lastPosition = uPosition::getLast();
|
|
||||||
if ($lastPosition->isValid) {
|
|
||||||
// display track of last position user
|
|
||||||
$displayUserId = $lastPosition->userId;
|
|
||||||
}
|
|
||||||
// populate users array (for <select>)
|
|
||||||
$usersArr = uUser::getAll();
|
|
||||||
} else if ($auth->isAuthenticated()) {
|
|
||||||
// display track of authenticated user
|
|
||||||
$displayUserId = $auth->user->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tracksArr = uTrack::getAll($displayUserId);
|
|
||||||
if (!empty($tracksArr)) {
|
|
||||||
// get id of the latest track
|
|
||||||
$displayTrackId = $tracksArr[0]->id;
|
|
||||||
} else {
|
|
||||||
$tracksArr = [];
|
|
||||||
$displayTrackId = NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="<?= uConfig::$lang ?>">
|
||||||
<head>
|
<head>
|
||||||
<title><?= $lang["title"] ?></title>
|
<title><?= $lang['title'] ?></title>
|
||||||
<?php include("meta.php"); ?>
|
<?php include('meta.php'); ?>
|
||||||
<script>
|
<script src="js/dist/bundle.js"></script>
|
||||||
var interval = '<?= uConfig::$interval ?>';
|
|
||||||
var userid = '<?= ($displayUserId) ? $displayUserId : -1 ?>';
|
|
||||||
var trackid = '<?= ($displayTrackId) ? $displayTrackId : -1 ?>';
|
|
||||||
var units = '<?= uConfig::$units ?>';
|
|
||||||
var mapapi = '<?= uConfig::$mapapi ?>';
|
|
||||||
var gkey = '<?= !empty(uConfig::$gkey) ? uConfig::$gkey : "null" ?>';
|
|
||||||
var ol_layers = <?= json_encode(uConfig::$ol_layers) ?>;
|
|
||||||
var init_latitude = <?= uConfig::$init_latitude ?>;
|
|
||||||
var init_longitude = <?= uConfig::$init_longitude ?>;
|
|
||||||
var lang = <?= json_encode($lang) ?>;
|
|
||||||
var admin = <?= json_encode($auth->isAdmin()) ?>;
|
|
||||||
var auth = '<?= ($auth->isAuthenticated()) ? $auth->user->login : "null" ?>';
|
|
||||||
var pass_regex = <?= uConfig::passRegex() ?>;
|
|
||||||
var strokeWeight = <?= uConfig::$strokeWeight ?>;
|
|
||||||
var strokeColor = '<?= uConfig::$strokeColor ?>';
|
|
||||||
var strokeOpacity = <?= uConfig::$strokeOpacity ?>;
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript" src="js/main.js"></script>
|
|
||||||
<?php if ($auth->isAdmin()): ?>
|
|
||||||
<script type="text/javascript" src="js/admin.js"></script>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($auth->isAuthenticated()): ?>
|
|
||||||
<script type="text/javascript" src="js/track.js"></script>
|
|
||||||
<?php endif; ?>
|
|
||||||
<script type="text/javascript" src="js/pass.js"></script>
|
|
||||||
<script type="text/javascript" src="//www.google.com/jsapi"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
google.load('visualization', '1', { packages:['corechart'] });
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body onload="loadMapAPI();">
|
<body>
|
||||||
<div id="menu">
|
<div id="container">
|
||||||
<div id="menu-content">
|
<div id="menu">
|
||||||
|
<div id="menu-content">
|
||||||
|
|
||||||
<?php if ($auth->isAuthenticated()): ?>
|
<?php if ($auth->isAuthenticated()): ?>
|
||||||
<div id="user_menu">
|
<div>
|
||||||
<a href="javascript:void(0);" onclick="userMenu()"><img class="icon" alt="<?= $lang["user"] ?>" src="images/user.svg"> <?= htmlspecialchars($auth->user->login) ?></a>
|
<a data-bind="onShowUserMenu"><img class="icon" alt="<?= $lang['user'] ?>" src="images/user.svg"> <?= htmlspecialchars($auth->user->login) ?></a>
|
||||||
<div id="user_dropdown" class="dropdown">
|
<div id="user-menu" class="menu-hidden">
|
||||||
<a href="javascript:void(0)" onclick="changePass()"><img class="icon" alt="<?= $lang["changepass"] ?>" src="images/lock.svg"> <?= $lang["changepass"] ?></a>
|
<a id="user-pass" data-bind="onPasswordChange"><img class="icon" alt="<?= $lang['changepass'] ?>" src="images/lock.svg"> <?= $lang['changepass'] ?></a>
|
||||||
<a href="utils/logout.php"><img class="icon" alt="<?= $lang["logout"] ?>" src="images/poweroff.svg"> <?= $lang["logout"] ?></a>
|
<a href="utils/logout.php"><img class="icon" alt="<?= $lang['logout'] ?>" src="images/poweroff.svg"> <?= $lang['logout'] ?></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php else: ?>
|
||||||
<?php else: ?>
|
<a href="login.php"><img class="icon" alt="<?= $lang['login'] ?>" src="images/key.svg"> <?= $lang['login'] ?></a>
|
||||||
<a href="login.php"><img class="icon" alt="<?= $lang["login"] ?>" src="images/key.svg"> <?= $lang["login"] ?></a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div id="user">
|
|
||||||
<?php if (!empty($usersArr)): ?>
|
|
||||||
<div class="menutitle" style="padding-top: 1em"><?= $lang["user"] ?></div>
|
|
||||||
<form>
|
|
||||||
<select name="user" onchange="selectUser(this);">
|
|
||||||
<option value="0" disabled><?= $lang["suser"] ?></option>
|
|
||||||
<?php foreach ($usersArr as $aUser): ?>
|
|
||||||
<option <?= ($aUser->id == $displayUserId) ? "selected " : "" ?>value="<?= $aUser->id ?>"><?= htmlspecialchars($aUser->login) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="track">
|
<div class="section">
|
||||||
<div class="menutitle"><?= $lang["track"] ?></div>
|
<label for="user"><?= $lang['user'] ?></label>
|
||||||
<form>
|
<select id="user" data-bind="currentUserId" name="user"></select>
|
||||||
<select name="track" onchange="selectTrack(this)">
|
</div>
|
||||||
<?php foreach ($tracksArr as $aTrack): ?>
|
|
||||||
<option value="<?= $aTrack->id ?>"><?= htmlspecialchars($aTrack->name) ?></option>
|
<div class="section">
|
||||||
<?php endforeach; ?>
|
<label for="track"><?= $lang['track'] ?></label>
|
||||||
|
<select id="track" data-bind="currentTrackId" name="track"></select>
|
||||||
|
<input id="latest" type="checkbox" data-bind="showLatest"> <label for="latest"><?= $lang['latest'] ?></label><br>
|
||||||
|
<input id="auto-reload" type="checkbox" data-bind="autoReload"> <label for="auto-reload"><?= $lang['autoreload'] ?></label> (<a id="set-interval" data-bind="onSetInterval"><span id="interval" data-bind="interval"><?= uConfig::$interval ?></span></a> s)<br>
|
||||||
|
<a id="force-reload" data-bind="onReload"> <?= $lang['reload'] ?></a><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="summary" class="section" data-bind="summary"></div>
|
||||||
|
|
||||||
|
<div id="other" class="section">
|
||||||
|
<a id="altitudes" data-bind="onChartToggle"><?= $lang['chart'] ?></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="api"><?= $lang['api'] ?></label>
|
||||||
|
<select id="api" name="api" data-bind="mapApi">
|
||||||
|
<option value="gmaps"<?= (uConfig::$mapapi === 'gmaps') ? ' selected' : '' ?>>Google Maps</option>
|
||||||
|
<option value="openlayers"<?= (uConfig::$mapapi === 'openlayers') ? ' selected' : '' ?>>OpenLayers</option>
|
||||||
</select>
|
</select>
|
||||||
<input id="latest" type="checkbox" onchange="toggleLatest();"> <?= $lang["latest"] ?><br>
|
</div>
|
||||||
<input type="checkbox" onchange="autoReload();"> <?= $lang["autoreload"] ?> (<a href="javascript:void(0);" onclick="setTime();"><span id="auto"><?= uConfig::$interval ?></span></a> s)<br>
|
|
||||||
</form>
|
|
||||||
<a href="javascript:void(0);" onclick="reload(userid, trackid);"> <?= $lang["reload"] ?></a><br>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="summary"></div>
|
<div>
|
||||||
|
<label for="lang"><?= $lang['language'] ?></label>
|
||||||
<div id="other">
|
<select id="lang" name="lang" data-bind="lang">
|
||||||
<a id="altitudes" href="javascript:void(0);" onclick="toggleChart();"><?= $lang["chart"] ?></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="api">
|
|
||||||
<div class="menutitle"><?= $lang["api"] ?></div>
|
|
||||||
<form>
|
|
||||||
<select name="api" onchange="loadMapAPI(this.options[this.selectedIndex].value);">
|
|
||||||
<option value="gmaps"<?= (uConfig::$mapapi == "gmaps") ? " selected" : "" ?>>Google Maps</option>
|
|
||||||
<option value="openlayers"<?= (uConfig::$mapapi == "openlayers") ? " selected" : "" ?>>OpenLayers</option>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="lang">
|
|
||||||
<div class="menutitle"><?= $lang["language"] ?></div>
|
|
||||||
<form>
|
|
||||||
<select name="units" onchange="setLang(this.options[this.selectedIndex].value);">
|
|
||||||
<?php foreach ($langsArr as $langCode => $langName): ?>
|
<?php foreach ($langsArr as $langCode => $langName): ?>
|
||||||
<option value="<?= $langCode ?>"<?= (uConfig::$lang == $langCode) ? " selected" : "" ?>><?= $langName ?></option>
|
<option value="<?= $langCode ?>"<?= (uConfig::$lang === $langCode) ? ' selected' : '' ?>><?= $langName ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="units">
|
<div class="section">
|
||||||
<div class="menutitle"><?= $lang["units"] ?></div>
|
<label for="units"><?= $lang['units'] ?></label>
|
||||||
<form>
|
<select id="units" name="units" data-bind="units">
|
||||||
<select name="units" onchange="setUnits(this.options[this.selectedIndex].value);">
|
<option value="metric"<?= (uConfig::$units === 'metric') ? ' selected' : '' ?>><?= $lang['metric'] ?></option>
|
||||||
<option value="metric"<?= (uConfig::$units == "metric") ? " selected" : "" ?>><?= $lang["metric"] ?></option>
|
<option value="imperial"<?= (uConfig::$units === 'imperial') ? ' selected' : '' ?>><?= $lang['imperial'] ?></option>
|
||||||
<option value="imperial"<?= (uConfig::$units == "imperial") ? " selected" : "" ?>><?= $lang["imperial"] ?></option>
|
<option value="nautical"<?= (uConfig::$units === 'nautical') ? ' selected' : '' ?>><?= $lang['nautical'] ?></option>
|
||||||
<option value="nautical"<?= (uConfig::$units == "nautical") ? " selected" : "" ?>><?= $lang["nautical"] ?></option>
|
|
||||||
</select>
|
</select>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="export">
|
|
||||||
<div class="menutitle u"><?= $lang["export"] ?></div>
|
|
||||||
<a class="menulink" href="javascript:void(0);" onclick="exportFile('kml', userid, trackid);">kml</a>
|
|
||||||
<a class="menulink" href="javascript:void(0);" onclick="exportFile('gpx', userid, trackid);">gpx</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($auth->isAuthenticated()): ?>
|
|
||||||
<div id="import">
|
|
||||||
<div class="menutitle u"><?= $lang["import"] ?></div>
|
|
||||||
<form id="importForm" enctype="multipart/form-data" method="post">
|
|
||||||
<input type="hidden" name="MAX_FILE_SIZE" value="<?= uUtils::getUploadMaxSize() ?>" />
|
|
||||||
<input type="file" id="inputFile" name="gpx" style="display:none" onchange="importFile(this)" />
|
|
||||||
</form>
|
|
||||||
<a class="menulink" href="javascript:void(0);" onclick="document.getElementById('inputFile').click();">gpx</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="admin_menu">
|
<div class="section">
|
||||||
<div class="menutitle u"><?= $lang["adminmenu"] ?></div>
|
<div class="menu-title"><?= $lang['export'] ?></div>
|
||||||
<?php if ($auth->isAdmin()): ?>
|
<a id="export-kml" class="menu-link" data-bind="onExportKml">kml</a>
|
||||||
<a class="menulink" href="javascript:void(0);" onclick="addUser()"><?= $lang["adduser"] ?></a>
|
<a id="export-gpx" class="menu-link" data-bind="onExportGpx">gpx</a>
|
||||||
<a class="menulink" href="javascript:void(0);" onclick="editUser()"><?= $lang["edituser"] ?></a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a class="menulink" href="javascript:void(0);" onclick="editTrack()"><?= $lang["edittrack"] ?></a>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
|
<?php if ($auth->isAuthenticated()): ?>
|
||||||
|
<div class="section">
|
||||||
|
<div id="import" class="menu-title"><?= $lang['import'] ?></div>
|
||||||
|
<form id="import-form" enctype="multipart/form-data" method="post">
|
||||||
|
<input type="hidden" name="MAX_FILE_SIZE" value="<?= uUtils::getUploadMaxSize() ?>" />
|
||||||
|
<input type="file" id="input-file" name="gpx" data-bind="inputFile"/>
|
||||||
|
</form>
|
||||||
|
<a id="import-gpx" class="menu-link" data-bind="onImportGpx">gpx</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="admin-menu">
|
||||||
|
<div class="menu-title"><?= $lang['adminmenu'] ?></div>
|
||||||
|
<?php if ($auth->isAdmin()): ?>
|
||||||
|
<a id="adduser" class="menu-link" data-bind="onUserAdd"><?= $lang['adduser'] ?></a>
|
||||||
|
<a id="edituser" class="menu-link" data-bind="onUserEdit"><?= $lang['edituser'] ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a id="edittrack" class="menu-link" data-bind="onTrackEdit"><?= $lang['edittrack'] ?></a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="menu-button"><a data-bind="onMenuToggle"></a></div>
|
||||||
|
<div id="footer"><a target="_blank" href="https://github.com/bfabiszewski/ulogger-server"><span class="mi">μ</span>logger</a> <?= uConfig::$version ?></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="menu-close" onclick="toggleMenu();">»</div>
|
|
||||||
<div id="footer"><a target="_blank" href="https://github.com/bfabiszewski/ulogger-server"><span class="mi">μ</span>logger</a> <?= uConfig::$version ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div id="map-canvas"></div>
|
<div id="map-canvas"></div>
|
||||||
<div id="bottom">
|
<div id="bottom">
|
||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
<div id="close"><a href="javascript:void(0);" onclick="toggleChart(0);"><?= $lang["close"] ?></a></div>
|
<a id="chart-close" data-bind="onChartToggle"><?= $lang['close'] ?></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
119
js/admin.js
@ -1,119 +0,0 @@
|
|||||||
/* μlogger
|
|
||||||
*
|
|
||||||
* Copyright(C) 2017 Bartek Fabiszewski (www.fabiszewski.net)
|
|
||||||
*
|
|
||||||
* This is free software; you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function addUser() {
|
|
||||||
var form = '<form id="userForm" method="post" onsubmit="submitUser(\'add\'); return false">';
|
|
||||||
form += '<label><b>' + lang['username'] + '</b></label><input type="text" placeholder="' + lang['usernameenter'] + '" name="login" required>';
|
|
||||||
form += '<label><b>' + lang['password'] + '</b></label><input type="password" placeholder="' + lang['passwordenter'] + '" name="pass" required>';
|
|
||||||
form += '<label><b>' + lang['passwordrepeat'] + '</b></label><input type="password" placeholder="' + lang['passwordenter'] + '" name="pass2" required>';
|
|
||||||
form += '<div class="buttons"><button type="button" onclick="removeModal()">' + lang['cancel'] + '</button><button type="submit">' + lang['submit'] + '</button></div>';
|
|
||||||
form += '</form>';
|
|
||||||
showModal(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
function editUser() {
|
|
||||||
var userForm = document.getElementsByName('user')[0];
|
|
||||||
var userLogin = (userForm !== undefined) ? userForm.options[userForm.selectedIndex].text : auth;
|
|
||||||
if (userLogin == auth) {
|
|
||||||
alert(lang['selfeditwarn']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var message = '<div style="float:left">' + sprintf(lang['editinguser'], '<b>' + htmlEncode(userLogin) + '</b>') + '</div>';
|
|
||||||
message += '<div class="red-button"><b><a href="javascript:void(0);" onclick="submitUser(\'delete\'); return false">' + lang['deluser'] + '</a></b></div>';
|
|
||||||
message += '<div style="clear: both; padding-bottom: 1em;"></div>';
|
|
||||||
|
|
||||||
var form = '<form id="userForm" method="post" onsubmit="submitUser(\'update\'); return false">';
|
|
||||||
form += '<input type="hidden" name="login" value="' + htmlEncode(userLogin) + '">';
|
|
||||||
form += '<label><b>' + lang['password'] + '</b></label><input type="password" placeholder="' + lang['passwordenter'] + '" name="pass" required>';
|
|
||||||
form += '<label><b>' + lang['passwordrepeat'] + '</b></label><input type="password" placeholder="' + lang['passwordenter'] + '" name="pass2" required>';
|
|
||||||
form += '<div class="buttons"><button type="button" onclick="removeModal()">' + lang['cancel'] + '</button><button type="submit">' + lang['submit'] + '</button></div>';
|
|
||||||
form += '</form>';
|
|
||||||
showModal(message + form);
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmedDelete(login) {
|
|
||||||
return confirm(sprintf(lang['userdelwarn'], '"' + login + '"'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitUser(action) {
|
|
||||||
var form = document.getElementById('userForm');
|
|
||||||
var login = form.elements['login'].value.trim();
|
|
||||||
if (!login) {
|
|
||||||
alert(lang['allrequired']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var pass = null;
|
|
||||||
var pass2 = null;
|
|
||||||
if (action != 'delete') {
|
|
||||||
pass = form.elements['pass'].value;
|
|
||||||
pass2 = form.elements['pass2'].value;
|
|
||||||
if (!pass || !pass2) {
|
|
||||||
alert(lang['allrequired']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pass != pass2) {
|
|
||||||
alert(lang['passnotmatch']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!pass_regex.test(pass)) {
|
|
||||||
alert(lang['passlenmin'] + '\n' + lang['passrules']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!confirmedDelete(login)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var xhr = getXHR();
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if (xhr.readyState == 4) {
|
|
||||||
var error = true;
|
|
||||||
var message = '';
|
|
||||||
if (xhr.status == 200) {
|
|
||||||
var xml = xhr.responseXML;
|
|
||||||
if (xml) {
|
|
||||||
var root = xml.getElementsByTagName('root');
|
|
||||||
if (root.length && getNode(root[0], 'error') == 0) {
|
|
||||||
removeModal();
|
|
||||||
alert(lang['actionsuccess']);
|
|
||||||
if (action == 'delete') {
|
|
||||||
// select current user in users form
|
|
||||||
var f = document.getElementsByName('user')[0];
|
|
||||||
f.remove(f.selectedIndex);
|
|
||||||
selectUser(f);
|
|
||||||
}
|
|
||||||
error = false;
|
|
||||||
} else if (root.length) {
|
|
||||||
errorMsg = getNode(root[0], 'message');
|
|
||||||
if (errorMsg) { message = errorMsg; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
alert(lang['actionfailure'] + '\n' + message);
|
|
||||||
}
|
|
||||||
xhr = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.open('POST', 'utils/handleuser.php', true);
|
|
||||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
|
||||||
var params = 'action=' + action + '&login=' + encodeURIComponent(login) + '&pass=' + encodeURIComponent(pass);
|
|
||||||
params = params.replace(/%20/g, '+');
|
|
||||||
xhr.send(params);
|
|
||||||
return;
|
|
||||||
}
|
|
208
js/api_gmaps.js
@ -1,208 +0,0 @@
|
|||||||
/* μlogger
|
|
||||||
*
|
|
||||||
* Copyright(C) 2017 Bartek Fabiszewski (www.fabiszewski.net)
|
|
||||||
*
|
|
||||||
* This is free software; you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// google maps
|
|
||||||
var map;
|
|
||||||
var polies = [];
|
|
||||||
var markers = [];
|
|
||||||
var popups = [];
|
|
||||||
var popup;
|
|
||||||
var polyOptions;
|
|
||||||
var mapOptions;
|
|
||||||
var loadedAPI = 'gmaps';
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (gm_error) { return gm_authFailure(); }
|
|
||||||
google.maps.visualRefresh = true;
|
|
||||||
polyOptions = {
|
|
||||||
strokeColor: strokeColor,
|
|
||||||
strokeOpacity: strokeOpacity,
|
|
||||||
strokeWeight: strokeWeight
|
|
||||||
}
|
|
||||||
mapOptions = {
|
|
||||||
center: new google.maps.LatLng(init_latitude, init_longitude),
|
|
||||||
zoom: 8,
|
|
||||||
mapTypeId: google.maps.MapTypeId.ROADMAP,
|
|
||||||
scaleControl: true
|
|
||||||
};
|
|
||||||
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
|
|
||||||
}
|
|
||||||
function cleanup() {
|
|
||||||
map = undefined;
|
|
||||||
polies = undefined;
|
|
||||||
markers = undefined;
|
|
||||||
popups = undefined;
|
|
||||||
popup = undefined;
|
|
||||||
polyOptions = undefined;
|
|
||||||
mapOptions = undefined;
|
|
||||||
document.getElementById('map-canvas').innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayTrack(xml, update) {
|
|
||||||
altitudes = {};
|
|
||||||
var totalMeters = 0;
|
|
||||||
var totalSeconds = 0;
|
|
||||||
// init polyline
|
|
||||||
var poly = new google.maps.Polyline(polyOptions);
|
|
||||||
poly.setMap(map);
|
|
||||||
var path = poly.getPath();
|
|
||||||
var latlngbounds = new google.maps.LatLngBounds();
|
|
||||||
var positions = xml.getElementsByTagName('position');
|
|
||||||
var posLen = positions.length;
|
|
||||||
for (var i = 0; i < posLen; i++) {
|
|
||||||
var p = parsePosition(positions[i], i);
|
|
||||||
totalMeters += p.distance;
|
|
||||||
totalSeconds += p.seconds;
|
|
||||||
p.totalMeters = totalMeters;
|
|
||||||
p.totalSeconds = totalSeconds;
|
|
||||||
p.coordinates = new google.maps.LatLng(p.latitude, p.longitude);
|
|
||||||
// set marker
|
|
||||||
setMarker(p, i, posLen);
|
|
||||||
// update polyline
|
|
||||||
path.push(p.coordinates);
|
|
||||||
latlngbounds.extend(p.coordinates);
|
|
||||||
}
|
|
||||||
if (update) {
|
|
||||||
map.fitBounds(latlngbounds);
|
|
||||||
if (i == 1) {
|
|
||||||
// only one point, zoom out
|
|
||||||
zListener =
|
|
||||||
google.maps.event.addListenerOnce(map, 'bounds_changed', function (event) {
|
|
||||||
if (this.getZoom()) {
|
|
||||||
this.setZoom(15);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setTimeout(function () { google.maps.event.removeListener(zListener) }, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
polies.push(poly);
|
|
||||||
|
|
||||||
updateSummary(p.timestamp, totalMeters, totalSeconds);
|
|
||||||
if (p.tid != trackid) {
|
|
||||||
trackid = p.tid;
|
|
||||||
setTrack(trackid);
|
|
||||||
}
|
|
||||||
if (document.getElementById('bottom').style.display == 'block') {
|
|
||||||
// update altitudes chart
|
|
||||||
chart.clearChart();
|
|
||||||
displayChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearMap() {
|
|
||||||
if (polies) {
|
|
||||||
for (var i = 0; i < polies.length; i++) {
|
|
||||||
polies[i].setMap(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (markers) {
|
|
||||||
for (var i = 0; i < markers.length; i++) {
|
|
||||||
google.maps.event.removeListener(popups[i].listener);
|
|
||||||
popups[i].setMap(null);
|
|
||||||
markers[i].setMap(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markers.length = 0;
|
|
||||||
polies.length = 0;
|
|
||||||
popups.lentgth = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMarker(p, i, posLen) {
|
|
||||||
// marker
|
|
||||||
var marker = new google.maps.Marker({
|
|
||||||
map: map,
|
|
||||||
position: new google.maps.LatLng(p.latitude, p.longitude),
|
|
||||||
title: (new Date(p.timestamp * 1000)).toLocaleString()
|
|
||||||
});
|
|
||||||
if (latest == 1) { marker.setIcon('images/marker-red.png') }
|
|
||||||
else if (i == 0) { marker.setIcon('images/marker-green.png') }
|
|
||||||
else if (i == posLen - 1) { marker.setIcon('images/marker-red.png') }
|
|
||||||
else { marker.setIcon('images/marker-white.png') }
|
|
||||||
// popup
|
|
||||||
var content = getPopupHtml(p, i, posLen);
|
|
||||||
popup = new google.maps.InfoWindow();
|
|
||||||
popup.listener = google.maps.event.addListener(marker, 'click', (function (marker, content) {
|
|
||||||
return function () {
|
|
||||||
popup.setContent(content);
|
|
||||||
popup.open(map, marker);
|
|
||||||
if (document.getElementById('bottom').style.display == 'block') {
|
|
||||||
var index = 0;
|
|
||||||
for (var key in altitudes) {
|
|
||||||
if (altitudes.hasOwnProperty(key) && key == i) {
|
|
||||||
chart.setSelection([{ row: index, column: null }]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(marker, content));
|
|
||||||
markers.push(marker);
|
|
||||||
popups.push(popup);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addChartEvent(chart, data) {
|
|
||||||
google.visualization.events.addListener(chart, 'select', function () {
|
|
||||||
if (popup) { popup.close(); clearTimeout(altTimeout); }
|
|
||||||
var selection = chart.getSelection()[0];
|
|
||||||
if (selection) {
|
|
||||||
var id = data.getValue(selection.row, 0) - 1;
|
|
||||||
var icon = markers[id].getIcon();
|
|
||||||
markers[id].setIcon('images/marker-gold.png');
|
|
||||||
altTimeout = setTimeout(function () { markers[id].setIcon(icon); }, 2000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//((52.20105108685229, 20.789387865580238), (52.292069558807135, 21.172192736185707))
|
|
||||||
function getBounds() {
|
|
||||||
var bounds = map.getBounds();
|
|
||||||
var lat_sw = bounds.getSouthWest().lat();
|
|
||||||
var lon_sw = bounds.getSouthWest().lng();
|
|
||||||
var lat_ne = bounds.getNorthEast().lat();
|
|
||||||
var lon_ne = bounds.getNorthEast().lng();
|
|
||||||
return [lon_sw, lat_sw, lon_ne, lat_ne];
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoomToExtent() {
|
|
||||||
var latlngbounds = new google.maps.LatLngBounds();
|
|
||||||
for (var i = 0; i < markers.length; i++) {
|
|
||||||
var coordinates = new google.maps.LatLng(markers[i].position.lat(), markers[i].position.lng());
|
|
||||||
latlngbounds.extend(coordinates);
|
|
||||||
}
|
|
||||||
map.fitBounds(latlngbounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoomToBounds(b) {
|
|
||||||
var sw = new google.maps.LatLng(b[1], b[0]);
|
|
||||||
var ne = new google.maps.LatLng(b[3], b[2]);
|
|
||||||
var bounds = new google.maps.LatLngBounds(sw, ne);
|
|
||||||
map.fitBounds(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
function gm_authFailure() {
|
|
||||||
gm_error = true;
|
|
||||||
message = sprintf(lang['apifailure'], 'Google Maps');
|
|
||||||
message += '<br><br>' + lang['gmauthfailure'];
|
|
||||||
message += '<br><br>' + lang['gmapilink'];
|
|
||||||
showModal(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateSize() {
|
|
||||||
// ignore
|
|
||||||
}
|
|
@ -1,431 +0,0 @@
|
|||||||
/* μlogger
|
|
||||||
*
|
|
||||||
* Copyright(C) 2017 Bartek Fabiszewski (www.fabiszewski.net)
|
|
||||||
*
|
|
||||||
* This is free software; you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// openlayers 3+
|
|
||||||
var map;
|
|
||||||
var layerTrack;
|
|
||||||
var layerMarkers;
|
|
||||||
var selectedLayer;
|
|
||||||
var olStyles;
|
|
||||||
var loadedAPI = 'openlayers';
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
|
|
||||||
addCss('css/ol.css', 'ol_css');
|
|
||||||
|
|
||||||
var controls = [
|
|
||||||
new ol.control.Zoom(),
|
|
||||||
new ol.control.Rotate(),
|
|
||||||
new ol.control.ScaleLine(),
|
|
||||||
new ol.control.ZoomToExtent({ label: getExtentImg() }),
|
|
||||||
];
|
|
||||||
|
|
||||||
var view = new ol.View({
|
|
||||||
center: ol.proj.fromLonLat([init_longitude, init_latitude]),
|
|
||||||
zoom: 8
|
|
||||||
});
|
|
||||||
|
|
||||||
map = new ol.Map({
|
|
||||||
target: 'map-canvas',
|
|
||||||
controls: controls,
|
|
||||||
view: view
|
|
||||||
});
|
|
||||||
|
|
||||||
// default layer: OpenStreetMap
|
|
||||||
var osm = new ol.layer.Tile({
|
|
||||||
name: 'OpenStreetMap',
|
|
||||||
visible: true,
|
|
||||||
source: new ol.source.OSM()
|
|
||||||
});
|
|
||||||
map.addLayer(osm);
|
|
||||||
selectedLayer = osm;
|
|
||||||
|
|
||||||
// add extra layers
|
|
||||||
for (var layerName in ol_layers) {
|
|
||||||
if (ol_layers.hasOwnProperty(layerName)) {
|
|
||||||
var layerUrl = ol_layers[layerName];
|
|
||||||
var ol_layer = new ol.layer.Tile({
|
|
||||||
name: layerName,
|
|
||||||
visible: false,
|
|
||||||
source: new ol.source.XYZ({
|
|
||||||
url: layerUrl
|
|
||||||
})
|
|
||||||
});
|
|
||||||
map.addLayer(ol_layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// init layers
|
|
||||||
var lineStyle = new ol.style.Style({
|
|
||||||
stroke: new ol.style.Stroke({
|
|
||||||
color: hexToRGBA(strokeColor, strokeOpacity),
|
|
||||||
width: strokeWeight
|
|
||||||
})
|
|
||||||
});
|
|
||||||
layerTrack = new ol.layer.Vector({
|
|
||||||
name: 'Track',
|
|
||||||
type: 'data',
|
|
||||||
source: new ol.source.Vector(),
|
|
||||||
style: lineStyle
|
|
||||||
});
|
|
||||||
layerMarkers = new ol.layer.Vector({
|
|
||||||
name: 'Markers',
|
|
||||||
type: 'data',
|
|
||||||
source: new ol.source.Vector()
|
|
||||||
});
|
|
||||||
map.addLayer(layerTrack);
|
|
||||||
map.addLayer(layerMarkers);
|
|
||||||
|
|
||||||
// styles
|
|
||||||
olStyles = {};
|
|
||||||
var iconRed = new ol.style.Icon({
|
|
||||||
anchor: [ 0.5, 1 ],
|
|
||||||
src: 'images/marker-red.png'
|
|
||||||
});
|
|
||||||
var iconGreen = new ol.style.Icon({
|
|
||||||
anchor: [ 0.5, 1 ],
|
|
||||||
src: 'images/marker-green.png'
|
|
||||||
});
|
|
||||||
var iconWhite = new ol.style.Icon({
|
|
||||||
anchor: [ 0.5, 1 ],
|
|
||||||
opacity: 0.7,
|
|
||||||
src: 'images/marker-white.png'
|
|
||||||
});
|
|
||||||
var iconGold = new ol.style.Icon({
|
|
||||||
anchor: [ 0.5, 1 ],
|
|
||||||
src: 'images/marker-gold.png'
|
|
||||||
});
|
|
||||||
olStyles['red'] = new ol.style.Style({
|
|
||||||
image: iconRed
|
|
||||||
});
|
|
||||||
olStyles['green'] = new ol.style.Style({
|
|
||||||
image: iconGreen
|
|
||||||
});
|
|
||||||
olStyles['white'] = new ol.style.Style({
|
|
||||||
image: iconWhite
|
|
||||||
});
|
|
||||||
olStyles['gold'] = new ol.style.Style({
|
|
||||||
image: iconGold
|
|
||||||
});
|
|
||||||
|
|
||||||
// popups
|
|
||||||
var popupContainer = document.createElement('div');
|
|
||||||
popupContainer.id = 'popup';
|
|
||||||
popupContainer.className = 'ol-popup';
|
|
||||||
document.getElementsByTagName('body')[0].appendChild(popupContainer);
|
|
||||||
var popupCloser = document.createElement('a');
|
|
||||||
popupCloser.id = 'popup-closer';
|
|
||||||
popupCloser.className = 'ol-popup-closer';
|
|
||||||
popupCloser.href = '#';
|
|
||||||
popupContainer.appendChild(popupCloser);
|
|
||||||
var popupContent = document.createElement('div');
|
|
||||||
popupContent.id = 'popup-content';
|
|
||||||
popupContainer.appendChild(popupContent);
|
|
||||||
|
|
||||||
var popup = new ol.Overlay({
|
|
||||||
element: popupContainer,
|
|
||||||
autoPan: true,
|
|
||||||
autoPanAnimation: {
|
|
||||||
duration: 250
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
popupCloser.onclick = function() {
|
|
||||||
popup.setPosition(undefined);
|
|
||||||
popupCloser.blur();
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// add click handler to map to show popup
|
|
||||||
map.on('click', function(e) {
|
|
||||||
var coordinate = e.coordinate;
|
|
||||||
var feature = map.forEachFeatureAtPixel(e.pixel,
|
|
||||||
function(feature, layer) {
|
|
||||||
if (layer.get('name') == 'Markers') {
|
|
||||||
return feature;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (feature) {
|
|
||||||
var p = feature.get('p');
|
|
||||||
var i = feature.getId();
|
|
||||||
var posLen = feature.get('posLen');
|
|
||||||
// popup show
|
|
||||||
popup.setPosition(coordinate);
|
|
||||||
popupContent.innerHTML = getPopupHtml(p, i, posLen);
|
|
||||||
map.addOverlay(popup);
|
|
||||||
if (document.getElementById('bottom').style.display == 'block') {
|
|
||||||
var index = 0;
|
|
||||||
for (var key in altitudes) {
|
|
||||||
if (altitudes.hasOwnProperty(key) && key == i) {
|
|
||||||
chart.setSelection([{ row: index, column: null }]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// popup destroy
|
|
||||||
popup.setPosition(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// change mouse cursor when over marker
|
|
||||||
map.on('pointermove', function(e) {
|
|
||||||
var hit = map.forEachFeatureAtPixel(e.pixel, function(feature, layer) {
|
|
||||||
if (layer.get('name') == 'Markers') {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (hit) {
|
|
||||||
this.getTargetElement().style.cursor = 'pointer';
|
|
||||||
} else {
|
|
||||||
this.getTargetElement().style.cursor = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// layer switcher
|
|
||||||
var switcher = document.createElement('div');
|
|
||||||
switcher.id = 'switcher';
|
|
||||||
switcher.className = 'ol-control';
|
|
||||||
document.getElementsByTagName('body')[0].appendChild(switcher);
|
|
||||||
var switcherContent = document.createElement('div');
|
|
||||||
switcherContent.id = 'switcher-content';
|
|
||||||
switcherContent.className = 'ol-layerswitcher';
|
|
||||||
switcher.appendChild(switcherContent);
|
|
||||||
|
|
||||||
map.getLayers().forEach(function (layer) {
|
|
||||||
var layerLabel = document.createElement('label');
|
|
||||||
layerLabel.innerHTML = layer.get('name');
|
|
||||||
switcherContent.appendChild(layerLabel);
|
|
||||||
|
|
||||||
var layerRadio = document.createElement('input');
|
|
||||||
if (layer.get('type') === 'data') {
|
|
||||||
layerRadio.type = 'checkbox';
|
|
||||||
layerLabel.className = 'ol-datalayer';
|
|
||||||
} else {
|
|
||||||
layerRadio.type = 'radio';
|
|
||||||
}
|
|
||||||
layerRadio.name = 'layer';
|
|
||||||
layerRadio.value = layer.get('name');
|
|
||||||
layerRadio.onclick = switchLayer;
|
|
||||||
if (layer.getVisible()) {
|
|
||||||
layerRadio.checked = true;
|
|
||||||
}
|
|
||||||
layerLabel.insertBefore(layerRadio, layerLabel.childNodes[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function switchLayer() {
|
|
||||||
var layerName = this.value;
|
|
||||||
map.getLayers().forEach(function (layer) {
|
|
||||||
if (layer.get('name') === layerName) {
|
|
||||||
if (layer.get('type') === 'data') {
|
|
||||||
if (layer.getVisible()) {
|
|
||||||
layer.setVisible(false);
|
|
||||||
} else {
|
|
||||||
layer.setVisible(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedLayer.setVisible(false);
|
|
||||||
selectedLayer = layer;
|
|
||||||
layer.setVisible(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var switcherButton = document.createElement('button');
|
|
||||||
var layerImg = document.createElement('img');
|
|
||||||
layerImg.src = 'images/layers.svg';
|
|
||||||
layerImg.style.width = '60%';
|
|
||||||
switcherButton.appendChild(layerImg);
|
|
||||||
|
|
||||||
var switcherHandle = function() {
|
|
||||||
var el = document.getElementById('switcher');
|
|
||||||
if (el.style.display === 'block') {
|
|
||||||
el.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
el.style.display = 'block';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
switcherButton.addEventListener('click', switcherHandle, false);
|
|
||||||
switcherButton.addEventListener('touchstart', switcherHandle, false);
|
|
||||||
|
|
||||||
var element = document.createElement('div');
|
|
||||||
element.className = 'ol-switcher-button ol-unselectable ol-control';
|
|
||||||
element.appendChild(switcherButton);
|
|
||||||
|
|
||||||
var switcherControl = new ol.control.Control({
|
|
||||||
element: element
|
|
||||||
});
|
|
||||||
map.addControl(switcherControl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
map = undefined;
|
|
||||||
layerTrack = undefined;
|
|
||||||
layerMarkers = undefined;
|
|
||||||
selectedLayer = undefined;
|
|
||||||
olStyles = undefined;
|
|
||||||
removeElementById('popup');
|
|
||||||
removeElementById('switcher');
|
|
||||||
document.getElementById('map-canvas').innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function displayTrack(xml, update) {
|
|
||||||
altitudes = {};
|
|
||||||
var totalMeters = 0;
|
|
||||||
var totalSeconds = 0;
|
|
||||||
var points = [];
|
|
||||||
var positions = xml.getElementsByTagName('position');
|
|
||||||
var posLen = positions.length;
|
|
||||||
for (var i = 0; i < posLen; i++) {
|
|
||||||
var p = parsePosition(positions[i], i);
|
|
||||||
totalMeters += p.distance;
|
|
||||||
totalSeconds += p.seconds;
|
|
||||||
p.totalMeters = totalMeters;
|
|
||||||
p.totalSeconds = totalSeconds;
|
|
||||||
// set marker
|
|
||||||
setMarker(p, i, posLen);
|
|
||||||
// update polyline
|
|
||||||
var point = ol.proj.fromLonLat([p.longitude, p.latitude]);
|
|
||||||
points.push(point);
|
|
||||||
}
|
|
||||||
var lineString = new ol.geom.LineString(points);
|
|
||||||
|
|
||||||
var lineFeature = new ol.Feature({
|
|
||||||
geometry: lineString,
|
|
||||||
});
|
|
||||||
|
|
||||||
layerTrack.getSource().addFeature(lineFeature);
|
|
||||||
|
|
||||||
var extent = layerTrack.getSource().getExtent();
|
|
||||||
|
|
||||||
map.getControls().forEach(function (el) {
|
|
||||||
if (el instanceof ol.control.ZoomToExtent) {
|
|
||||||
map.removeControl(el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (update) {
|
|
||||||
map.getView().fit(extent);
|
|
||||||
var zoom = map.getView().getZoom();
|
|
||||||
if (zoom > 20) {
|
|
||||||
map.getView().setZoom(20);
|
|
||||||
extent = map.getView().calculateExtent(map.getSize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var zoomToExtentControl = new ol.control.ZoomToExtent({
|
|
||||||
extent: extent,
|
|
||||||
label: getExtentImg()
|
|
||||||
});
|
|
||||||
map.addControl(zoomToExtentControl);
|
|
||||||
|
|
||||||
updateSummary(p.timestamp, totalMeters, totalSeconds);
|
|
||||||
if (p.tid != trackid) {
|
|
||||||
trackid = p.tid;
|
|
||||||
setTrack(trackid);
|
|
||||||
}
|
|
||||||
if (document.getElementById('bottom').style.display == 'block') {
|
|
||||||
// update altitudes chart
|
|
||||||
chart.clearChart();
|
|
||||||
displayChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearMap() {
|
|
||||||
if (layerTrack) {
|
|
||||||
layerTrack.getSource().clear();
|
|
||||||
}
|
|
||||||
if (layerMarkers) {
|
|
||||||
layerMarkers.getSource().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMarker(p, i, posLen) {
|
|
||||||
// marker
|
|
||||||
var marker = new ol.Feature({
|
|
||||||
geometry: new ol.geom.Point(ol.proj.fromLonLat([p.longitude, p.latitude]))
|
|
||||||
});
|
|
||||||
|
|
||||||
if (latest == 1) {
|
|
||||||
var iconStyle = olStyles['red'];
|
|
||||||
} else if (i == 0) {
|
|
||||||
var iconStyle = olStyles['green'];
|
|
||||||
} else if (i == posLen - 1) {
|
|
||||||
var iconStyle = olStyles['red'];
|
|
||||||
} else {
|
|
||||||
var iconStyle = olStyles['white'];
|
|
||||||
}
|
|
||||||
marker.setStyle(iconStyle);
|
|
||||||
marker.setId(i);
|
|
||||||
marker.set('p', p);
|
|
||||||
marker.set('posLen', posLen);
|
|
||||||
layerMarkers.getSource().addFeature(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addChartEvent(chart, data) {
|
|
||||||
google.visualization.events.addListener(chart, 'select', function () {
|
|
||||||
var selection = chart.getSelection()[0];
|
|
||||||
if (selection) {
|
|
||||||
var id = data.getValue(selection.row, 0) - 1;
|
|
||||||
var marker = layerMarkers.getSource().getFeatureById(id);
|
|
||||||
var url = marker.get('src');
|
|
||||||
var initStyle = marker.getStyle();
|
|
||||||
var iconStyle = olStyles['gold'];
|
|
||||||
marker.setStyle(iconStyle);
|
|
||||||
altTimeout = setTimeout(function () { marker.setStyle(initStyle); }, 2000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//20.597985430276808,52.15547181298076,21.363595171488573,52.33750879522563
|
|
||||||
function getBounds() {
|
|
||||||
var extent = map.getView().calculateExtent(map.getSize());
|
|
||||||
var bounds = ol.proj.transformExtent(extent, 'EPSG:900913', 'EPSG:4326');
|
|
||||||
var lon_sw = bounds[0];
|
|
||||||
var lat_sw = bounds[1];
|
|
||||||
var lon_ne = bounds[2];
|
|
||||||
var lat_ne = bounds[3];
|
|
||||||
return [lon_sw, lat_sw, lon_ne, lat_ne];
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoomToExtent() {
|
|
||||||
map.getView().fit(layerMarkers.getSource().getExtent())
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoomToBounds(b) {
|
|
||||||
var bounds = ol.proj.transformExtent(b, 'EPSG:4326', 'EPSG:900913');
|
|
||||||
map.getView().fit(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSize() {
|
|
||||||
map.updateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExtentImg() {
|
|
||||||
var extentImg = document.createElement('img');
|
|
||||||
extentImg.src = 'images/extent.svg';
|
|
||||||
extentImg.style.width = '60%';
|
|
||||||
return extentImg;
|
|
||||||
}
|
|
2
js/dist/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
725
js/main.js
@ -1,725 +0,0 @@
|
|||||||
/* μlogger
|
|
||||||
*
|
|
||||||
* Copyright(C) 2017 Bartek Fabiszewski (www.fabiszewski.net)
|
|
||||||
*
|
|
||||||
* This is free software; you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// general stuff
|
|
||||||
var factor_kmh, unit_kmh, factor_m, unit_m, factor_km, unit_km;
|
|
||||||
if (units == 'imperial') {
|
|
||||||
factor_kmh = 0.62; //to mph
|
|
||||||
unit_kmh = 'mph';
|
|
||||||
factor_m = 3.28; // to feet
|
|
||||||
unit_m = 'ft';
|
|
||||||
factor_km = 0.62; // to miles
|
|
||||||
unit_km = 'mi';
|
|
||||||
} else if (units == 'nautical') {
|
|
||||||
factor_kmh = 0.54; //to knots
|
|
||||||
unit_kmh = 'kt';
|
|
||||||
factor_m = 1; //
|
|
||||||
unit_m = 'm';
|
|
||||||
factor_km = 0.54; // to nautical miles
|
|
||||||
unit_km = 'nm';
|
|
||||||
} else {
|
|
||||||
factor_kmh = 1;
|
|
||||||
unit_kmh = 'km/h';
|
|
||||||
factor_m = 1;
|
|
||||||
unit_m = 'm';
|
|
||||||
factor_km = 1;
|
|
||||||
unit_km = 'km';
|
|
||||||
}
|
|
||||||
var latest = 0;
|
|
||||||
var live = 0;
|
|
||||||
var chart;
|
|
||||||
var altitudes = {};
|
|
||||||
var altTimeout;
|
|
||||||
var gm_error = false;
|
|
||||||
var loadTime = 0;
|
|
||||||
var auto;
|
|
||||||
var savedBounds = null;
|
|
||||||
|
|
||||||
function displayChart() {
|
|
||||||
if (chart) { google.visualization.events.removeAllListeners(chart); }
|
|
||||||
var data = new google.visualization.DataTable();
|
|
||||||
data.addColumn('number', 'id');
|
|
||||||
data.addColumn('number', lang['altitude']);
|
|
||||||
|
|
||||||
for (var id in altitudes) {
|
|
||||||
if (altitudes.hasOwnProperty(id)) {
|
|
||||||
data.addRow([parseInt(id) + 1, Math.round((altitudes[id] * factor_m))]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
title: lang['altitude'] + ' (' + unit_m + ')',
|
|
||||||
hAxis: { textPosition: 'none' },
|
|
||||||
legend: { position: 'none' }
|
|
||||||
};
|
|
||||||
|
|
||||||
chart = new google.visualization.LineChart(document.getElementById('chart'));
|
|
||||||
chart.draw(data, options);
|
|
||||||
|
|
||||||
addChartEvent(chart, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleChart(i) {
|
|
||||||
var altLen = altitudes.length;
|
|
||||||
if (altLen <= 1) { return; }
|
|
||||||
var e = document.getElementById('bottom');
|
|
||||||
if (arguments.length < 1) {
|
|
||||||
if (e.style.display == 'block') { i = 0 }
|
|
||||||
else { i = 1; }
|
|
||||||
}
|
|
||||||
if (i == 0) {
|
|
||||||
chart.clearChart();
|
|
||||||
e.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
e.style.display = 'block';
|
|
||||||
displayChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleChartLink() {
|
|
||||||
var link = document.getElementById('altitudes');
|
|
||||||
if (Object.keys(altitudes).length > 1) {
|
|
||||||
link.style.visibility = 'visible';
|
|
||||||
} else {
|
|
||||||
link.style.visibility = 'hidden';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMenu(i) {
|
|
||||||
var emenu = document.getElementById('menu');
|
|
||||||
var emain = document.getElementById('main');
|
|
||||||
var ebutton = document.getElementById('menu-close');
|
|
||||||
if (arguments.length < 1) {
|
|
||||||
if (ebutton.innerHTML == '»') { i = 0 }
|
|
||||||
else { i = 1; }
|
|
||||||
}
|
|
||||||
if (i == 0) {
|
|
||||||
emenu.style.width = '0';
|
|
||||||
emain.style.marginRight = '0';
|
|
||||||
ebutton.style.right = '0';
|
|
||||||
ebutton.innerHTML = '«';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
emenu.style.width = '165px';
|
|
||||||
emain.style.marginRight = '165px';
|
|
||||||
ebutton.style.right = '165px';
|
|
||||||
ebutton.innerHTML = '»';
|
|
||||||
}
|
|
||||||
updateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getXHR() {
|
|
||||||
var xmlhttp = null;
|
|
||||||
if (window.XMLHttpRequest) {
|
|
||||||
xmlhttp = new XMLHttpRequest();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
|
|
||||||
}
|
|
||||||
return xmlhttp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTrack(userid, trackid, update) {
|
|
||||||
var title = document.getElementById('track').getElementsByClassName('menutitle')[0];
|
|
||||||
if (trackid < 0) { return; }
|
|
||||||
if (latest == 1) { trackid = 0; }
|
|
||||||
var xhr = getXHR();
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
if (xhr.readyState == 4) {
|
|
||||||
if (xhr.status == 200) {
|
|
||||||
var xml = xhr.responseXML;
|
|
||||||
var positions = xml.getElementsByTagName('position');
|
|
||||||
if (positions.length > 0) {
|
|
||||||
clearMap();
|
|
||||||
displayTrack(xml, update);
|
|
||||||
toggleChartLink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr = null;
|
|
||||||
removeLoader(title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.open('GET', 'utils/getpositions.php?trackid=' + trackid + '&userid=' + userid + '&last=' + latest, true);
|
|
||||||
xhr.send();
|
|
||||||
setLoader(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadLastPositionAllUsers() {
|
|
||||||
var xhr = getXHR();
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
if (xhr.readyState == 4) {
|
|
||||||
if (xhr.status == 200) {
|
|
||||||
clearMap();
|
|
||||||
var xml = xhr.responseXML;
|
|
||||||
var positions = xml.getElementsByTagName('position');
|
|
||||||
var posLen = positions.length;
|
|
||||||
var timestampMax = 0;
|
|
||||||
for (var i = 0; i < posLen; i++) {
|
|
||||||
var p = parsePosition(positions[i], i);
|
|
||||||
// set marker
|
|
||||||
setMarker(p, i, posLen);
|
|
||||||
if (p.timestamp > timestampMax) {
|
|
||||||
timestampMax = p.timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zoomToExtent();
|
|
||||||
updateSummary(timestampMax);
|
|
||||||
}
|
|
||||||
xhr = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.open('GET', 'utils/getpositions.php?last=' + latest, true);
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePosition(p, id) {
|
|
||||||
// read data
|
|
||||||
var latitude = parseFloat(getNode(p, 'latitude'));
|
|
||||||
var longitude = parseFloat(getNode(p, 'longitude'));
|
|
||||||
var altitude = getNode(p, 'altitude'); // may be null
|
|
||||||
if (altitude != null) {
|
|
||||||
altitude = parseInt(altitude);
|
|
||||||
// save altitudes for chart
|
|
||||||
altitudes[id] = altitude;
|
|
||||||
}
|
|
||||||
var speed = getNode(p, 'speed'); // may be null
|
|
||||||
if (speed != null) { speed = parseInt(speed); }
|
|
||||||
var bearing = getNode(p, 'bearing'); // may be null
|
|
||||||
if (bearing != null) { bearing = parseInt(bearing); }
|
|
||||||
var accuracy = getNode(p, 'accuracy'); // may be null
|
|
||||||
if (accuracy != null) { accuracy = parseInt(accuracy); }
|
|
||||||
var provider = getNode(p, 'provider'); // may be null
|
|
||||||
var comments = getNode(p, 'comments'); // may be null
|
|
||||||
var username = getNode(p, 'username');
|
|
||||||
var trackname = getNode(p, 'trackname');
|
|
||||||
var tid = getNode(p, 'trackid');
|
|
||||||
var timestamp = getNode(p, 'timestamp');
|
|
||||||
var distance = parseInt(getNode(p, 'distance'));
|
|
||||||
var seconds = parseInt(getNode(p, 'seconds'));
|
|
||||||
return {
|
|
||||||
'latitude': latitude,
|
|
||||||
'longitude': longitude,
|
|
||||||
'altitude': altitude,
|
|
||||||
'speed': speed,
|
|
||||||
'bearing': bearing,
|
|
||||||
'accuracy': accuracy,
|
|
||||||
'provider': provider,
|
|
||||||
'comments': comments,
|
|
||||||
'username': username,
|
|
||||||
'trackname': trackname,
|
|
||||||
'tid': tid,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'distance': distance,
|
|
||||||
'seconds': seconds
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPopupHtml(p, i, count) {
|
|
||||||
var date = '–––';
|
|
||||||
var time = '–––';
|
|
||||||
if (p.timestamp > 0) {
|
|
||||||
var d = new Date(p.timestamp * 1000);
|
|
||||||
date = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
|
|
||||||
time = d.toTimeString();
|
|
||||||
var offset;
|
|
||||||
if ((offset = time.indexOf(' ')) >= 0) {
|
|
||||||
time = time.substr(0, offset) + ' <span class="smaller">' + time.substr(offset + 1) + '</span>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var provider = '';
|
|
||||||
if (p.provider == 'gps') {
|
|
||||||
provider = ' (<img class="icon" alt="' + lang['gps'] + '" title="' + lang['gps'] + '" src="images/gps_dark.svg">)';
|
|
||||||
} else if (p.provider == 'network') {
|
|
||||||
provider = ' (<img class="icon" alt="' + lang['network'] + '" title="' + lang['network'] + '" src="images/network_dark.svg">)';
|
|
||||||
}
|
|
||||||
var stats = '';
|
|
||||||
if (latest == 0) {
|
|
||||||
stats =
|
|
||||||
'<div id="pright">' +
|
|
||||||
'<img class="icon" alt="' + lang['track'] + '" src="images/stats_blue.svg" style="padding-left: 3em;"><br>' +
|
|
||||||
'<img class="icon" alt="' + lang['ttime'] + '" title="' + lang['ttime'] + '" src="images/time_blue.svg"> ' +
|
|
||||||
p.totalSeconds.toHMS() + '<br>' +
|
|
||||||
'<img class="icon" alt="' + lang['aspeed'] + '" title="' + lang['aspeed'] + '" src="images/speed_blue.svg"> ' +
|
|
||||||
((p.totalSeconds > 0) ? ((p.totalMeters / p.totalSeconds).toKmH() * factor_kmh).toFixed() : 0) + ' ' + unit_kmh + '<br>' +
|
|
||||||
'<img class="icon" alt="' + lang['tdistance'] + '" title="' + lang['tdistance'] + '" src="images/distance_blue.svg"> ' +
|
|
||||||
(p.totalMeters.toKm() * factor_km).toFixed(2) + ' ' + unit_km + '<br>' + '</div>';
|
|
||||||
}
|
|
||||||
var popup =
|
|
||||||
'<div id="popup">' +
|
|
||||||
'<div id="pheader">' +
|
|
||||||
'<div><img alt="' + lang['user'] + '" title="' + lang['user'] + '" src="images/user_dark.svg"> ' + htmlEncode(p.username) + '</div>' +
|
|
||||||
'<div><img alt="' + lang['track'] + '" title="' + lang['track'] + '" src="images/route_dark.svg"> ' + htmlEncode(p.trackname) + '</div>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div id="pbody">' +
|
|
||||||
((p.comments != null) ? '<div id="pcomments">' + htmlEncode(p.comments) + '</div>' : '') +
|
|
||||||
'<div id="pleft">' +
|
|
||||||
'<img class="icon" alt="' + lang['time'] + '" title="' + lang['time'] + '" src="images/calendar_dark.svg"> ' + date + '<br>' +
|
|
||||||
'<img class="icon" alt="' + lang['time'] + '" title="' + lang['time'] + '" src="images/clock_dark.svg"> ' + time + '<br>' +
|
|
||||||
((p.speed != null) ? '<img class="icon" alt="' + lang['speed'] + '" title="' + lang['speed'] + '" src="images/speed_dark.svg"> ' +
|
|
||||||
(p.speed.toKmH() * factor_kmh) + ' ' + unit_kmh + '<br>' : '') +
|
|
||||||
((p.altitude != null) ? '<img class="icon" alt="' + lang['altitude'] + '" title="' + lang['altitude'] + '" src="images/altitude_dark.svg"> ' +
|
|
||||||
(p.altitude * factor_m).toFixed() + ' ' + unit_m + '<br>' : '') +
|
|
||||||
((p.accuracy != null) ? '<img class="icon" alt="' + lang['accuracy'] + '" title="' + lang['accuracy'] + '" src="images/accuracy_dark.svg"> ' +
|
|
||||||
(p.accuracy * factor_m).toFixed() + ' ' + unit_m + provider + '<br>' : '') +
|
|
||||||
'</div>' +
|
|
||||||
stats +
|
|
||||||
'</div><div id="pfooter">' + sprintf(lang['pointof'], i + 1, count) + '</div>' +
|
|
||||||
'</div>';
|
|
||||||
return popup;
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportFile(type, userid, trackid) {
|
|
||||||
var url = 'utils/export.php?type=' + type + '&userid=' + userid + '&trackid=' + trackid;
|
|
||||||
window.location.assign(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function importFile(input) {
|
|
||||||
var form = input.parentElement;
|
|
||||||
var title = form.parentElement.getElementsByClassName('menutitle')[0];
|
|
||||||
var sizeMax = form.elements['MAX_FILE_SIZE'].value;
|
|
||||||
if (input.files && input.files.length == 1 && input.files[0].size > sizeMax) {
|
|
||||||
alert(sprintf(lang['isizefailure'], sizeMax));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var xhr = getXHR();
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if (xhr.readyState == 4) {
|
|
||||||
var error = true;
|
|
||||||
var message = '';
|
|
||||||
if (xhr.status == 200) {
|
|
||||||
var xml = xhr.responseXML;
|
|
||||||
if (xml) {
|
|
||||||
var root = xml.getElementsByTagName('root');
|
|
||||||
if (root.length && getNode(root[0], 'error') == 0) {
|
|
||||||
trackId = getNode(root[0], 'trackid');
|
|
||||||
trackCnt = getNode(root[0], 'trackcnt');
|
|
||||||
getTracks(userid, trackId);
|
|
||||||
if (trackCnt > 1) {
|
|
||||||
alert(sprintf(lang['imultiple'], trackCnt));
|
|
||||||
}
|
|
||||||
error = false;
|
|
||||||
} else if (root.length) {
|
|
||||||
errorMsg = getNode(root[0], 'message');
|
|
||||||
if (errorMsg) { message = errorMsg; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
alert(lang['actionfailure'] + '\n' + message);
|
|
||||||
}
|
|
||||||
removeLoader(title);
|
|
||||||
xhr = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.open('POST', 'utils/import.php', true);
|
|
||||||
xhr.send(new FormData(form));
|
|
||||||
input.value = '';
|
|
||||||
setLoader(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLoader(el) {
|
|
||||||
var s = el.textContent || el.innerText;
|
|
||||||
var newHTML = '';
|
|
||||||
for (var i = 0, len = s.length; i < len; i++) {
|
|
||||||
newHTML += '<span class="loader">' + s.charAt(i) + '</span>';
|
|
||||||
}
|
|
||||||
el.innerHTML = newHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLoader(el) {
|
|
||||||
el.innerHTML = el.textContent || el.innerText;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSummary(timestamp, d, s) {
|
|
||||||
var t = document.getElementById('summary');
|
|
||||||
if (latest == 0) {
|
|
||||||
t.innerHTML = '<div class="menutitle u">' + lang['summary'] + '</div>' +
|
|
||||||
'<div><img class="icon" alt="' + lang['tdistance'] + '" title="' + lang['tdistance'] + '" src="images/distance.svg"> ' + (d.toKm() * factor_km).toFixed(2) + ' ' + unit_km + '</div>' +
|
|
||||||
'<div><img class="icon" alt="' + lang['ttime'] + '" title="' + lang['ttime'] + '" src="images/time.svg"> ' + s.toHMS() + '</div>';
|
|
||||||
} else {
|
|
||||||
var today = new Date();
|
|
||||||
var d = new Date(timestamp * 1000);
|
|
||||||
var dateString = '';
|
|
||||||
if (d.toDateString() != today.toDateString()) {
|
|
||||||
dateString += d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
|
|
||||||
dateString += '<br>';
|
|
||||||
}
|
|
||||||
var timeString = d.toTimeString();
|
|
||||||
var offset;
|
|
||||||
if ((offset = timeString.indexOf(' ')) >= 0) {
|
|
||||||
timeString = timeString.substr(0, offset) + ' <span style="font-weight:normal">' + timeString.substr(offset + 1) + '</span>';
|
|
||||||
}
|
|
||||||
t.innerHTML = '<div class="menutitle u">' + lang['latest'] + ':</div>' + dateString + timeString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNode(p, name) {
|
|
||||||
return ((p.getElementsByTagName(name)[0].childNodes[0]) ? p.getElementsByTagName(name)[0].childNodes[0].nodeValue : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// seconds to (d) H:M:S
|
|
||||||
Number.prototype.toHMS = function() {
|
|
||||||
var s = this;
|
|
||||||
var d = Math.floor(s / 86400);
|
|
||||||
var h = Math.floor((s % 86400) / 3600);
|
|
||||||
var m = Math.floor(((s % 86400) % 3600) / 60);
|
|
||||||
s = ((s % 86400) % 3600) % 60;
|
|
||||||
|
|
||||||
return ((d > 0) ? (d + ' d ') : '') + (('00' + h).slice(-2)) + ':' + (('00' + m).slice(-2)) + ':' + (('00' + s).slice(-2)) + '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// meters to km
|
|
||||||
Number.prototype.toKm = function() {
|
|
||||||
return Math.round(this / 10) / 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
// m/s to km/h
|
|
||||||
Number.prototype.toKmH = function() {
|
|
||||||
return Math.round(this * 3600 / 10) / 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
// toggle latest
|
|
||||||
function toggleLatest() {
|
|
||||||
var usersSelect = document.getElementsByName('user')[0];
|
|
||||||
if (latest == 0) {
|
|
||||||
if (!hasAllUsers() && usersSelect && usersSelect.length > 2) {
|
|
||||||
usersSelect.options.add(new Option('- ' + lang['allusers'] + ' -', 'all'), usersSelect.options[1]);
|
|
||||||
}
|
|
||||||
latest = 1;
|
|
||||||
loadTrack(userid, 0, 1);
|
|
||||||
} else {
|
|
||||||
if (usersSelect && hasAllUsers()) {
|
|
||||||
if (isSelectedAllUsers()) {
|
|
||||||
usersSelect.selectedIndex = 0;
|
|
||||||
}
|
|
||||||
usersSelect.remove(1);
|
|
||||||
}
|
|
||||||
latest = 0;
|
|
||||||
loadTrack(userid, trackid, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTrack(t) {
|
|
||||||
document.getElementsByName('track')[0].value = t;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTrack(f) {
|
|
||||||
if (f.selectedIndex >= 0) {
|
|
||||||
trackid = f.options[f.selectedIndex].value;
|
|
||||||
} else {
|
|
||||||
trackid = 0;
|
|
||||||
}
|
|
||||||
document.getElementById('latest').checked = false;
|
|
||||||
if (latest == 1) {
|
|
||||||
toggleLatest();
|
|
||||||
} else {
|
|
||||||
loadTrack(userid, trackid, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectUser(f) {
|
|
||||||
userid = f.options[f.selectedIndex].value;
|
|
||||||
if (isSelectedAllUsers()) {
|
|
||||||
clearOptions(document.getElementsByName('track')[0]);
|
|
||||||
loadLastPositionAllUsers();
|
|
||||||
} else {
|
|
||||||
getTracks(userid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTracks(userid, trackid) {
|
|
||||||
var title = document.getElementById('track').getElementsByClassName('menutitle')[0];
|
|
||||||
var xhr = getXHR();
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
if (xhr.readyState == 4) {
|
|
||||||
if (xhr.status == 200) {
|
|
||||||
var xml = xhr.responseXML;
|
|
||||||
var trackSelect = document.getElementsByName('track')[0];
|
|
||||||
clearOptions(trackSelect);
|
|
||||||
var tracks = xml.getElementsByTagName('track');
|
|
||||||
if (tracks.length > 0) {
|
|
||||||
fillOptions(xml, userid, trackid);
|
|
||||||
} else {
|
|
||||||
clearMap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
removeLoader(title);
|
|
||||||
xhr = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.open('GET', 'utils/gettracks.php?userid=' + userid, true);
|
|
||||||
xhr.send();
|
|
||||||
setLoader(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillOptions(xml, uid, tid) {
|
|
||||||
var trackSelect = document.getElementsByName('track')[0];
|
|
||||||
var tracks = xml.getElementsByTagName('track');
|
|
||||||
var trackLen = tracks.length;
|
|
||||||
for (var i = 0; i < trackLen; i++) {
|
|
||||||
var trackid = getNode(tracks[i], 'trackid');
|
|
||||||
var trackname = getNode(tracks[i], 'trackname');
|
|
||||||
var option = document.createElement('option');
|
|
||||||
option.value = trackid;
|
|
||||||
option.innerHTML = htmlEncode(trackname);
|
|
||||||
trackSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
var defaultTrack = tid || getNode(tracks[0], 'trackid');
|
|
||||||
var defaultUser = uid || userid;
|
|
||||||
loadTrack(defaultUser, defaultTrack, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearOptions(el) {
|
|
||||||
if (el.options) {
|
|
||||||
while (el.options.length) {
|
|
||||||
el.remove(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload(userid, trackid) {
|
|
||||||
if (isSelectedAllUsers()) {
|
|
||||||
loadLastPositionAllUsers();
|
|
||||||
} else {
|
|
||||||
loadTrack(userid, trackid, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoReload() {
|
|
||||||
if (live == 0) {
|
|
||||||
live = 1;
|
|
||||||
if (isSelectedAllUsers()) {
|
|
||||||
auto = setInterval(function () { loadLastPositionAllUsers(); }, interval * 1000);
|
|
||||||
} else {
|
|
||||||
auto = setInterval(function () { loadTrack(userid, trackid, 0); }, interval * 1000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
live = 0;
|
|
||||||
clearInterval(auto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSelectedAllUsers() {
|
|
||||||
var usersSelect = document.getElementsByName('user')[0];
|
|
||||||
return usersSelect && usersSelect[usersSelect.selectedIndex].value == 'all';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasAllUsers() {
|
|
||||||
var usersSelect = document.getElementsByName('user')[0];
|
|
||||||
if (usersSelect && usersSelect.length > 2 && usersSelect.options[1].value == 'all') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTime() {
|
|
||||||
var i = parseInt(prompt(lang['newinterval']));
|
|
||||||
if (!isNaN(i) && i != interval) {
|
|
||||||
interval = i;
|
|
||||||
document.getElementById('auto').innerHTML = interval;
|
|
||||||
// if live tracking on, reload with new interval
|
|
||||||
if (live == 1) {
|
|
||||||
live = 0;
|
|
||||||
clearInterval(auto);
|
|
||||||
autoReload();
|
|
||||||
}
|
|
||||||
// save current state as default
|
|
||||||
setCookie('interval', interval, 30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dynamic change of map api
|
|
||||||
function loadMapAPI(api) {
|
|
||||||
if (api) {
|
|
||||||
mapapi = api;
|
|
||||||
try {
|
|
||||||
savedBounds = getBounds();
|
|
||||||
} catch (e) {
|
|
||||||
savedBounds = null;
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
removeElementById('mapapi');
|
|
||||||
var urls = [];
|
|
||||||
if (mapapi == 'gmaps') {
|
|
||||||
addScript('js/api_gmaps.js', 'mapapi');
|
|
||||||
urls.push('//maps.googleapis.com/maps/api/js?' + ((gkey !== null) ? ('key=' + gkey + '&') : '') + 'callback=init');
|
|
||||||
} else {
|
|
||||||
addScript('js/api_openlayers.js', 'mapapi');
|
|
||||||
urls.push('//cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList')
|
|
||||||
urls.push('js/ol.js');
|
|
||||||
}
|
|
||||||
waitAndLoad(mapapi, urls);
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitAndLoad(api, urls) {
|
|
||||||
// wait till first script loaded
|
|
||||||
if (loadTime > 5000) { loadTime = 0; alert(sprintf(lang['apifailure'], api)); return; }
|
|
||||||
if (typeof loadedAPI === 'undefined' || loadedAPI !== api) {
|
|
||||||
setTimeout(function () { loadTime += 50; waitAndLoad(api, urls); }, 50);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (var i = 0; i < urls.length; i++) {
|
|
||||||
addScript(urls[i], 'mapapi_' + api + '_' + i);
|
|
||||||
}
|
|
||||||
loadTime = 0;
|
|
||||||
waitAndInit(api);
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitAndInit(api) {
|
|
||||||
// wait till main api loads
|
|
||||||
if (loadTime > 10000) { loadTime = 0; alert(sprintf(lang['apifailure'], api)); return; }
|
|
||||||
try {
|
|
||||||
init();
|
|
||||||
} catch (e) {
|
|
||||||
setTimeout(function () { loadTime += 50; waitAndInit(api); }, 50);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadTime = 0;
|
|
||||||
var update = 1;
|
|
||||||
if (savedBounds) {
|
|
||||||
zoomToBounds(savedBounds);
|
|
||||||
update = 0;
|
|
||||||
}
|
|
||||||
if (latest && isSelectedAllUsers()) {
|
|
||||||
loadLastPositionAllUsers();
|
|
||||||
} else {
|
|
||||||
loadTrack(userid, trackid, update);
|
|
||||||
}
|
|
||||||
// save current api as default
|
|
||||||
setCookie('api', api, 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addScript(url, id) {
|
|
||||||
if (id && document.getElementById(id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var tag = document.createElement('script');
|
|
||||||
tag.type = 'text/javascript';
|
|
||||||
tag.src = url;
|
|
||||||
if (id) {
|
|
||||||
tag.id = id;
|
|
||||||
}
|
|
||||||
document.getElementsByTagName('head')[0].appendChild(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCss(url, id) {
|
|
||||||
if (id && document.getElementById(id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var tag = document.createElement('link');
|
|
||||||
tag.type = 'text/css';
|
|
||||||
tag.rel = 'stylesheet';
|
|
||||||
tag.href = url;
|
|
||||||
if (id) {
|
|
||||||
tag.id = id;
|
|
||||||
}
|
|
||||||
document.getElementsByTagName('head')[0].appendChild(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeElementById(id) {
|
|
||||||
var tag = document.getElementById(id);
|
|
||||||
if (tag && tag.parentNode) {
|
|
||||||
tag.parentNode.removeChild(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCookie(name, value, days) {
|
|
||||||
if (days) {
|
|
||||||
var date = new Date();
|
|
||||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
||||||
var expires = '; expires=' + date.toGMTString();
|
|
||||||
} else {
|
|
||||||
var expires = '';
|
|
||||||
}
|
|
||||||
document.cookie = 'ulogger_' + name + '=' + value + expires + '; path=/';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLang(lang) {
|
|
||||||
setCookie('lang', lang, 30);
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUnits(unit) {
|
|
||||||
units = unit;
|
|
||||||
setCookie('units', unit, 30);
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showModal(contentHTML) {
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.setAttribute('id', 'modal');
|
|
||||||
div.innerHTML = '<div id="modal-header"><button type="button" onclick="removeModal()"><img alt="' + lang['close'] + '" src="images/close.svg"></button></div><div id="modal-body"></div>';
|
|
||||||
document.body.appendChild(div);
|
|
||||||
var modalBody = document.getElementById('modal-body');
|
|
||||||
modalBody.innerHTML = contentHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeModal() {
|
|
||||||
document.body.removeChild(document.getElementById('modal'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function userMenu() {
|
|
||||||
var dropdown = document.getElementById('user_dropdown');
|
|
||||||
if (dropdown.classList.contains('show')) {
|
|
||||||
dropdown.classList.remove('show');
|
|
||||||
} else {
|
|
||||||
dropdown.classList.add('show');
|
|
||||||
window.addEventListener('click', removeOnClick, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeOnClick(event) {
|
|
||||||
var parent = event.target.parentElement;
|
|
||||||
var dropdown = document.getElementById('user_dropdown');
|
|
||||||
dropdown.classList.remove('show');
|
|
||||||
window.removeEventListener('click', removeOnClick, true);
|
|
||||||
if (!parent.classList.contains('dropdown')) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// naive approach, only %s, %d supported
|
|
||||||
function sprintf() {
|
|
||||||
var args = Array.prototype.slice.call(arguments);
|
|
||||||
var format = args.shift();
|
|
||||||
var i = 0;
|
|
||||||
return format.replace(/%%|%s|%d/g, function(match) {
|
|
||||||
if (match == '%%') { return '%'; }
|
|
||||||
return (typeof args[i] != 'undefined') ? args[i++] : match;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function htmlEncode(s) {
|
|
||||||
return s.replace(/&/g, '&')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert hex string and opacity to an rgba string
|
|
||||||
function hexToRGBA(hex, opacity) {
|
|
||||||
return 'rgba(' + (hex = hex.replace('#', '')).match(new RegExp('(.{' + hex.length/3 + '})', 'g')).map(function(l) { return parseInt(hex.length%2 ? l+l : l, 16) }).concat(opacity||1).join(',') + ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!String.prototype.trim) {
|
|
||||||
String.prototype.trim = function () {
|
|
||||||
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
|
||||||
};
|
|
||||||
}
|
|
78
js/pass.js
@ -1,78 +0,0 @@
|
|||||||
/* μlogger
|
|
||||||
*
|
|
||||||
* Copyright(C) 2017 Bartek Fabiszewski (www.fabiszewski.net)
|
|
||||||
*
|
|
||||||
* This is free software; you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function changePass() {
|
|
||||||
var form = '<form id="passForm" method="post" onsubmit="submitPass(); return false">';
|
|
||||||
form += '<label><b>' + lang['oldpassword'] + '</b></label><input type="password" placeholder="' + lang['passwordenter'] + '" name="oldpass" required>';
|
|
||||||
form += '<label><b>' + lang['newpassword'] + '</b></label><input type="password" placeholder="' + lang['passwordenter'] + '" name="pass" required>';
|
|
||||||
form += '<label><b>' + lang['newpasswordrepeat'] + '</b></label><input type="password" placeholder="' + lang['passwordenter'] + '" name="pass2" required>';
|
|
||||||
form += '<button type="button" onclick="removeModal()">' + lang['cancel'] + '</button><button type="submit">' + lang['submit'] + '</button>';
|
|
||||||
form += '</form>';
|
|
||||||
showModal(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitPass() {
|
|
||||||
var form = document.getElementById('passForm');
|
|
||||||
var oldpass = form.elements['oldpass'].value;
|
|
||||||
var pass = form.elements['pass'].value;
|
|
||||||
var pass2 = form.elements['pass2'].value;
|
|
||||||
if (!oldpass || !pass || !pass2) {
|
|
||||||
alert(lang['allrequired']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pass != pass2) {
|
|
||||||
alert(lang['passnotmatch']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!pass_regex.test(pass)) {
|
|
||||||
alert(lang['passlenmin'] + '\n' + lang['passrules']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var xhr = getXHR();
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
if (xhr.readyState == 4) {
|
|
||||||
var error = true;
|
|
||||||
var message = '';
|
|
||||||
if (xhr.status == 200) {
|
|
||||||
var xml = xhr.responseXML;
|
|
||||||
if (xml) {
|
|
||||||
var root = xml.getElementsByTagName('root');
|
|
||||||
if (root.length && getNode(root[0], 'error') == 0) {
|
|
||||||
removeModal();
|
|
||||||
alert(lang['actionsuccess']);
|
|
||||||
error = false;
|
|
||||||
} else if (root.length) {
|
|
||||||
errorMsg = getNode(root[0], 'message');
|
|
||||||
if (errorMsg) { message = errorMsg; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
alert(lang['actionfailure'] + '\n' + message);
|
|
||||||
}
|
|
||||||
xhr = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.open('POST', 'utils/changepass.php', true);
|
|
||||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
|
||||||
var params = 'oldpass=' + encodeURIComponent(oldpass) + '&pass=' + encodeURIComponent(pass);
|
|
||||||
params = params.replace(/%20/g, '+');
|
|
||||||
xhr.send(params);
|
|
||||||
return;
|
|
||||||
}
|
|
111
js/src/ajax.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class uAjax {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform POST HTTP request
|
||||||
|
* @alias ajax
|
||||||
|
*/
|
||||||
|
static post(url, data, options) {
|
||||||
|
const params = options || {};
|
||||||
|
params.method = 'POST';
|
||||||
|
return this.ajax(url, data, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform GET HTTP request
|
||||||
|
* @alias ajax
|
||||||
|
*/
|
||||||
|
static get(url, data, options) {
|
||||||
|
const params = options || {};
|
||||||
|
params.method = 'GET';
|
||||||
|
return this.ajax(url, data, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform ajax HTTP request
|
||||||
|
* @param {string} url Request URL
|
||||||
|
* @param {Object|HTMLFormElement} [data] Optional request parameters: key/value pairs or form element
|
||||||
|
* @param {Object} [options] Optional options
|
||||||
|
* @param {string} [options.method='GET'] Optional query method, default 'GET'
|
||||||
|
* @return {Promise<Object, Error>}
|
||||||
|
*/
|
||||||
|
static ajax(url, data, options) {
|
||||||
|
const params = [];
|
||||||
|
data = data || {};
|
||||||
|
options = options || {};
|
||||||
|
const method = options.method || 'GET';
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState !== XMLHttpRequest.DONE) { return; }
|
||||||
|
let message = '';
|
||||||
|
let error = true;
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(xhr.responseText);
|
||||||
|
if (obj) {
|
||||||
|
if (!obj.error) {
|
||||||
|
if (resolve && typeof resolve === 'function') {
|
||||||
|
resolve(obj);
|
||||||
|
}
|
||||||
|
error = false;
|
||||||
|
} else if (obj.message) {
|
||||||
|
message = obj.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = `HTTP error ${xhr.status}`;
|
||||||
|
}
|
||||||
|
if (error && reject && typeof reject === 'function') {
|
||||||
|
reject(new Error(message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let body;
|
||||||
|
if (data instanceof HTMLFormElement) {
|
||||||
|
if (method === 'POST') {
|
||||||
|
body = new FormData(data);
|
||||||
|
} else {
|
||||||
|
body = new URLSearchParams(new FormData(data)).toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const key in data) {
|
||||||
|
if (data.hasOwnProperty(key)) {
|
||||||
|
params.push(`${key}=${encodeURIComponent(data[key])}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body = params.join('&');
|
||||||
|
body = body.replace(/%20/g, '+');
|
||||||
|
}
|
||||||
|
if (method === 'GET' && body.length) {
|
||||||
|
url += `?${body}`;
|
||||||
|
body = null;
|
||||||
|
}
|
||||||
|
xhr.open(method, url, true);
|
||||||
|
if (method === 'POST' && !(data instanceof HTMLFormElement)) {
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||||
|
}
|
||||||
|
xhr.send(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
94
js/src/auth.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uUser from './user.js';
|
||||||
|
|
||||||
|
export default class uAuth {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
/** @type {boolean} */
|
||||||
|
this._isAdmin = false;
|
||||||
|
/** @type {boolean} */
|
||||||
|
this._isAuthenticated = false;
|
||||||
|
/** @type {?uUser} */
|
||||||
|
this._user = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uUser} user
|
||||||
|
*/
|
||||||
|
set user(user) {
|
||||||
|
if (user) {
|
||||||
|
this._user = user;
|
||||||
|
this._isAuthenticated = true;
|
||||||
|
} else {
|
||||||
|
this._user = null;
|
||||||
|
this._isAuthenticated = false;
|
||||||
|
this._isAdmin = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} isAdmin
|
||||||
|
*/
|
||||||
|
set isAdmin(isAdmin) {
|
||||||
|
if (!this._user) {
|
||||||
|
throw new Error('No authenticated user');
|
||||||
|
}
|
||||||
|
this._isAdmin = isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isAdmin() {
|
||||||
|
return this._isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isAuthenticated() {
|
||||||
|
return this._isAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {?uUser}
|
||||||
|
*/
|
||||||
|
get user() {
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load auth state from data object
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {boolean} data.isAdmin
|
||||||
|
* @param {boolean} data.isAuthenticated
|
||||||
|
* @param {?number} data.userId
|
||||||
|
* @param {?string} data.userLogin
|
||||||
|
*/
|
||||||
|
load(data) {
|
||||||
|
if (data) {
|
||||||
|
if (data.isAuthenticated) {
|
||||||
|
this.user = new uUser(data.userId, data.userLogin);
|
||||||
|
this.isAdmin = data.isAdmin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
213
js/src/chartviewmodel.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $ } from './initializer.js';
|
||||||
|
import Chartist from 'chartist'
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import ctAxisTitle from 'chartist-plugin-axistitle';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PlotPoint
|
||||||
|
* @property {number} x
|
||||||
|
* @property {number} y
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @typedef {PlotPoint[]} PlotData
|
||||||
|
*/
|
||||||
|
|
||||||
|
// FIXME: Chartist is not suitable for large data sets
|
||||||
|
const LARGE_DATA = 1000;
|
||||||
|
export default class ChartViewModel extends ViewModel {
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
*/
|
||||||
|
constructor(state) {
|
||||||
|
super({
|
||||||
|
pointSelected: null,
|
||||||
|
chartVisible: false,
|
||||||
|
buttonVisible: false,
|
||||||
|
onChartToggle: null,
|
||||||
|
onMenuToggle: null
|
||||||
|
});
|
||||||
|
this.state = state;
|
||||||
|
/** @type {PlotData} */
|
||||||
|
this.data = [];
|
||||||
|
/** @type {?Chartist.Line} */
|
||||||
|
this.chart = null;
|
||||||
|
/** @type {?NodeListOf<SVGLineElement>} */
|
||||||
|
this.chartPoints = null;
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
this.chartElement = document.querySelector('#chart');
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
this.chartContainer = this.chartElement.parentElement;
|
||||||
|
/** @type {HTMLAnchorElement} */
|
||||||
|
this.buttonElement = document.querySelector('#altitudes');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {ChartViewModel}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.chartSetup();
|
||||||
|
this.setObservers();
|
||||||
|
this.bindAll();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
chartSetup() {
|
||||||
|
uUtils.addCss('css/chartist.min.css', 'chartist_css');
|
||||||
|
this.chart = new Chartist.Line(this.chartElement, {
|
||||||
|
series: [ this.data ]
|
||||||
|
}, {
|
||||||
|
lineSmooth: true,
|
||||||
|
showArea: true,
|
||||||
|
axisX: {
|
||||||
|
type: Chartist.AutoScaleAxis,
|
||||||
|
onlyInteger: true,
|
||||||
|
showLabel: false
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
ctAxisTitle({
|
||||||
|
axisY: {
|
||||||
|
axisTitle: `${$._('altitude')} (${$.unit('unitDistance')})`,
|
||||||
|
axisClass: 'ct-axis-title',
|
||||||
|
offset: {
|
||||||
|
x: 0,
|
||||||
|
y: 11
|
||||||
|
},
|
||||||
|
textAnchor: 'middle',
|
||||||
|
flipTitle: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chart.on('created', () => this.onCreated());
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreated() {
|
||||||
|
if (this.data.length && this.data.length <= LARGE_DATA) {
|
||||||
|
this.chartPoints = document.querySelectorAll('.ct-series .ct-point');
|
||||||
|
const len = this.chartPoints.length;
|
||||||
|
for (let id = 0; id < len; id++) {
|
||||||
|
this.chartPoints[id].addEventListener('click', () => {
|
||||||
|
this.model.pointSelected = id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setObservers() {
|
||||||
|
this.state.onChanged('currentTrack', (track) => {
|
||||||
|
this.render();
|
||||||
|
this.model.buttonVisible = !!track && track.hasPlotData;
|
||||||
|
});
|
||||||
|
this.onChanged('buttonVisible', (visible) => this.renderButton(visible));
|
||||||
|
this.onChanged('chartVisible', (visible) => this.renderContainer(visible));
|
||||||
|
this.model.onChartToggle = () => {
|
||||||
|
this.model.chartVisible = !this.model.chartVisible;
|
||||||
|
};
|
||||||
|
this.model.onMenuToggle = () => {
|
||||||
|
this.chart.update();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} isVisible
|
||||||
|
*/
|
||||||
|
renderContainer(isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
this.chartContainer.style.display = 'block';
|
||||||
|
this.render();
|
||||||
|
} else {
|
||||||
|
this.chartContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} isVisible
|
||||||
|
*/
|
||||||
|
renderButton(isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
this.buttonElement.style.visibility = 'visible';
|
||||||
|
} else {
|
||||||
|
this.buttonElement.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let data = [];
|
||||||
|
if (this.state.currentTrack && this.state.currentTrack.hasPlotData && this.model.chartVisible) {
|
||||||
|
data = this.state.currentTrack.plotData;
|
||||||
|
} else {
|
||||||
|
this.model.chartVisible = false;
|
||||||
|
}
|
||||||
|
if (this.data !== data) {
|
||||||
|
console.log(`Chart update (${data.length})`);
|
||||||
|
this.data = data;
|
||||||
|
const options = {
|
||||||
|
lineSmooth: (data.length <= LARGE_DATA)
|
||||||
|
};
|
||||||
|
this.chart.update({ series: [ data ] }, options, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} pointId
|
||||||
|
* @param {string} $className
|
||||||
|
*/
|
||||||
|
pointAddClass(pointId, $className) {
|
||||||
|
if (this.model.chartVisible && this.chartPoints.length > pointId) {
|
||||||
|
const point = this.chartPoints[pointId];
|
||||||
|
point.classList.add($className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} $className
|
||||||
|
*/
|
||||||
|
pointsRemoveClass($className) {
|
||||||
|
if (this.model.chartVisible && this.chartPoints) {
|
||||||
|
this.chartPoints.forEach((el) => el.classList.remove($className));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} pointId
|
||||||
|
*/
|
||||||
|
onPointOver(pointId) {
|
||||||
|
this.pointAddClass(pointId, 'ct-point-hilight');
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointOut() {
|
||||||
|
this.pointsRemoveClass('ct-point-hilight');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} pointId
|
||||||
|
*/
|
||||||
|
onPointSelect(pointId) {
|
||||||
|
this.pointAddClass(pointId, 'ct-point-selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointUnselect() {
|
||||||
|
this.pointsRemoveClass('ct-point-selected');
|
||||||
|
}
|
||||||
|
}
|
127
js/src/config.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uObserve from './observe.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class uConfig
|
||||||
|
* @property {number} interval;
|
||||||
|
* @property {string} units
|
||||||
|
* @property {string} mapApi
|
||||||
|
* @property {?string} gkey
|
||||||
|
* @property {Object<string, string>} olLayers
|
||||||
|
* @property {number} initLatitude
|
||||||
|
* @property {number} initLongitude
|
||||||
|
* @property {RegExp} passRegex
|
||||||
|
* @property {number} strokeWeight
|
||||||
|
* @property {string} strokeColor
|
||||||
|
* @property {number} strokeOpacity
|
||||||
|
* @property {boolean} showLatest
|
||||||
|
* @property {string} colorNormal
|
||||||
|
* @property {string} colorStart
|
||||||
|
* @property {string} colorStop
|
||||||
|
* @property {string} colorExtra
|
||||||
|
* @property {string} colorHilite
|
||||||
|
*/
|
||||||
|
export default class uConfig {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.interval = 10;
|
||||||
|
this.units = 'metric';
|
||||||
|
this.lang = 'en';
|
||||||
|
this.mapApi = 'openlayers';
|
||||||
|
this.gkey = null;
|
||||||
|
this.olLayers = {};
|
||||||
|
this.initLatitude = 52.23;
|
||||||
|
this.initLongitude = 21.01;
|
||||||
|
this.passRegex = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{12,})');
|
||||||
|
this.strokeWeight = 2;
|
||||||
|
this.strokeColor = '#ff0000';
|
||||||
|
this.strokeOpacity = 1;
|
||||||
|
// marker colors
|
||||||
|
this.colorNormal = '#fff';
|
||||||
|
this.colorStart = '#55b500';
|
||||||
|
this.colorStop = '#ff6a00';
|
||||||
|
this.colorExtra = '#ccc';
|
||||||
|
this.colorHilite = '#feff6a';
|
||||||
|
this.initUnits();
|
||||||
|
}
|
||||||
|
|
||||||
|
initUnits() {
|
||||||
|
if (this.units === 'imperial') {
|
||||||
|
this.factorSpeed = 2.237; // m/s to mph
|
||||||
|
this.unitSpeed = 'unitmph';
|
||||||
|
this.factorDistance = 3.28; // m to feet
|
||||||
|
this.unitDistance = 'unitft';
|
||||||
|
this.factorDistanceMajor = 0.621; // km to miles
|
||||||
|
this.unitDistanceMajor = 'unitmi';
|
||||||
|
} else if (this.units === 'nautical') {
|
||||||
|
this.factorSpeed = 1.944; // m/s to kt
|
||||||
|
this.unitSpeed = 'unitkt';
|
||||||
|
this.factorDistance = 1; // meters
|
||||||
|
this.unitDistance = 'unitm';
|
||||||
|
this.factorDistanceMajor = 0.54; // km to nautical miles
|
||||||
|
this.unitDistanceMajor = 'unitnm';
|
||||||
|
} else {
|
||||||
|
this.factorSpeed = 3.6; // m/s to km/h
|
||||||
|
this.unitSpeed = 'unitkmh';
|
||||||
|
this.factorDistance = 1;
|
||||||
|
this.unitDistance = 'unitm';
|
||||||
|
this.factorDistanceMajor = 1;
|
||||||
|
this.unitDistanceMajor = 'unitkm';
|
||||||
|
}
|
||||||
|
this.unitDay = 'unitday';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load config values from data object
|
||||||
|
* @param {Object} data
|
||||||
|
*/
|
||||||
|
load(data) {
|
||||||
|
if (data) {
|
||||||
|
for (const property in data) {
|
||||||
|
if (data.hasOwnProperty(property) && this.hasOwnProperty(property)) {
|
||||||
|
this[property] = data[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.passRegex) {
|
||||||
|
const re = data.passRegex;
|
||||||
|
this.passRegex = new RegExp(re.substr(1, re.length - 2));
|
||||||
|
}
|
||||||
|
this.initUnits();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reinitialize() {
|
||||||
|
uObserve.unobserveAll(this);
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} property
|
||||||
|
* @param {ObserveCallback} callback
|
||||||
|
*/
|
||||||
|
onChanged(property, callback) {
|
||||||
|
uObserve.observe(this, property, callback);
|
||||||
|
}
|
||||||
|
}
|
73
js/src/configviewmodel.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $, config } from './initializer.js';
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class ConfigViewModel
|
||||||
|
*/
|
||||||
|
export default class ConfigViewModel extends ViewModel {
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
*/
|
||||||
|
constructor(state) {
|
||||||
|
super(config);
|
||||||
|
this.state = state;
|
||||||
|
this.model.onSetInterval = () => this.setAutoReloadInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {ConfigViewModel}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.setObservers();
|
||||||
|
this.bindAll();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setObservers() {
|
||||||
|
this.onChanged('mapApi', (api) => {
|
||||||
|
uUtils.setCookie('api', api);
|
||||||
|
});
|
||||||
|
this.onChanged('lang', (_lang) => {
|
||||||
|
uUtils.setCookie('lang', _lang);
|
||||||
|
ConfigViewModel.reload();
|
||||||
|
});
|
||||||
|
this.onChanged('units', (units) => {
|
||||||
|
uUtils.setCookie('units', units);
|
||||||
|
ConfigViewModel.reload();
|
||||||
|
});
|
||||||
|
this.onChanged('interval', (interval) => {
|
||||||
|
uUtils.setCookie('interval', interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static reload() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAutoReloadInterval() {
|
||||||
|
const interval = parseInt(prompt($._('newinterval')));
|
||||||
|
if (!isNaN(interval) && interval !== this.model.interval) {
|
||||||
|
this.model.interval = interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
js/src/dialog.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $ } from './initializer.js';
|
||||||
|
|
||||||
|
export default class uDialog {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds modal dialog
|
||||||
|
* @param {(string|Node|NodeList|Array.<Node>)} content
|
||||||
|
*/
|
||||||
|
constructor(content) {
|
||||||
|
const dialog = document.createElement('div');
|
||||||
|
dialog.setAttribute('id', 'modal');
|
||||||
|
const dialogHeader = document.createElement('div');
|
||||||
|
dialogHeader.setAttribute('id', 'modal-header');
|
||||||
|
const buttonClose = document.createElement('button');
|
||||||
|
buttonClose.setAttribute('id', 'modal-close');
|
||||||
|
buttonClose.setAttribute('type', 'button');
|
||||||
|
buttonClose.setAttribute('class', 'button-reject');
|
||||||
|
buttonClose.setAttribute('data-bind', 'onCancel');
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.setAttribute('src', 'images/close.svg');
|
||||||
|
img.setAttribute('alt', $._('close'));
|
||||||
|
buttonClose.append(img);
|
||||||
|
dialogHeader.append(buttonClose);
|
||||||
|
dialog.append(dialogHeader);
|
||||||
|
const dialogBody = document.createElement('div');
|
||||||
|
dialogBody.setAttribute('id', 'modal-body');
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
dialogBody.innerHTML = content;
|
||||||
|
} else if (content instanceof NodeList || content instanceof Array) {
|
||||||
|
for (const node of content) {
|
||||||
|
dialogBody.append(node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialogBody.append(content);
|
||||||
|
}
|
||||||
|
dialog.append(dialogBody);
|
||||||
|
this.element = dialog;
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show modal dialog
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
if (!this.visible) {
|
||||||
|
document.body.append(this.element);
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove modal dialog
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
document.body.removeChild(this.element);
|
||||||
|
this.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show confirmation dialog and return user decision
|
||||||
|
* @param {string} message
|
||||||
|
* @return {boolean} True if confirmed, false otherwise
|
||||||
|
*/
|
||||||
|
static isConfirmed(message) {
|
||||||
|
return confirm(message);
|
||||||
|
}
|
||||||
|
}
|
67
js/src/initializer.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uAjax from './ajax.js';
|
||||||
|
import uAuth from './auth.js';
|
||||||
|
import uConfig from './config.js';
|
||||||
|
import uLang from './lang.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Class uInitializer
|
||||||
|
* @property {uAuth} auth
|
||||||
|
* @property {uConfig} config
|
||||||
|
* @property {uLang} lang
|
||||||
|
*/
|
||||||
|
export class uInitializer {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.auth = new uAuth();
|
||||||
|
this.config = new uConfig();
|
||||||
|
this.lang = new uLang();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
return uAjax.get('utils/getinit.php').then((_data) => {
|
||||||
|
if (!_data || !_data.auth || !_data.config || !_data.lang) {
|
||||||
|
throw new Error('Corrupted initialization data');
|
||||||
|
}
|
||||||
|
this.auth.load(_data.auth);
|
||||||
|
this.config.load(_data.config);
|
||||||
|
this.lang.init(this.config, _data.lang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static waitForDom() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
|
setTimeout(resolve, 1);
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initializer = new uInitializer();
|
||||||
|
export const config = initializer.config;
|
||||||
|
export const lang = initializer.lang;
|
||||||
|
export const auth = initializer.auth;
|
164
js/src/lang.js
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
export default class uLang {
|
||||||
|
constructor() {
|
||||||
|
this.strings = {};
|
||||||
|
this.config = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uConfig} config
|
||||||
|
* @param {Object<string, string>} data
|
||||||
|
*/
|
||||||
|
init(config, data) {
|
||||||
|
this.config = config;
|
||||||
|
if (data) {
|
||||||
|
/** @type {Object<string, string>} */
|
||||||
|
this.strings = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {...(string|number)=} params Optional parameters
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
_(name, ...params) {
|
||||||
|
if (typeof this.strings[name] === 'undefined') {
|
||||||
|
throw new Error('Unknown localized string');
|
||||||
|
}
|
||||||
|
if (params.length) {
|
||||||
|
return uUtils.sprintf(this.strings[name], ...params);
|
||||||
|
}
|
||||||
|
return this.strings[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
unit(name) {
|
||||||
|
const unitName = this.config[name];
|
||||||
|
if (typeof this.config[name] === 'undefined') {
|
||||||
|
throw new Error('Unknown localized unit');
|
||||||
|
}
|
||||||
|
return this._(unitName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get speed converted to locale units
|
||||||
|
* @param {number} ms Speed in meters per second
|
||||||
|
* @param {boolean} withUnit
|
||||||
|
* @return {(number|string)} String when with unit
|
||||||
|
*/
|
||||||
|
getLocaleSpeed(ms, withUnit) {
|
||||||
|
const value = Math.round(ms * this.config.factorSpeed * 100) / 100;
|
||||||
|
if (withUnit) {
|
||||||
|
return `${value.toLocaleString(this.config.lang)} ${this.unit('unitSpeed')}`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get distance converted to locale units
|
||||||
|
* @param {number} m Distance in meters
|
||||||
|
* @param {boolean} withUnit
|
||||||
|
* @return {(number|string)} String when with unit
|
||||||
|
*/
|
||||||
|
getLocaleDistanceMajor(m, withUnit) {
|
||||||
|
const value = Math.round(m * this.config.factorDistanceMajor / 10) / 100;
|
||||||
|
if (withUnit) {
|
||||||
|
return `${value.toLocaleString(this.config.lang)} ${this.unit('unitDistanceMajor')}`
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} m Distance in meters
|
||||||
|
* @param {boolean} withUnit
|
||||||
|
* @return {(number|string)} String when with unit
|
||||||
|
*/
|
||||||
|
getLocaleDistance(m, withUnit) {
|
||||||
|
const value = Math.round(m * this.config.factorDistance * 100) / 100;
|
||||||
|
if (withUnit) {
|
||||||
|
return `${value.toLocaleString(this.config.lang)} ${this.unit('unitDistance')}`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} m Distance in meters
|
||||||
|
* @param {boolean} withUnit
|
||||||
|
* @return {(number|string)} String when with unit
|
||||||
|
*/
|
||||||
|
getLocaleAltitude(m, withUnit) {
|
||||||
|
return this.getLocaleDistance(m, withUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} m Distance in meters
|
||||||
|
* @param {boolean} withUnit
|
||||||
|
* @return {(number|string)} String when with unit
|
||||||
|
*/
|
||||||
|
getLocaleAccuracy(m, withUnit) {
|
||||||
|
return this.getLocaleDistance(m, withUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} s Duration in seconds
|
||||||
|
* @return {string} Formatted to (d) h:m:s
|
||||||
|
*/
|
||||||
|
getLocaleDuration(s) {
|
||||||
|
const d = Math.floor(s / 86400);
|
||||||
|
const h = Math.floor((s % 86400) / 3600);
|
||||||
|
const m = Math.floor(((s % 86400) % 3600) / 60);
|
||||||
|
s = ((s % 86400) % 3600) % 60;
|
||||||
|
return ((d > 0) ? (`${d} ${this.unit('unitDay')} `) : '') +
|
||||||
|
((`00${h}`).slice(-2)) + ':' + ((`00${m}`).slice(-2)) + ':' + ((`00${s}`).slice(-2)) + '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uPosition} pos
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getLocaleCoordinates(pos) {
|
||||||
|
return `${this.coordStr(pos.longitude, true)} ${this.coordStr(pos.latitude, false)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} pos
|
||||||
|
* @param {boolean} isLon
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
coordStr(pos, isLon) {
|
||||||
|
const ipos = Math.trunc(pos);
|
||||||
|
const dec = Math.abs((pos - ipos) * 60);
|
||||||
|
let dir;
|
||||||
|
|
||||||
|
if (isLon) {
|
||||||
|
dir = pos < 0 ? 'W' : 'E';
|
||||||
|
} else {
|
||||||
|
dir = pos < 0 ? 'S' : 'N';
|
||||||
|
}
|
||||||
|
return `${Math.abs(ipos).toLocaleString(this.config.lang)}°${dec.toLocaleString(this.config.lang, { maximumFractionDigits: 2 })}'${dir}`;
|
||||||
|
}
|
||||||
|
}
|
42
js/src/lib/ol.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Control, Rotate, ScaleLine, Zoom, ZoomToExtent } from 'ol/control';
|
||||||
|
import { LineString, Point } from 'ol/geom';
|
||||||
|
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||||
|
import Feature from 'ol/Feature';
|
||||||
|
import Icon from 'ol/style/Icon';
|
||||||
|
import Map from 'ol/Map';
|
||||||
|
import OSM from 'ol/source/OSM';
|
||||||
|
import Overlay from 'ol/Overlay';
|
||||||
|
import Stroke from 'ol/style/Stroke';
|
||||||
|
import Style from 'ol/style/Style';
|
||||||
|
import TileLayer from 'ol/layer/Tile';
|
||||||
|
import Vector from 'ol/source/Vector';
|
||||||
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
|
import View from 'ol/View';
|
||||||
|
import XYZ from 'ol/source/XYZ';
|
||||||
|
|
||||||
|
export { Feature, Map, Overlay, View };
|
||||||
|
export const control = { Control, Rotate, ScaleLine, Zoom, ZoomToExtent };
|
||||||
|
export const geom = { LineString, Point };
|
||||||
|
export const layer = { TileLayer, VectorLayer };
|
||||||
|
export const proj = { fromLonLat, toLonLat };
|
||||||
|
export const source = { OSM, Vector, XYZ };
|
||||||
|
export const style = { Icon, Stroke, Style };
|
48
js/src/listitem.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uSelect from './select.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class uListItem
|
||||||
|
* @property {string} listValue
|
||||||
|
* @property {string} listText
|
||||||
|
*/
|
||||||
|
export default class uListItem {
|
||||||
|
/**
|
||||||
|
* @param {string|number} id
|
||||||
|
* @param {string|number} value
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.listValue = uSelect.allValue;
|
||||||
|
this.listText = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
listItem(id, value) {
|
||||||
|
this.listValue = String(id);
|
||||||
|
this.listText = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
return `[${this.listValue}, ${this.listText}]`;
|
||||||
|
}
|
||||||
|
}
|
83
js/src/mainviewmodel.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
|
||||||
|
const hiddenClass = 'menu-hidden';
|
||||||
|
|
||||||
|
export default class MainViewModel extends ViewModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
*/
|
||||||
|
constructor(state) {
|
||||||
|
super({
|
||||||
|
onMenuToggle: null,
|
||||||
|
onShowUserMenu: null
|
||||||
|
});
|
||||||
|
this.state = state;
|
||||||
|
this.model.onMenuToggle = () => this.toggleSideMenu();
|
||||||
|
this.model.onShowUserMenu = () => this.toggleUserMenu();
|
||||||
|
this.hideUserMenuCallback = (e) => this.hideUserMenu(e);
|
||||||
|
this.menuEl = document.querySelector('#menu');
|
||||||
|
this.userMenuEl = document.querySelector('#user-menu');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {MainViewModel}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.bindAll();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSideMenu() {
|
||||||
|
if (this.menuEl.classList.contains(hiddenClass)) {
|
||||||
|
this.menuEl.classList.remove(hiddenClass);
|
||||||
|
} else {
|
||||||
|
this.menuEl.classList.add(hiddenClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle user menu visibility
|
||||||
|
*/
|
||||||
|
toggleUserMenu() {
|
||||||
|
if (this.userMenuEl.classList.contains(hiddenClass)) {
|
||||||
|
this.userMenuEl.classList.remove(hiddenClass);
|
||||||
|
window.addEventListener('click', this.hideUserMenuCallback, true);
|
||||||
|
} else {
|
||||||
|
this.userMenuEl.classList.add(hiddenClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click listener callback to hide user menu
|
||||||
|
* @param {MouseEvent} event
|
||||||
|
*/
|
||||||
|
hideUserMenu(event) {
|
||||||
|
const el = event.target;
|
||||||
|
this.userMenuEl.classList.add(hiddenClass);
|
||||||
|
window.removeEventListener('click', this.hideUserMenuCallback, true);
|
||||||
|
if (el.parentElement.id !== 'user-menu') {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
367
js/src/mapapi/api_gmaps.js
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $, config } from '../initializer.js';
|
||||||
|
import MapViewModel from '../mapviewmodel.js';
|
||||||
|
import uTrack from '../track.js';
|
||||||
|
import uUtils from '../utils.js';
|
||||||
|
|
||||||
|
// google maps
|
||||||
|
/**
|
||||||
|
* Google Maps API
|
||||||
|
* @class GoogleMapsApi
|
||||||
|
* @implements {MapViewModel.api}
|
||||||
|
*/
|
||||||
|
export default class GoogleMapsApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MapViewModel} vm
|
||||||
|
*/
|
||||||
|
constructor(vm) {
|
||||||
|
/** @type {google.maps.Map} */
|
||||||
|
this.map = null;
|
||||||
|
/** @type {MapViewModel} */
|
||||||
|
this.viewModel = vm;
|
||||||
|
/** @type {google.maps.Polyline[]} */
|
||||||
|
this.polies = [];
|
||||||
|
/** @type {google.maps.Marker[]} */
|
||||||
|
this.markers = [];
|
||||||
|
/** @type {google.maps.InfoWindow} */
|
||||||
|
this.popup = null;
|
||||||
|
/** @type {number} */
|
||||||
|
this.timeoutHandle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and initialize api scripts
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
const params = `?${(config.gkey) ? `key=${config.gkey}&` : ''}callback=gm_loaded`;
|
||||||
|
const gmReady = Promise.all([
|
||||||
|
GoogleMapsApi.onScriptLoaded(),
|
||||||
|
uUtils.loadScript(`https://maps.googleapis.com/maps/api/js${params}`, 'mapapi_gmaps', GoogleMapsApi.loadTimeoutMs)
|
||||||
|
]);
|
||||||
|
return gmReady.then(() => this.initMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to Google Maps callbacks
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
static onScriptLoaded() {
|
||||||
|
const timeout = uUtils.timeoutPromise(GoogleMapsApi.loadTimeoutMs);
|
||||||
|
const gmInitialize = new Promise((resolve, reject) => {
|
||||||
|
window.gm_loaded = () => {
|
||||||
|
GoogleMapsApi.gmInitialized = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
window.gm_authFailure = () => {
|
||||||
|
GoogleMapsApi.authError = true;
|
||||||
|
let message = $._('apifailure', 'Google Maps');
|
||||||
|
message += '<br><br>' + $._('gmauthfailure');
|
||||||
|
message += '<br><br>' + $._('gmapilink');
|
||||||
|
if (GoogleMapsApi.gmInitialized) {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
reject(new Error(message));
|
||||||
|
};
|
||||||
|
if (GoogleMapsApi.authError) {
|
||||||
|
window.gm_authFailure();
|
||||||
|
}
|
||||||
|
if (GoogleMapsApi.gmInitialized) {
|
||||||
|
window.gm_loaded();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.race([ gmInitialize, timeout ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start map engine when loaded
|
||||||
|
*/
|
||||||
|
initMap() {
|
||||||
|
const mapOptions = {
|
||||||
|
center: new google.maps.LatLng(config.initLatitude, config.initLongitude),
|
||||||
|
zoom: 8,
|
||||||
|
mapTypeId: google.maps.MapTypeId.TERRAIN,
|
||||||
|
scaleControl: true,
|
||||||
|
controlSize: 30
|
||||||
|
};
|
||||||
|
// noinspection JSCheckFunctionSignatures
|
||||||
|
this.map = new google.maps.Map(this.viewModel.mapElement, mapOptions);
|
||||||
|
this.popup = new google.maps.InfoWindow();
|
||||||
|
this.popup.addListener('closeclick', () => {
|
||||||
|
this.popupClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up API
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
this.polies.length = 0;
|
||||||
|
this.markers.length = 0;
|
||||||
|
this.popup = null;
|
||||||
|
if (this.map && this.map.getDiv()) {
|
||||||
|
this.map.getDiv().innerHTML = '';
|
||||||
|
}
|
||||||
|
this.map = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display track
|
||||||
|
* @param {uPositionSet} track
|
||||||
|
* @param {boolean} update Should fit bounds if true
|
||||||
|
*/
|
||||||
|
displayTrack(track, update) {
|
||||||
|
if (!track || !track.hasPositions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// init polyline
|
||||||
|
const polyOptions = {
|
||||||
|
strokeColor: config.strokeColor,
|
||||||
|
strokeOpacity: config.strokeOpacity,
|
||||||
|
strokeWeight: config.strokeWeight
|
||||||
|
};
|
||||||
|
// noinspection JSCheckFunctionSignatures
|
||||||
|
let poly;
|
||||||
|
const latlngbounds = new google.maps.LatLngBounds();
|
||||||
|
if (this.polies.length) {
|
||||||
|
poly = this.polies[0];
|
||||||
|
for (let i = 0; i < this.markers.length; i++) {
|
||||||
|
latlngbounds.extend(this.markers[i].getPosition());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
poly = new google.maps.Polyline(polyOptions);
|
||||||
|
poly.setMap(this.map);
|
||||||
|
this.polies.push(poly);
|
||||||
|
}
|
||||||
|
const path = poly.getPath();
|
||||||
|
let start = this.markers.length;
|
||||||
|
if (start > 0) {
|
||||||
|
this.removePoint(--start);
|
||||||
|
}
|
||||||
|
for (let i = start; i < track.length; i++) {
|
||||||
|
// set marker
|
||||||
|
this.setMarker(i, track);
|
||||||
|
// update polyline
|
||||||
|
const position = track.positions[i];
|
||||||
|
const coordinates = new google.maps.LatLng(position.latitude, position.longitude);
|
||||||
|
if (track instanceof uTrack) {
|
||||||
|
path.push(coordinates);
|
||||||
|
}
|
||||||
|
latlngbounds.extend(coordinates);
|
||||||
|
}
|
||||||
|
if (update) {
|
||||||
|
this.map.fitBounds(latlngbounds);
|
||||||
|
if (track.length === 1) {
|
||||||
|
// only one point, zoom out
|
||||||
|
const zListener =
|
||||||
|
google.maps.event.addListenerOnce(this.map, 'bounds_changed', function () {
|
||||||
|
if (this.getZoom()) {
|
||||||
|
this.setZoom(15);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTimeout(function () {
|
||||||
|
google.maps.event.removeListener(zListener);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear map
|
||||||
|
*/
|
||||||
|
clearMap() {
|
||||||
|
if (this.polies) {
|
||||||
|
for (let i = 0; i < this.polies.length; i++) {
|
||||||
|
this.polies[i].setMap(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.markers) {
|
||||||
|
for (let i = 0; i < this.markers.length; i++) {
|
||||||
|
this.markers[i].setMap(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.popup.getMap()) {
|
||||||
|
this.popupClose();
|
||||||
|
}
|
||||||
|
this.popup.setContent('');
|
||||||
|
this.markers.length = 0;
|
||||||
|
this.polies.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} fill Fill color
|
||||||
|
* @param {boolean} isLarge Is large icon
|
||||||
|
* @param {boolean} isExtra Is styled with extra mark
|
||||||
|
* @return {google.maps.Icon}
|
||||||
|
*/
|
||||||
|
static getMarkerIcon(fill, isLarge, isExtra) {
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
return {
|
||||||
|
anchor: new google.maps.Point(15, 35),
|
||||||
|
url: MapViewModel.getSvgSrc(fill, isLarge, isExtra)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set marker
|
||||||
|
* @param {uPositionSet} track
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
setMarker(id, track) {
|
||||||
|
// marker
|
||||||
|
const position = track.positions[id];
|
||||||
|
// noinspection JSCheckFunctionSignatures
|
||||||
|
const marker = new google.maps.Marker({
|
||||||
|
position: new google.maps.LatLng(position.latitude, position.longitude),
|
||||||
|
title: (new Date(position.timestamp * 1000)).toLocaleString(),
|
||||||
|
map: this.map
|
||||||
|
});
|
||||||
|
const isExtra = position.hasComment() || position.hasImage();
|
||||||
|
let icon;
|
||||||
|
if (track.isLastPosition(id)) {
|
||||||
|
icon = GoogleMapsApi.getMarkerIcon(config.colorStop, true, isExtra);
|
||||||
|
} else if (track.isFirstPosition(id)) {
|
||||||
|
icon = GoogleMapsApi.getMarkerIcon(config.colorStart, true, isExtra);
|
||||||
|
} else {
|
||||||
|
icon = GoogleMapsApi.getMarkerIcon(isExtra ? config.colorExtra : config.colorNormal, false, isExtra);
|
||||||
|
}
|
||||||
|
marker.setIcon(icon);
|
||||||
|
|
||||||
|
marker.addListener('click', () => {
|
||||||
|
this.popupOpen(id, marker);
|
||||||
|
});
|
||||||
|
marker.addListener('mouseover', () => {
|
||||||
|
this.viewModel.model.markerOver = id;
|
||||||
|
});
|
||||||
|
marker.addListener('mouseout', () => {
|
||||||
|
this.viewModel.model.markerOver = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.markers.push(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
removePoint(id) {
|
||||||
|
if (this.markers.length > id) {
|
||||||
|
this.markers[id].setMap(null);
|
||||||
|
this.markers.splice(id, 1);
|
||||||
|
if (this.polies.length) {
|
||||||
|
this.polies[0].getPath().removeAt(id);
|
||||||
|
}
|
||||||
|
if (this.viewModel.model.markerSelect === id) {
|
||||||
|
this.popupClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open popup on marker with given id
|
||||||
|
* @param {number} id
|
||||||
|
* @param {google.maps.Marker} marker
|
||||||
|
*/
|
||||||
|
popupOpen(id, marker) {
|
||||||
|
this.popup.setContent(this.viewModel.getPopupElement(id));
|
||||||
|
this.popup.open(this.map, marker);
|
||||||
|
this.viewModel.model.markerSelect = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close popup
|
||||||
|
*/
|
||||||
|
popupClose() {
|
||||||
|
this.viewModel.model.markerSelect = null;
|
||||||
|
this.popup.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animate marker
|
||||||
|
* @param id Marker sequential id
|
||||||
|
*/
|
||||||
|
animateMarker(id) {
|
||||||
|
if (this.popup.getMap()) {
|
||||||
|
this.popupClose();
|
||||||
|
clearTimeout(this.timeoutHandle);
|
||||||
|
}
|
||||||
|
const icon = this.markers[id].getIcon();
|
||||||
|
this.markers[id].setIcon(GoogleMapsApi.getMarkerIcon(config.colorHilite, false, false));
|
||||||
|
this.markers[id].setAnimation(google.maps.Animation.BOUNCE);
|
||||||
|
this.timeoutHandle = setTimeout(() => {
|
||||||
|
this.markers[id].setIcon(icon);
|
||||||
|
this.markers[id].setAnimation(null);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get map bounds
|
||||||
|
* @returns {number[]} Bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
||||||
|
*/
|
||||||
|
getBounds() {
|
||||||
|
const bounds = this.map.getBounds();
|
||||||
|
const lat_sw = bounds.getSouthWest().lat();
|
||||||
|
const lon_sw = bounds.getSouthWest().lng();
|
||||||
|
const lat_ne = bounds.getNorthEast().lat();
|
||||||
|
const lon_ne = bounds.getNorthEast().lng();
|
||||||
|
return [ lon_sw, lat_sw, lon_ne, lat_ne ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom to track extent
|
||||||
|
*/
|
||||||
|
zoomToExtent() {
|
||||||
|
const bounds = new google.maps.LatLngBounds();
|
||||||
|
for (let i = 0; i < this.markers.length; i++) {
|
||||||
|
bounds.extend(this.markers[i].getPosition());
|
||||||
|
}
|
||||||
|
this.map.fitBounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom to bounds
|
||||||
|
* @param {number[]} bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
||||||
|
*/
|
||||||
|
zoomToBounds(bounds) {
|
||||||
|
const sw = new google.maps.LatLng(bounds[1], bounds[0]);
|
||||||
|
const ne = new google.maps.LatLng(bounds[3], bounds[2]);
|
||||||
|
const latLngBounds = new google.maps.LatLngBounds(sw, ne);
|
||||||
|
this.map.fitBounds(latLngBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update size
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
updateSize() {
|
||||||
|
// ignore for google API
|
||||||
|
}
|
||||||
|
|
||||||
|
static get loadTimeoutMs() {
|
||||||
|
return 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {boolean} */
|
||||||
|
GoogleMapsApi.authError = false;
|
||||||
|
/** @type {boolean} */
|
||||||
|
GoogleMapsApi.gmInitialized = false;
|
626
js/src/mapapi/api_openlayers.js
Normal file
@ -0,0 +1,626 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MapViewModel from '../mapviewmodel.js';
|
||||||
|
import { config } from '../initializer.js';
|
||||||
|
import uTrack from '../track.js';
|
||||||
|
import uUtils from '../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MarkerStyles
|
||||||
|
* @property {Style} normal
|
||||||
|
* @property {Style} start
|
||||||
|
* @property {Style} stop
|
||||||
|
* @property {Style} extra
|
||||||
|
* @property {Style} startExtra
|
||||||
|
* @property {Style} stopExtra
|
||||||
|
* @property {Style} hilite
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import("../lib/ol.js")} OpenLayers
|
||||||
|
* @property {import("ol/control")} control
|
||||||
|
* @property {import("ol/Feature").default} Feature
|
||||||
|
* @property {import("ol/geom")} geom
|
||||||
|
* @property {import("ol/layer/Tile").default} layer.TileLayer
|
||||||
|
* @property {import("ol/layer/Vector").default} layer.VectorLayer
|
||||||
|
* @property {import("ol/Map").default} Map
|
||||||
|
* @property {import("ol/Overlay").default} Overlay
|
||||||
|
* @property {import("ol/proj")} proj
|
||||||
|
* @property {import("ol/source/OSM").default} source.OSM
|
||||||
|
* @property {import("ol/source/Vector").default} source.Vector
|
||||||
|
* @property {import("ol/source/XYZ").default} source.XYZ
|
||||||
|
* @property {import("ol/style/Icon").default} style.Icon
|
||||||
|
* @property {import("ol/style/Stroke").default} style.Stroke
|
||||||
|
* @property {import("ol/style/Style").default} style.Style
|
||||||
|
* @property {import("ol/View").default} View
|
||||||
|
*/
|
||||||
|
/** @type {?OpenLayers} */
|
||||||
|
let ol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenLayers API
|
||||||
|
* @class OpenLayersApi
|
||||||
|
* @implements {MapViewModel.api}
|
||||||
|
*/
|
||||||
|
export default class OpenLayersApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MapViewModel} vm
|
||||||
|
* @param {?OpenLayers=} olModule
|
||||||
|
*/
|
||||||
|
constructor(vm, olModule = null) {
|
||||||
|
/** @type {Map} */
|
||||||
|
this.map = null;
|
||||||
|
/** @type {MapViewModel} */
|
||||||
|
this.viewModel = vm;
|
||||||
|
/** @type {VectorLayer} */
|
||||||
|
this.layerTrack = null;
|
||||||
|
/** @type {VectorLayer} */
|
||||||
|
this.layerMarkers = null;
|
||||||
|
/** @type {Layer} */
|
||||||
|
this.selectedLayer = null;
|
||||||
|
/** @type {?MarkerStyles} */
|
||||||
|
this.markerStyles = null;
|
||||||
|
/** @type {?Overlay} */
|
||||||
|
this.popup = null;
|
||||||
|
// for tests
|
||||||
|
if (olModule) { ol = olModule; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize map
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
uUtils.addCss('css/ol.css', 'ol_css');
|
||||||
|
const olReady = ol ? Promise.resolve() : import(/* webpackChunkName : "ol" */'../lib/ol.js').then((m) => { ol = m; });
|
||||||
|
return olReady.then(() => {
|
||||||
|
this.initMap();
|
||||||
|
this.initLayers();
|
||||||
|
this.initStyles();
|
||||||
|
this.initPopups();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initMap() {
|
||||||
|
const controls = [
|
||||||
|
new ol.control.Zoom(),
|
||||||
|
new ol.control.Rotate(),
|
||||||
|
new ol.control.ScaleLine()
|
||||||
|
];
|
||||||
|
|
||||||
|
const view = new ol.View({
|
||||||
|
center: ol.proj.fromLonLat([ config.initLongitude, config.initLatitude ]),
|
||||||
|
zoom: 8
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map = new ol.Map({
|
||||||
|
target: this.viewModel.mapElement,
|
||||||
|
controls: controls,
|
||||||
|
view: view
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map.on('pointermove', (e) => {
|
||||||
|
const feature = this.map.forEachFeatureAtPixel(e.pixel,
|
||||||
|
/**
|
||||||
|
* @param {Feature} _feature
|
||||||
|
* @param {Layer} _layer
|
||||||
|
* @return {Feature}
|
||||||
|
*/
|
||||||
|
(_feature, _layer) => {
|
||||||
|
if (_layer.get('name') === 'Markers') {
|
||||||
|
return _feature;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (feature) {
|
||||||
|
this.map.getTargetElement().style.cursor = 'pointer';
|
||||||
|
const id = feature.getId();
|
||||||
|
if (id !== this.viewModel.model.markerOver) {
|
||||||
|
this.viewModel.model.markerOver = id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.map.getTargetElement().style.cursor = '';
|
||||||
|
this.viewModel.model.markerOver = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize map layers
|
||||||
|
*/
|
||||||
|
initLayers() {
|
||||||
|
// default layer: OpenStreetMap
|
||||||
|
const osm = new ol.layer.TileLayer({
|
||||||
|
name: 'OpenStreetMap',
|
||||||
|
visible: true,
|
||||||
|
source: new ol.source.OSM()
|
||||||
|
});
|
||||||
|
this.map.addLayer(osm);
|
||||||
|
this.selectedLayer = osm;
|
||||||
|
|
||||||
|
// add extra tile layers
|
||||||
|
for (const layerName in config.olLayers) {
|
||||||
|
if (config.olLayers.hasOwnProperty(layerName)) {
|
||||||
|
const layerUrl = config.olLayers[layerName];
|
||||||
|
const ol_layer = new ol.layer.TileLayer({
|
||||||
|
name: layerName,
|
||||||
|
visible: false,
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: layerUrl
|
||||||
|
})
|
||||||
|
});
|
||||||
|
this.map.addLayer(ol_layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add track and markers layers
|
||||||
|
const lineStyle = new ol.style.Style({
|
||||||
|
stroke: new ol.style.Stroke({
|
||||||
|
color: uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity),
|
||||||
|
width: config.strokeWeight
|
||||||
|
})
|
||||||
|
});
|
||||||
|
this.layerTrack = new ol.layer.VectorLayer({
|
||||||
|
name: 'Track',
|
||||||
|
type: 'data',
|
||||||
|
source: new ol.source.Vector(),
|
||||||
|
style: lineStyle
|
||||||
|
});
|
||||||
|
this.layerMarkers = new ol.layer.VectorLayer({
|
||||||
|
name: 'Markers',
|
||||||
|
type: 'data',
|
||||||
|
source: new ol.source.Vector()
|
||||||
|
});
|
||||||
|
this.map.addLayer(this.layerTrack);
|
||||||
|
this.map.addLayer(this.layerMarkers);
|
||||||
|
|
||||||
|
this.initLayerSwitcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
initStyles() {
|
||||||
|
const anchor = [ 0.5, 1 ];
|
||||||
|
this.markerStyles = {
|
||||||
|
start: new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: anchor,
|
||||||
|
src: MapViewModel.getSvgSrc(config.colorStart, true)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
stop: new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: anchor,
|
||||||
|
src: MapViewModel.getSvgSrc(config.colorStop, true)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
normal: new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: anchor,
|
||||||
|
opacity: 0.7,
|
||||||
|
src: MapViewModel.getSvgSrc(config.colorNormal, false)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
extra: new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: anchor,
|
||||||
|
src: MapViewModel.getSvgSrc(config.colorExtra, false, true)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
startExtra: new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: anchor,
|
||||||
|
src: MapViewModel.getSvgSrc(config.colorStart, true, true)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
stopExtra: new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: anchor,
|
||||||
|
src: MapViewModel.getSvgSrc(config.colorStop, true, true)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
hilite: new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: anchor,
|
||||||
|
src: MapViewModel.getSvgSrc(config.colorHilite, false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
initPopups() {
|
||||||
|
const popupContainer = document.createElement('div');
|
||||||
|
popupContainer.id = 'popup-container';
|
||||||
|
popupContainer.className = 'ol-popup';
|
||||||
|
const popupContent = document.createElement('div');
|
||||||
|
popupContent.id = 'popup-content';
|
||||||
|
popupContainer.appendChild(popupContent);
|
||||||
|
const popupCloser = document.createElement('a');
|
||||||
|
popupCloser.className = 'ol-popup-closer';
|
||||||
|
popupContainer.appendChild(popupCloser);
|
||||||
|
|
||||||
|
this.popup = new ol.Overlay({
|
||||||
|
element: popupContainer,
|
||||||
|
autoPan: true,
|
||||||
|
autoPanAnimation: {
|
||||||
|
duration: 250
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.map.addOverlay(this.popup);
|
||||||
|
|
||||||
|
popupCloser.onclick = () => {
|
||||||
|
this.popupClose();
|
||||||
|
popupCloser.blur();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// add click handler to map to show popup
|
||||||
|
this.map.on('click', (e) => {
|
||||||
|
const coordinate = e.coordinate;
|
||||||
|
const feature = this.map.forEachFeatureAtPixel(e.pixel,
|
||||||
|
/** @param {Feature} _feature
|
||||||
|
* @param {Layer} _layer
|
||||||
|
* @return {?Feature}
|
||||||
|
*/
|
||||||
|
(_feature, _layer) => {
|
||||||
|
if (_layer.get('name') === 'Markers') {
|
||||||
|
return _feature;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (feature) {
|
||||||
|
this.popupOpen(feature.getId(), coordinate);
|
||||||
|
} else {
|
||||||
|
this.popupClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show popup at coordinate
|
||||||
|
* @param {number} id
|
||||||
|
* @param {Coordinate} coordinate
|
||||||
|
*/
|
||||||
|
popupOpen(id, coordinate) {
|
||||||
|
this.popup.getElement().firstElementChild.innerHTML = '';
|
||||||
|
this.popup.getElement().firstElementChild.appendChild(this.viewModel.getPopupElement(id));
|
||||||
|
this.popup.setPosition(coordinate);
|
||||||
|
this.viewModel.model.markerSelect = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close popup
|
||||||
|
*/
|
||||||
|
popupClose() {
|
||||||
|
if (this.popup) {
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
this.popup.setPosition(undefined);
|
||||||
|
this.popup.getElement().firstElementChild.innerHTML = '';
|
||||||
|
}
|
||||||
|
this.viewModel.model.markerSelect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch layer to target
|
||||||
|
* @param {string} targetName
|
||||||
|
*/
|
||||||
|
switchLayer(targetName) {
|
||||||
|
this.map.getLayers().forEach(/** @param {Layer} _layer */(_layer) => {
|
||||||
|
if (_layer.get('name') === targetName) {
|
||||||
|
if (_layer.get('type') === 'data') {
|
||||||
|
if (_layer.getVisible()) {
|
||||||
|
_layer.setVisible(false);
|
||||||
|
} else {
|
||||||
|
_layer.setVisible(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectedLayer.setVisible(false);
|
||||||
|
this.selectedLayer = _layer;
|
||||||
|
_layer.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initLayerSwitcher() {
|
||||||
|
const switcher = document.createElement('div');
|
||||||
|
switcher.id = 'switcher';
|
||||||
|
switcher.className = 'ol-control';
|
||||||
|
document.body.appendChild(switcher);
|
||||||
|
const switcherContent = document.createElement('div');
|
||||||
|
switcherContent.id = 'switcher-content';
|
||||||
|
switcherContent.className = 'ol-layerswitcher';
|
||||||
|
switcher.appendChild(switcherContent);
|
||||||
|
const switcherCloser = document.createElement('a');
|
||||||
|
switcherCloser.className = 'ol-popup-closer';
|
||||||
|
switcher.appendChild(switcherCloser);
|
||||||
|
|
||||||
|
this.map.getLayers().forEach(/** @param {Layer} _layer */(_layer) => {
|
||||||
|
const layerLabel = document.createElement('label');
|
||||||
|
layerLabel.innerHTML = _layer.get('name');
|
||||||
|
switcherContent.appendChild(layerLabel);
|
||||||
|
|
||||||
|
const layerRadio = document.createElement('input');
|
||||||
|
if (_layer.get('type') === 'data') {
|
||||||
|
layerRadio.type = 'checkbox';
|
||||||
|
layerLabel.className = 'ol-datalayer';
|
||||||
|
} else {
|
||||||
|
layerRadio.type = 'radio';
|
||||||
|
}
|
||||||
|
layerRadio.name = 'layer';
|
||||||
|
layerRadio.value = _layer.get('name');
|
||||||
|
layerRadio.onclick = (e) => {
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
|
const el = e.target;
|
||||||
|
this.switchLayer(el.value);
|
||||||
|
};
|
||||||
|
if (_layer.getVisible()) {
|
||||||
|
layerRadio.checked = true;
|
||||||
|
}
|
||||||
|
layerLabel.insertBefore(layerRadio, layerLabel.childNodes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const switcherButton = document.createElement('button');
|
||||||
|
const layerImg = document.createElement('img');
|
||||||
|
layerImg.src = 'images/layers.svg';
|
||||||
|
layerImg.style.width = '60%';
|
||||||
|
switcherButton.appendChild(layerImg);
|
||||||
|
|
||||||
|
const switcherHandle = () => {
|
||||||
|
if (switcher.style.display === 'block') {
|
||||||
|
switcher.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
switcher.style.display = 'block';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switcherCloser.addEventListener('click', switcherHandle, false);
|
||||||
|
switcherButton.addEventListener('click', switcherHandle, false);
|
||||||
|
switcherButton.addEventListener('touchstart', switcherHandle, false);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = 'ol-switcher-button ol-unselectable ol-control';
|
||||||
|
element.appendChild(switcherButton);
|
||||||
|
|
||||||
|
const switcherControl = new ol.control.Control({
|
||||||
|
element: element
|
||||||
|
});
|
||||||
|
this.map.addControl(switcherControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up API
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
this.layerTrack = null;
|
||||||
|
this.layerMarkers = null;
|
||||||
|
this.selectedLayer = null;
|
||||||
|
this.markerStyles = null;
|
||||||
|
uUtils.removeElementById('switcher');
|
||||||
|
if (this.map && this.map.getTargetElement()) {
|
||||||
|
this.map.getTargetElement().innerHTML = '';
|
||||||
|
}
|
||||||
|
this.map = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display track
|
||||||
|
* @param {uPositionSet} track Track
|
||||||
|
* @param {boolean} update Should fit bounds if true
|
||||||
|
*/
|
||||||
|
displayTrack(track, update) {
|
||||||
|
if (!track || !track.hasPositions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let start = this.layerMarkers ? this.layerMarkers.getSource().getFeatures().length : 0;
|
||||||
|
if (start > 0) {
|
||||||
|
this.removePoint(--start);
|
||||||
|
}
|
||||||
|
for (let i = start; i < track.length; i++) {
|
||||||
|
this.setMarker(i, track);
|
||||||
|
}
|
||||||
|
if (track instanceof uTrack) {
|
||||||
|
let lineString;
|
||||||
|
if (this.layerTrack && this.layerTrack.getSource().getFeatures().length) {
|
||||||
|
lineString = this.layerTrack.getSource().getFeatures()[0].getGeometry();
|
||||||
|
} else {
|
||||||
|
lineString = new ol.geom.LineString([]);
|
||||||
|
const lineFeature = new ol.Feature({ geometry: lineString });
|
||||||
|
this.layerTrack.getSource().addFeature(lineFeature);
|
||||||
|
}
|
||||||
|
for (let i = start; i < track.length; i++) {
|
||||||
|
const position = track.positions[i];
|
||||||
|
lineString.appendCoordinate(ol.proj.fromLonLat([ position.longitude, position.latitude ]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let extent = this.layerMarkers.getSource().getExtent();
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
extent = this.fitToExtent(extent);
|
||||||
|
}
|
||||||
|
this.setZoomToExtent(extent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set or replace ZoomToExtent control
|
||||||
|
* @param extent
|
||||||
|
*/
|
||||||
|
setZoomToExtent(extent) {
|
||||||
|
this.map.getControls().forEach((el) => {
|
||||||
|
if (el instanceof ol.control.ZoomToExtent) {
|
||||||
|
this.map.removeControl(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.map.addControl(new ol.control.ZoomToExtent({
|
||||||
|
extent,
|
||||||
|
label: OpenLayersApi.getExtentImg()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fit to extent, zoom out if needed
|
||||||
|
* @param {Array.<number>} extent
|
||||||
|
* @return {Array.<number>}
|
||||||
|
*/
|
||||||
|
fitToExtent(extent) {
|
||||||
|
this.map.getView().fit(extent, { padding: [ 40, 10, 10, 10 ] });
|
||||||
|
const zoom = this.map.getView().getZoom();
|
||||||
|
if (zoom > OpenLayersApi.ZOOM_MAX) {
|
||||||
|
this.map.getView().setZoom(OpenLayersApi.ZOOM_MAX);
|
||||||
|
extent = this.map.getView().calculateExtent(this.map.getSize());
|
||||||
|
}
|
||||||
|
return extent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear map
|
||||||
|
*/
|
||||||
|
clearMap() {
|
||||||
|
this.popupClose();
|
||||||
|
if (this.layerTrack) {
|
||||||
|
this.layerTrack.getSource().clear();
|
||||||
|
}
|
||||||
|
if (this.layerMarkers) {
|
||||||
|
this.layerMarkers.getSource().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get marker style
|
||||||
|
* @param {number} id
|
||||||
|
* @param {uPositionSet} track
|
||||||
|
* @return {Style}
|
||||||
|
*/
|
||||||
|
getMarkerStyle(id, track) {
|
||||||
|
const position = track.positions[id];
|
||||||
|
let iconStyle = this.markerStyles.normal;
|
||||||
|
if (position.hasComment() || position.hasImage()) {
|
||||||
|
if (track.isLastPosition(id)) {
|
||||||
|
iconStyle = this.markerStyles.stopExtra;
|
||||||
|
} else if (track.isFirstPosition(id)) {
|
||||||
|
iconStyle = this.markerStyles.startExtra;
|
||||||
|
} else {
|
||||||
|
iconStyle = this.markerStyles.extra;
|
||||||
|
}
|
||||||
|
} else if (track.isLastPosition(id)) {
|
||||||
|
iconStyle = this.markerStyles.stop;
|
||||||
|
} else if (track.isFirstPosition(id)) {
|
||||||
|
iconStyle = this.markerStyles.start;
|
||||||
|
}
|
||||||
|
return iconStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set marker
|
||||||
|
* @param {number} id
|
||||||
|
* @param {uPositionSet} track
|
||||||
|
*/
|
||||||
|
setMarker(id, track) {
|
||||||
|
// marker
|
||||||
|
const position = track.positions[id];
|
||||||
|
const marker = new ol.Feature({
|
||||||
|
geometry: new ol.geom.Point(ol.proj.fromLonLat([ position.longitude, position.latitude ]))
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconStyle = this.getMarkerStyle(id, track);
|
||||||
|
marker.setStyle(iconStyle);
|
||||||
|
marker.setId(id);
|
||||||
|
this.layerMarkers.getSource().addFeature(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
removePoint(id) {
|
||||||
|
const marker = this.layerMarkers.getSource().getFeatureById(id);
|
||||||
|
if (marker) {
|
||||||
|
this.layerMarkers.getSource().removeFeature(marker);
|
||||||
|
if (this.layerTrack) {
|
||||||
|
const lineString = this.layerTrack.getSource().getFeatures()[0].getGeometry();
|
||||||
|
const coordinates = lineString.getCoordinates();
|
||||||
|
coordinates.splice(id, 1);
|
||||||
|
lineString.setCoordinates(coordinates);
|
||||||
|
}
|
||||||
|
if (this.viewModel.model.markerSelect === id) {
|
||||||
|
this.popupClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animate marker
|
||||||
|
* @param id Marker sequential id
|
||||||
|
*/
|
||||||
|
animateMarker(id) {
|
||||||
|
const marker = this.layerMarkers.getSource().getFeatureById(id);
|
||||||
|
const initStyle = marker.getStyle();
|
||||||
|
marker.setStyle(this.markerStyles.hilite);
|
||||||
|
setTimeout(() => marker.setStyle(initStyle), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get map bounds
|
||||||
|
* eg. (20.597985430276808, 52.15547181298076, 21.363595171488573, 52.33750879522563)
|
||||||
|
* @returns {number[]} Bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
||||||
|
*/
|
||||||
|
getBounds() {
|
||||||
|
const extent = this.map.getView().calculateExtent(this.map.getSize());
|
||||||
|
const sw = ol.proj.toLonLat([ extent[0], extent[1] ]);
|
||||||
|
const ne = ol.proj.toLonLat([ extent[2], extent[3] ]);
|
||||||
|
return [ sw[0], sw[1], ne[0], ne[1] ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom to track extent
|
||||||
|
*/
|
||||||
|
zoomToExtent() {
|
||||||
|
this.map.getView().fit(this.layerMarkers.getSource().getExtent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom to bounds
|
||||||
|
* @param {number[]} bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
||||||
|
*/
|
||||||
|
zoomToBounds(bounds) {
|
||||||
|
const sw = ol.proj.fromLonLat([ bounds[0], bounds[1] ]);
|
||||||
|
const ne = ol.proj.fromLonLat([ bounds[2], bounds[3] ]);
|
||||||
|
this.map.getView().fit([ sw[0], sw[1], ne[0], ne[1] ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update size
|
||||||
|
*/
|
||||||
|
updateSize() {
|
||||||
|
this.map.updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get extent image
|
||||||
|
* @returns {HTMLImageElement}
|
||||||
|
*/
|
||||||
|
static getExtentImg() {
|
||||||
|
const extentImg = document.createElement('img');
|
||||||
|
extentImg.src = 'images/extent.svg';
|
||||||
|
extentImg.style.width = '60%';
|
||||||
|
return extentImg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLayersApi.ZOOM_MAX = 20;
|
265
js/src/mapviewmodel.js
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $, auth, config } from './initializer.js';
|
||||||
|
import GoogleMapsApi from './mapapi/api_gmaps.js';
|
||||||
|
import OpenLayersApi from './mapapi/api_openlayers.js';
|
||||||
|
import PositionDialogModel from './positiondialogmodel.js';
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import uDialog from './dialog.js';
|
||||||
|
import uObserve from './observe.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MapViewModel.api
|
||||||
|
* @interface
|
||||||
|
* @memberOf MapViewModel
|
||||||
|
* @type {Object}
|
||||||
|
* @property {function(MapViewModel)} init
|
||||||
|
* @property {function} cleanup
|
||||||
|
* @property {function(uTrack, boolean)} displayTrack
|
||||||
|
* @property {function} clearMap
|
||||||
|
* @property {function(number)} animateMarker
|
||||||
|
* @property {function} getBounds
|
||||||
|
* @property {function} zoomToExtent
|
||||||
|
* @property {function} zoomToBounds
|
||||||
|
* @property {function} updateSize
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class MapViewModel
|
||||||
|
*/
|
||||||
|
export default class MapViewModel extends ViewModel {
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
*/
|
||||||
|
constructor(state) {
|
||||||
|
super({
|
||||||
|
/** @type {?number} */
|
||||||
|
markerOver: null,
|
||||||
|
/** @type {?number} */
|
||||||
|
markerSelect: null,
|
||||||
|
// click handler
|
||||||
|
onMenuToggle: null
|
||||||
|
});
|
||||||
|
this.model.onMenuToggle = () => this.onMapResize();
|
||||||
|
this.state = state;
|
||||||
|
/** @type HTMLElement */
|
||||||
|
this.mapElement = document.querySelector('#map-canvas');
|
||||||
|
this.savedBounds = null;
|
||||||
|
this.api = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {MapViewModel}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.bindAll();
|
||||||
|
this.setObservers();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic change of map api
|
||||||
|
* @param {string} apiName API name
|
||||||
|
*/
|
||||||
|
loadMapAPI(apiName) {
|
||||||
|
if (this.api) {
|
||||||
|
try {
|
||||||
|
this.savedBounds = this.api.getBounds();
|
||||||
|
} catch (e) {
|
||||||
|
this.savedBounds = null;
|
||||||
|
}
|
||||||
|
this.api.cleanup();
|
||||||
|
}
|
||||||
|
this.api = this.getApi(apiName);
|
||||||
|
this.api.init()
|
||||||
|
.then(() => this.onReady())
|
||||||
|
.catch((e) => {
|
||||||
|
let txt = $._('apifailure', apiName);
|
||||||
|
if (e && e.message) {
|
||||||
|
txt += ` (${e.message})`;
|
||||||
|
}
|
||||||
|
uUtils.error(e, txt);
|
||||||
|
config.mapApi = (apiName === 'gmaps') ? 'openlayers' : 'gmaps';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} apiName
|
||||||
|
* @return {OpenLayersApi|GoogleMapsApi}
|
||||||
|
*/
|
||||||
|
getApi(apiName) {
|
||||||
|
return apiName === 'gmaps' ? new GoogleMapsApi(this) : new OpenLayersApi(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onReady() {
|
||||||
|
if (this.savedBounds) {
|
||||||
|
this.api.zoomToBounds(this.savedBounds);
|
||||||
|
}
|
||||||
|
if (this.state.currentTrack) {
|
||||||
|
this.api.displayTrack(this.state.currentTrack, this.savedBounds === null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setObservers() {
|
||||||
|
config.onChanged('mapApi', (mapApi) => {
|
||||||
|
this.loadMapAPI(mapApi);
|
||||||
|
});
|
||||||
|
this.state.onChanged('currentTrack', (track) => {
|
||||||
|
if (!this.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.api.clearMap();
|
||||||
|
if (track) {
|
||||||
|
uObserve.observe(track, 'positions', () => {
|
||||||
|
this.api.displayTrack(track, false);
|
||||||
|
this.api.zoomToExtent();
|
||||||
|
});
|
||||||
|
this.api.displayTrack(track, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get popup html
|
||||||
|
* @param {number} id Position index
|
||||||
|
* @returns {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
getPopupElement(id) {
|
||||||
|
const pos = this.state.currentTrack.positions[id];
|
||||||
|
const count = this.state.currentTrack.length;
|
||||||
|
const user = this.state.currentTrack.user;
|
||||||
|
const isEditable = auth.user && (auth.isAdmin || auth.user === user);
|
||||||
|
let date = '–––';
|
||||||
|
let time = '–––';
|
||||||
|
if (pos.timestamp > 0) {
|
||||||
|
const dateTime = uUtils.getTimeString(new Date(pos.timestamp * 1000));
|
||||||
|
date = dateTime.date;
|
||||||
|
time = `${dateTime.time}<span class="smaller">${dateTime.zone}</span>`;
|
||||||
|
}
|
||||||
|
let provider = '';
|
||||||
|
if (pos.provider === 'gps') {
|
||||||
|
provider = ` <img class="icon" alt="${$._('gps')}" title="${$._('gps')}" src="images/gps_dark.svg">`;
|
||||||
|
} else if (pos.provider === 'network') {
|
||||||
|
provider = ` <img class="icon" alt="${$._('network')}" title="${$._('network')}" src="images/network_dark.svg">`;
|
||||||
|
}
|
||||||
|
let editLink = '';
|
||||||
|
if (isEditable) {
|
||||||
|
editLink = `<a id="editposition" class="menu-link" data-bind="onUserAdd">${$._('editposition')}</a>`;
|
||||||
|
}
|
||||||
|
let stats = '';
|
||||||
|
if (!this.state.showLatest) {
|
||||||
|
stats =
|
||||||
|
`<div id="pright">
|
||||||
|
<img class="icon" alt="${$._('track')}" src="images/stats_blue.svg" style="margin-left: 3em;"><br>
|
||||||
|
<img class="icon" alt="${$._('ttime')}" title="${$._('ttime')}" src="images/time_blue.svg"> ${$.getLocaleDuration(pos.totalSeconds)}<br>
|
||||||
|
<img class="icon" alt="${$._('aspeed')}" title="${$._('aspeed')}" src="images/speed_blue.svg"> ${$.getLocaleSpeed(pos.totalSpeed, true)}<br>
|
||||||
|
<img class="icon" alt="${$._('tdistance')}" title="${$._('tdistance')}" src="images/distance_blue.svg"> ${$.getLocaleDistanceMajor(pos.totalMeters, true)}<br>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const html =
|
||||||
|
`<div id="pheader">
|
||||||
|
<div><img alt="${$._('user')}" title="${$._('user')}" src="images/user_dark.svg"> ${uUtils.htmlEncode(pos.username)}</div>
|
||||||
|
<div><img alt="${$._('track')}" title="${$._('track')}" src="images/route_dark.svg"> ${uUtils.htmlEncode(pos.trackname)}</div>
|
||||||
|
</div>
|
||||||
|
<div id="pbody">
|
||||||
|
${(pos.hasComment()) ? `<div id="pcomments">${uUtils.htmlEncode(pos.comment).replace(/\n/, '<br>')}</div>` : ''}
|
||||||
|
${(pos.hasImage()) ? `<div id="pimage"><img src="uploads/${pos.image}" alt="image"></div>` : ''}
|
||||||
|
<div id="pleft">
|
||||||
|
<img class="icon" alt="${$._('time')}" title="${$._('time')}" src="images/calendar_dark.svg"> ${date}<br>
|
||||||
|
<img class="icon" alt="${$._('time')}" title="${$._('time')}" src="images/clock_dark.svg"> ${time}<br>
|
||||||
|
${(pos.speed !== null) ? `<img class="icon" alt="${$._('speed')}" title="${$._('speed')}" src="images/speed_dark.svg">${$.getLocaleSpeed(pos.speed, true)}<br>` : ''}
|
||||||
|
${(pos.altitude !== null) ? `<img class="icon" alt="${$._('altitude')}" title="${$._('altitude')}" src="images/altitude_dark.svg">${$.getLocaleAltitude(pos.altitude, true)}<br>` : ''}
|
||||||
|
${(pos.accuracy !== null) ? `<img class="icon" alt="${$._('accuracy')}" title="${$._('accuracy')}" src="images/accuracy_dark.svg">${$.getLocaleAccuracy(pos.accuracy, true)}${provider}<br>` : ''}
|
||||||
|
${(pos.bearing !== null) ? `<img class="icon" alt="${$._('bearing')}" title="${$._('bearing')}" src="images/bearing.svg" style="transform: rotate(${pos.bearing}deg) scale(1.2);">${pos.bearing}°<br>` : ''}
|
||||||
|
<img class="icon" alt="${$._('position')}" title="${$._('position')}" src="images/position.svg">${$.getLocaleCoordinates(pos)}<br>
|
||||||
|
</div>${stats}</div>
|
||||||
|
<div id="pfooter"><div>${$._('pointof', id + 1, count)}</div><div>${editLink}</div></div>`;
|
||||||
|
const node = document.createElement('div');
|
||||||
|
node.setAttribute('id', 'popup');
|
||||||
|
node.innerHTML = html;
|
||||||
|
if (pos.hasImage()) {
|
||||||
|
const image = node.querySelector('#pimage img');
|
||||||
|
image.onclick = () => {
|
||||||
|
const modal = new uDialog(`<img src="uploads/${pos.image}" alt="image">`);
|
||||||
|
const closeEl = modal.element.querySelector('#modal-close');
|
||||||
|
closeEl.onclick = () => modal.destroy();
|
||||||
|
modal.element.classList.add('image');
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isEditable) {
|
||||||
|
const edit = node.querySelector('#editposition');
|
||||||
|
edit.onclick = () => {
|
||||||
|
const vm = new PositionDialogModel(this.state, id);
|
||||||
|
vm.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SVG marker path
|
||||||
|
* @param {boolean} isLarge Large marker with hole if true
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
static getMarkerPath(isLarge) {
|
||||||
|
const markerHole = 'M15,34.911c0,0,0.359-3.922,1.807-8.588c0.414-1.337,1.011-2.587,2.495-4.159' +
|
||||||
|
'c1.152-1.223,3.073-2.393,3.909-4.447c1.681-6.306-3.676-9.258-8.211-9.258c-4.536,0-9.893,2.952-8.211,9.258' +
|
||||||
|
'c0.836,2.055,2.756,3.225,3.91,4.447c1.484,1.572,2.08,2.822,2.495,4.159C14.64,30.989,15,34.911,15,34.911z M18,15.922' +
|
||||||
|
'c0,1.705-1.342,3.087-2.999,3.087c-1.657,0-3-1.382-3-3.087c0-1.704,1.343-3.086,3-3.086C16.658,12.836,18,14.218,18,15.922z';
|
||||||
|
const marker = 'M14.999,34.911c0,0,0.232-1.275,1.162-4.848c0.268-1.023,0.652-1.98,1.605-3.184' +
|
||||||
|
'c0.742-0.937,1.975-1.832,2.514-3.404c1.082-4.828-2.363-7.088-5.281-7.088c-2.915,0-6.361,2.26-5.278,7.088' +
|
||||||
|
'c0.538,1.572,1.771,2.468,2.514,3.404c0.953,1.203,1.337,2.16,1.604,3.184C14.77,33.635,14.999,34.911,14.999,34.911z';
|
||||||
|
return isLarge ? markerHole : marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get marker extra mark
|
||||||
|
* @param {boolean} isLarge
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
static getMarkerExtra(isLarge) {
|
||||||
|
const offset1 = isLarge ? 'M26.074,13.517' : 'M23.328,20.715';
|
||||||
|
const offset2 = isLarge ? 'M28.232,10.942' : 'M25.486,18.141';
|
||||||
|
return `<path fill="none" stroke="red" stroke-width="2" d="${offset1}c0-3.961-3.243-7.167-7.251-7.167"/>
|
||||||
|
<path fill="none" stroke="red" stroke-width="2" d="${offset2}c-0.5-4.028-3.642-7.083-7.724-7.542"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get inline SVG source
|
||||||
|
* @param {string} fill
|
||||||
|
* @param {boolean=} isLarge
|
||||||
|
* @param {boolean=} isExtra
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
static getSvgSrc(fill, isLarge, isExtra) {
|
||||||
|
const svg = `<svg viewBox="0 0 30 35" width="30px" height="35px" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g><path stroke="black" fill="${fill}" d="${MapViewModel.getMarkerPath(isLarge)}"/>${isExtra ? MapViewModel.getMarkerExtra(isLarge) : ''}</g></svg>`;
|
||||||
|
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMapResize() {
|
||||||
|
if (this.api) {
|
||||||
|
this.api.updateSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
363
js/src/observe.js
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
export default class uObserve {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe object's property or all properties if not specified.
|
||||||
|
* On change call observer function.
|
||||||
|
* observe(obj, prop, observer) observes given property prop;
|
||||||
|
* observe(obj, observer) observes all properties of object obj.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {(string|ObserveCallback)} p1
|
||||||
|
* @param {ObserveCallback=} p2
|
||||||
|
*/
|
||||||
|
static observe(obj, p1, p2) {
|
||||||
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
|
throw new Error('Invalid argument: invalid object');
|
||||||
|
}
|
||||||
|
if (typeof p2 === 'function') {
|
||||||
|
this.observeProperty(obj, p1, p2);
|
||||||
|
} else if (typeof p1 === 'function') {
|
||||||
|
this.observeRecursive(obj, p1);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid argument for observe');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify callback
|
||||||
|
* @callback ObserveCallback
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify observers
|
||||||
|
* @param {Set<ObserveCallback>} observers
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
static notify(observers, value) {
|
||||||
|
for (const observer of observers) {
|
||||||
|
(async () => {
|
||||||
|
await observer(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger notify of property observers
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {string} property
|
||||||
|
*/
|
||||||
|
static forceUpdate(obj, property) {
|
||||||
|
const value = obj._values[property];
|
||||||
|
const observers = obj._observers[property];
|
||||||
|
this.notify(observers, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if object property is observed;
|
||||||
|
* Optionally check if it is observed by given observer
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {string} property
|
||||||
|
* @param {Function=} observer
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
static isObserved(obj, property, observer) {
|
||||||
|
if (typeof obj !== 'object' || obj === null || !obj.hasOwnProperty(property)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isObserved = !!(obj._observers && obj._observers[property] && obj._observers[property].size > 0);
|
||||||
|
if (isObserved && observer) {
|
||||||
|
return obj._observers[property].has(observer);
|
||||||
|
}
|
||||||
|
return isObserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set observed property value without notifying observers
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {string} property
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
static setSilently(obj, property, value) {
|
||||||
|
if (!obj.hasOwnProperty(property)) {
|
||||||
|
throw new Error(`Invalid argument: object does not have property "${property}"`);
|
||||||
|
}
|
||||||
|
if (this.isObserved(obj, property)) {
|
||||||
|
obj._values[property] = value;
|
||||||
|
if (Array.isArray(obj[property])) {
|
||||||
|
for (const obs of obj._observers[property]) {
|
||||||
|
this.observeArray(obj[property], obs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj[property] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe object's property. On change call observer
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {string} property
|
||||||
|
* @param {ObserveCallback} observer
|
||||||
|
*/
|
||||||
|
static observeProperty(obj, property, observer) {
|
||||||
|
if (!obj.hasOwnProperty(property)) {
|
||||||
|
throw new Error(`Invalid argument: object does not have property "${property}"`);
|
||||||
|
}
|
||||||
|
if (this.isObserved(obj, property, observer)) {
|
||||||
|
throw new Error(`Observer already registered for property ${property}`);
|
||||||
|
}
|
||||||
|
this.addObserver(obj, observer, property);
|
||||||
|
if (!obj.hasOwnProperty('_values')) {
|
||||||
|
Object.defineProperty(obj, '_values', { enumerable: false, configurable: false, value: {} });
|
||||||
|
}
|
||||||
|
obj._values[property] = obj[property];
|
||||||
|
Object.defineProperty(obj, property, {
|
||||||
|
get: () => obj._values[property],
|
||||||
|
set: (newValue) => {
|
||||||
|
if (obj._values[property] !== newValue) {
|
||||||
|
obj._values[property] = newValue;
|
||||||
|
console.log(`${property} = ` + (Array.isArray(newValue) && newValue.length ? `[${newValue[0]}, …](${newValue.length})` : newValue));
|
||||||
|
uObserve.notify(obj._observers[property], newValue);
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj[property])) {
|
||||||
|
this.observeArray(obj[property], obj._observers[property]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (Array.isArray(obj[property])) {
|
||||||
|
this.observeArray(obj[property], observer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively add observer to all properties
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {ObserveCallback} observer
|
||||||
|
*/
|
||||||
|
static observeRecursive(obj, observer) {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
this.observeArray(obj, observer);
|
||||||
|
} else {
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (obj.hasOwnProperty(prop)) {
|
||||||
|
uObserve.observeProperty(obj, prop, observer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe array
|
||||||
|
* @param {Object} arr
|
||||||
|
* @param {(ObserveCallback|Set<ObserveCallback>)} observer
|
||||||
|
*/
|
||||||
|
static observeArray(arr, observer) {
|
||||||
|
if (observer instanceof Set) {
|
||||||
|
for (const obs of observer) {
|
||||||
|
this.addObserver(arr, obs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addObserver(arr, observer);
|
||||||
|
}
|
||||||
|
this.overrideArrayPrototypes(arr, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store observer in object
|
||||||
|
* @param {Object} obj Object
|
||||||
|
* @param {ObserveCallback} observer Observer
|
||||||
|
* @param {string=} property Optional property
|
||||||
|
*/
|
||||||
|
static addObserver(obj, observer, property) {
|
||||||
|
if (!obj.hasOwnProperty('_observers')) {
|
||||||
|
Object.defineProperty(obj, '_observers', {
|
||||||
|
enumerable: false,
|
||||||
|
configurable: false,
|
||||||
|
value: (arguments.length === 3) ? [] : new Set()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (arguments.length === 3) {
|
||||||
|
if (!obj._observers[property]) {
|
||||||
|
obj._observers[property] = new Set();
|
||||||
|
}
|
||||||
|
obj._observers[property].add(observer);
|
||||||
|
} else {
|
||||||
|
obj._observers.add(observer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove observer from object's property or all it's properties
|
||||||
|
* unobserve(obj, prop, observer) unobserves given property prop;
|
||||||
|
* unobserve(obj, observer) unobserves all properties of object obj.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {(string|ObserveCallback)} p1
|
||||||
|
* @param {ObserveCallback=} p2
|
||||||
|
*/
|
||||||
|
static unobserve(obj, p1, p2) {
|
||||||
|
if (typeof p2 === 'function') {
|
||||||
|
this.unobserveProperty(obj, p1, p2);
|
||||||
|
} else if (typeof p1 === 'function') {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
this.unobserveArray(obj, p1);
|
||||||
|
} else {
|
||||||
|
this.unobserveRecursive(obj, p1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid argument for unobserve');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all observers from object's property or all it's properties
|
||||||
|
* unobserveAll(obj, prop) removes all observes from given property prop;
|
||||||
|
* unobserveAll(obj) removes all observers from all properties of object obj.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {string=} property
|
||||||
|
*/
|
||||||
|
static unobserveAll(obj, property) {
|
||||||
|
if (arguments.length === 1) {
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (obj.hasOwnProperty(prop)) {
|
||||||
|
this.unobserveAll(obj, prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.isObserved(obj, property)) {
|
||||||
|
console.log(`Removing all observers for ${property}…`);
|
||||||
|
if (Array.isArray(obj[property])) {
|
||||||
|
this.restoreArrayPrototypes(obj[property]);
|
||||||
|
} else if (typeof obj[property] === 'object' && obj[property] !== null) {
|
||||||
|
for (const prop in obj[property]) {
|
||||||
|
if (obj[property].hasOwnProperty(prop)) {
|
||||||
|
this.unobserveAll(obj[property], prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete obj._observers[property];
|
||||||
|
delete obj[property];
|
||||||
|
obj[property] = obj._values[property];
|
||||||
|
delete obj._values[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove observer from object's property
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {?string} property
|
||||||
|
* @param {ObserveCallback} observer
|
||||||
|
*/
|
||||||
|
static unobserveProperty(obj, property, observer) {
|
||||||
|
if (Array.isArray(obj[property])) {
|
||||||
|
this.unobserveArray(obj[property], observer);
|
||||||
|
}
|
||||||
|
this.removeObserver(obj, observer, property);
|
||||||
|
if (!obj._observers[property].size) {
|
||||||
|
delete obj[property];
|
||||||
|
obj[property] = obj._values[property];
|
||||||
|
delete obj._values[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively remove observers from all properties
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {ObserveCallback} observer
|
||||||
|
*/
|
||||||
|
static unobserveRecursive(obj, observer) {
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (obj.hasOwnProperty(prop)) {
|
||||||
|
uObserve.unobserveProperty(obj, prop, observer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove observer from array
|
||||||
|
* @param {Object} arr
|
||||||
|
* @param {ObserveCallback} observer
|
||||||
|
*/
|
||||||
|
static unobserveArray(arr, observer) {
|
||||||
|
this.removeObserver(arr, observer);
|
||||||
|
if (!arr._observers.size) {
|
||||||
|
this.restoreArrayPrototypes(arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} arr
|
||||||
|
*/
|
||||||
|
static overrideArrayPrototypes(arr) {
|
||||||
|
[ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ].forEach(
|
||||||
|
(operation) => {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, operation);
|
||||||
|
if (!arr.hasOwnProperty(operation)) {
|
||||||
|
descriptor.value = function () {
|
||||||
|
const result = Array.prototype[operation].apply(arr, arguments);
|
||||||
|
console.log(`[${operation}] ` + (arr.length ? `[${arr[0]}, …](${arr.length})` : arr));
|
||||||
|
uObserve.notify(arr._observers, arr);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(arr, operation, descriptor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} arr
|
||||||
|
*/
|
||||||
|
static restoreArrayPrototypes(arr) {
|
||||||
|
[ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ].forEach(
|
||||||
|
(operation) => {
|
||||||
|
delete arr[operation];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove observer from object's property
|
||||||
|
* @param {Object} obj Object
|
||||||
|
* @param {string} property Optional property
|
||||||
|
* @param {ObserveCallback} observer Observer
|
||||||
|
*/
|
||||||
|
static removeObserver(obj, observer, property) {
|
||||||
|
if (!obj.hasOwnProperty('_observers')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let observers;
|
||||||
|
if (arguments.length === 3) {
|
||||||
|
if (!obj._observers[property]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
observers = obj._observers[property];
|
||||||
|
console.log(`Removing observer for ${property}…`);
|
||||||
|
} else {
|
||||||
|
observers = obj._observers;
|
||||||
|
console.log('Removing observer for object…');
|
||||||
|
}
|
||||||
|
observers.forEach((obs) => {
|
||||||
|
if (obs === observer) {
|
||||||
|
console.log('Removed');
|
||||||
|
observers.delete(obs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
151
js/src/position.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uAjax from './ajax.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class uPosition
|
||||||
|
* @property {number} id
|
||||||
|
* @property {number} latitude
|
||||||
|
* @property {number} longitude
|
||||||
|
* @property {?number} altitude
|
||||||
|
* @property {?number} speed
|
||||||
|
* @property {?number} bearing
|
||||||
|
* @property {?number} accuracy
|
||||||
|
* @property {?string} provider
|
||||||
|
* @property {?string} comment
|
||||||
|
* @property {?string} image
|
||||||
|
* @property {string} username
|
||||||
|
* @property {string} trackname
|
||||||
|
* @property {number} trackid
|
||||||
|
* @property {number} timestamp
|
||||||
|
* @property {number} meters Distance to previous position
|
||||||
|
* @property {number} seconds Time difference to previous position
|
||||||
|
* @property {number} totalMeters Distance to first position
|
||||||
|
* @property {number} totalSeconds Time difference to first position
|
||||||
|
*/
|
||||||
|
export default class uPosition {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws On invalid input
|
||||||
|
* @param {Object} pos
|
||||||
|
* @returns {uPosition}
|
||||||
|
*/
|
||||||
|
static fromJson(pos) {
|
||||||
|
const position = new uPosition();
|
||||||
|
position.id = uUtils.getInteger(pos.id);
|
||||||
|
position.latitude = uUtils.getFloat(pos.latitude);
|
||||||
|
position.longitude = uUtils.getFloat(pos.longitude);
|
||||||
|
position.altitude = uUtils.getInteger(pos.altitude, true); // may be null
|
||||||
|
position.speed = uUtils.getFloat(pos.speed, true); // may be null
|
||||||
|
position.bearing = uUtils.getInteger(pos.bearing, true); // may be null
|
||||||
|
position.accuracy = uUtils.getInteger(pos.accuracy, true); // may be null
|
||||||
|
position.provider = uUtils.getString(pos.provider, true); // may be null
|
||||||
|
position.comment = uUtils.getString(pos.comment, true); // may be null
|
||||||
|
position.image = uUtils.getString(pos.image, true); // may be null
|
||||||
|
position.username = uUtils.getString(pos.username);
|
||||||
|
position.trackname = uUtils.getString(pos.trackname);
|
||||||
|
position.trackid = uUtils.getInteger(pos.trackid);
|
||||||
|
position.timestamp = uUtils.getInteger(pos.timestamp);
|
||||||
|
position.meters = uUtils.getInteger(pos.meters);
|
||||||
|
position.seconds = uUtils.getInteger(pos.seconds);
|
||||||
|
position.totalMeters = 0;
|
||||||
|
position.totalSeconds = 0;
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
hasComment() {
|
||||||
|
return (this.comment != null && this.comment.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
hasImage() {
|
||||||
|
return (this.image != null && this.image.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get calculatedSpeed() {
|
||||||
|
return this.seconds ? this.meters / this.seconds : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalSpeed() {
|
||||||
|
return this.totalSeconds ? this.totalMeters / this.totalSeconds : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
delete() {
|
||||||
|
return uPosition.update({
|
||||||
|
action: 'delete',
|
||||||
|
posid: this.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
return uPosition.update({
|
||||||
|
action: 'update',
|
||||||
|
posid: this.id,
|
||||||
|
comment: this.comment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save track data
|
||||||
|
* @param {Object} data
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
static update(data) {
|
||||||
|
return uAjax.post('utils/handleposition.php', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate distance to target point using haversine formula
|
||||||
|
* @param {uPosition} target
|
||||||
|
* @return {number} Distance in meters
|
||||||
|
*/
|
||||||
|
distanceTo(target) {
|
||||||
|
const lat1 = uUtils.deg2rad(this.latitude);
|
||||||
|
const lon1 = uUtils.deg2rad(this.longitude);
|
||||||
|
const lat2 = uUtils.deg2rad(target.latitude);
|
||||||
|
const lon2 = uUtils.deg2rad(target.longitude);
|
||||||
|
const latD = lat2 - lat1;
|
||||||
|
const lonD = lon2 - lon1;
|
||||||
|
const bearing = 2 * Math.asin(Math.sqrt((Math.sin(latD / 2) ** 2) + Math.cos(lat1) * Math.cos(lat2) * (Math.sin(lonD / 2) ** 2)));
|
||||||
|
return bearing * 6371000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate time elapsed since target point
|
||||||
|
* @param {uPosition} target
|
||||||
|
* @return {number} Number of seconds
|
||||||
|
*/
|
||||||
|
secondsTo(target) {
|
||||||
|
return this.timestamp - target.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
111
js/src/positiondialogmodel.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2020 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $ } from './initializer.js';
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import uDialog from './dialog.js';
|
||||||
|
import uObserve from './observe.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class PositionDialogModel
|
||||||
|
*/
|
||||||
|
export default class PositionDialogModel extends ViewModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
* @param {number} positionIndex
|
||||||
|
*/
|
||||||
|
constructor(state, positionIndex) {
|
||||||
|
super({
|
||||||
|
onPositionDelete: null,
|
||||||
|
onPositionUpdate: null,
|
||||||
|
onCancel: null,
|
||||||
|
comment: ''
|
||||||
|
});
|
||||||
|
this.state = state;
|
||||||
|
this.positionIndex = positionIndex;
|
||||||
|
this.position = this.state.currentTrack.positions[positionIndex];
|
||||||
|
this.model.onPositionDelete = () => this.onPositionDelete();
|
||||||
|
this.model.onPositionUpdate = () => this.onPositionUpdate();
|
||||||
|
this.model.onCancel = () => this.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const html = this.getHtml();
|
||||||
|
this.dialog = new uDialog(html);
|
||||||
|
this.dialog.show();
|
||||||
|
this.bindAll(this.dialog.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getHtml() {
|
||||||
|
return `<div class="red-button button-resolve"><b><a data-bind="onPositionDelete">${$._('delposition')}</a></b></div>
|
||||||
|
<div>${$._('editingposition', this.positionIndex + 1, `<b>${uUtils.htmlEncode(this.position.trackname)}</b>`)}</div>
|
||||||
|
<div style="clear: both; padding-bottom: 1em;"></div>
|
||||||
|
<form id="positionForm">
|
||||||
|
<label><b>${$._('comment')}</b></label><br>
|
||||||
|
<textarea style="width:100%;" maxlength="255" rows="5" placeholder="${$._('comment')}" name="comment" data-bind="comment">${this.position.hasComment() ? uUtils.htmlEncode(this.position.comment) : ''}</textarea>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button-reject" data-bind="onCancel" type="button">${$._('cancel')}</button>
|
||||||
|
<button class="button-resolve" data-bind="onPositionUpdate" type="submit">${$._('submit')}</button>
|
||||||
|
</div>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionDelete() {
|
||||||
|
if (uDialog.isConfirmed($._('positiondelwarn', this.positionIndex + 1, uUtils.htmlEncode(this.position.trackname)))) {
|
||||||
|
this.position.delete()
|
||||||
|
.then(() => {
|
||||||
|
const track = this.state.currentTrack;
|
||||||
|
this.state.currentTrack = null;
|
||||||
|
track.positions.splice(this.positionIndex, 1);
|
||||||
|
track.recalculatePositions();
|
||||||
|
this.state.currentTrack = track;
|
||||||
|
this.dialog.destroy();
|
||||||
|
}).catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionUpdate() {
|
||||||
|
if (this.validate()) {
|
||||||
|
this.position.comment = this.model.comment;
|
||||||
|
this.position.save()
|
||||||
|
.then(() => {
|
||||||
|
uObserve.forceUpdate(this.state, 'currentTrack');
|
||||||
|
this.dialog.destroy()
|
||||||
|
})
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialog.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate form
|
||||||
|
* @return {boolean} True if valid
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
return this.model.comment !== this.position.comment;
|
||||||
|
}
|
||||||
|
}
|
115
js/src/positionset.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uAjax from './ajax.js';
|
||||||
|
import uListItem from './listitem.js';
|
||||||
|
import uPosition from './position.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of unrelated positions
|
||||||
|
* @class uPositionSet
|
||||||
|
* @property {uPosition[]} positions
|
||||||
|
*/
|
||||||
|
export default class uPositionSet extends uListItem {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.positions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.positions.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get length() {
|
||||||
|
return this.positions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get hasPositions() {
|
||||||
|
return this.positions.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars,class-methods-use-this
|
||||||
|
isLastPosition(id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars,class-methods-use-this
|
||||||
|
isFirstPosition(id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get track data from json
|
||||||
|
* @param {Object[]} posArr Positions data
|
||||||
|
* @param {boolean=} isUpdate If true append to old data
|
||||||
|
*/
|
||||||
|
fromJson(posArr, isUpdate = false) {
|
||||||
|
let positions = [];
|
||||||
|
if (isUpdate) {
|
||||||
|
positions = this.positions;
|
||||||
|
} else {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
for (const pos of posArr) {
|
||||||
|
positions.push(uPosition.fromJson(pos));
|
||||||
|
}
|
||||||
|
// update at the end to avoid observers update invidual points
|
||||||
|
this.positions = positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch latest position of each user.
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
fetchLatest() {
|
||||||
|
this.clear();
|
||||||
|
return uPositionSet.fetch({ last: true }).then((_positions) => {
|
||||||
|
this.fromJson(_positions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch latest position of each user.
|
||||||
|
* @return {Promise<?uPositionSet, Error>}
|
||||||
|
*/
|
||||||
|
static fetchLatest() {
|
||||||
|
const set = new uPositionSet();
|
||||||
|
return set.fetchLatest().then(() => {
|
||||||
|
if (set.length) {
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param params
|
||||||
|
* @return {Promise<Object[], Error>}
|
||||||
|
*/
|
||||||
|
static fetch(params) {
|
||||||
|
return uAjax.get('utils/getpositions.php', params);
|
||||||
|
}
|
||||||
|
}
|
154
js/src/select.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uObserve from './observe.js';
|
||||||
|
|
||||||
|
export default class uSelect {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLSelectElement} element Select element
|
||||||
|
* @param {string=} head Optional header text
|
||||||
|
* @param {string=} all Optional all option text
|
||||||
|
*/
|
||||||
|
constructor(element, head, all) {
|
||||||
|
if (!(element instanceof HTMLSelectElement)) {
|
||||||
|
throw new Error('Invalid argument for select');
|
||||||
|
}
|
||||||
|
this.element = element;
|
||||||
|
this.hasAllOption = false;
|
||||||
|
this.allText = '';
|
||||||
|
if (all && all.length) {
|
||||||
|
this.allText = all;
|
||||||
|
}
|
||||||
|
if (head && head.length) {
|
||||||
|
this.head = head;
|
||||||
|
} else {
|
||||||
|
this.hasHead = false;
|
||||||
|
this.headText = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
set selected(value) {
|
||||||
|
if (this.hasValue(value)) {
|
||||||
|
this.element.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get selected() {
|
||||||
|
return this.element.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
set head(text) {
|
||||||
|
if (text.length) {
|
||||||
|
this.hasHead = true;
|
||||||
|
this.headText = text;
|
||||||
|
this.addHead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string=} text Optional text
|
||||||
|
*/
|
||||||
|
showAllOption(text) {
|
||||||
|
if (text) {
|
||||||
|
this.allText = text;
|
||||||
|
}
|
||||||
|
this.hasAllOption = true;
|
||||||
|
const index = this.hasHead ? 1 : 0;
|
||||||
|
this.element.add(new Option(this.allText, uSelect.allValue), index);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllOption() {
|
||||||
|
const isSelectedAll = this.selected === uSelect.allValue;
|
||||||
|
this.hasAllOption = false;
|
||||||
|
this.remove(uSelect.allValue);
|
||||||
|
if (isSelectedAll) {
|
||||||
|
this.selected = this.hasHead ? uSelect.headValue : '';
|
||||||
|
this.element.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addHead() {
|
||||||
|
const head = new Option(this.headText, uSelect.headValue, true, true);
|
||||||
|
head.disabled = true;
|
||||||
|
this.element.options.add(head, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
hasValue(value) {
|
||||||
|
return (typeof this.getOption(value) !== 'undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
getOption(value) {
|
||||||
|
return [ ...this.element.options ].find((o) => o.value === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
remove(value) {
|
||||||
|
/** @type HTMLOptionElement */
|
||||||
|
const option = this.getOption(value);
|
||||||
|
if (option) {
|
||||||
|
this.element.remove(option.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uListItem[]} options
|
||||||
|
* @param {string=} selected
|
||||||
|
*/
|
||||||
|
setOptions(options, selected) {
|
||||||
|
selected = selected || this.element.value;
|
||||||
|
this.element.options.length = 0;
|
||||||
|
if (this.hasHead) {
|
||||||
|
this.addHead();
|
||||||
|
}
|
||||||
|
if (this.hasAllOption) {
|
||||||
|
this.element.add(new Option(this.allText, uSelect.allValue, false, selected === uSelect.allValue));
|
||||||
|
}
|
||||||
|
for (const option of options) {
|
||||||
|
const optEl = new Option(option.listText, option.listValue, false, selected === option.listValue);
|
||||||
|
this.element.add(optEl);
|
||||||
|
uObserve.observe(option, 'listText', (text) => {
|
||||||
|
optEl.text = text;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get allValue() {
|
||||||
|
return 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get headValue() {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
}
|
45
js/src/state.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uObserve from './observe.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @property {?uTrack} currentTrack
|
||||||
|
* @property {?uUser} currentUser
|
||||||
|
* @property {boolean} showLatest
|
||||||
|
* @property {boolean} showAllUsers
|
||||||
|
*/
|
||||||
|
export default class uState {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.currentTrack = null;
|
||||||
|
this.currentUser = null;
|
||||||
|
this.showLatest = false;
|
||||||
|
this.showAllUsers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} property
|
||||||
|
* @param {ObserveCallback} callback
|
||||||
|
*/
|
||||||
|
onChanged(property, callback) {
|
||||||
|
uObserve.observe(this, property, callback);
|
||||||
|
}
|
||||||
|
}
|
267
js/src/track.js
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uAjax from './ajax.js';
|
||||||
|
import uPosition from './position.js';
|
||||||
|
import uPositionSet from './positionset.js';
|
||||||
|
import uUser from './user.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of positions representing user's track
|
||||||
|
* @class uTrack
|
||||||
|
* @property {number} id
|
||||||
|
* @property {string} name
|
||||||
|
* @property {uUser} user
|
||||||
|
* @property {uPosition[]} positions
|
||||||
|
* @property {PlotData} plotData
|
||||||
|
*/
|
||||||
|
export default class uTrack extends uPositionSet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @param {string} name
|
||||||
|
* @param {uUser} user
|
||||||
|
*/
|
||||||
|
constructor(id, name, user) {
|
||||||
|
super();
|
||||||
|
if (!Number.isSafeInteger(id) || id <= 0 || !name || !(user instanceof uUser)) {
|
||||||
|
throw new Error('Invalid argument for track constructor');
|
||||||
|
}
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.user = user;
|
||||||
|
this.plotData = [];
|
||||||
|
this.maxId = 0;
|
||||||
|
this.totalMeters = 0;
|
||||||
|
this.totalSeconds = 0;
|
||||||
|
this.listItem(id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name) {
|
||||||
|
this.name = name;
|
||||||
|
this.listText = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
super.clear();
|
||||||
|
this.clearTrackCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTrackCounters() {
|
||||||
|
this.maxId = 0;
|
||||||
|
this.plotData.length = 0;
|
||||||
|
this.totalMeters = 0;
|
||||||
|
this.totalSeconds = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uTrack} track
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isEqualTo(track) {
|
||||||
|
return !!track && track.id === this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get hasPlotData() {
|
||||||
|
return this.plotData.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get track data from json
|
||||||
|
* @param {Object[]} posArr Positions data
|
||||||
|
* @param {boolean=} isUpdate If true append to old data
|
||||||
|
*/
|
||||||
|
fromJson(posArr, isUpdate = false) {
|
||||||
|
let positions = [];
|
||||||
|
if (isUpdate && this.hasPositions) {
|
||||||
|
positions = this.positions;
|
||||||
|
} else {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
for (const pos of posArr) {
|
||||||
|
const position = uPosition.fromJson(pos);
|
||||||
|
this.calculatePosition(position);
|
||||||
|
positions.push(position);
|
||||||
|
}
|
||||||
|
// update at the end to avoid observers update invidual points
|
||||||
|
this.positions = positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isLastPosition(id) {
|
||||||
|
return this.length > 0 && id === this.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isFirstPosition(id) {
|
||||||
|
return this.length > 0 && id === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch track positions
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
fetchPositions() {
|
||||||
|
const params = {
|
||||||
|
userid: this.user.id,
|
||||||
|
trackid: this.id
|
||||||
|
};
|
||||||
|
if (this.maxId) {
|
||||||
|
params.afterid = this.maxId;
|
||||||
|
}
|
||||||
|
return uPositionSet.fetch(params).then((_positions) => {
|
||||||
|
this.fromJson(_positions, params.afterid > 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch track with latest position of a user.
|
||||||
|
* @param {uUser} user
|
||||||
|
* @return {Promise<?uTrack, Error>}
|
||||||
|
*/
|
||||||
|
static fetchLatest(user) {
|
||||||
|
return this.fetch({
|
||||||
|
last: true,
|
||||||
|
userid: user.id
|
||||||
|
}).then((_positions) => {
|
||||||
|
if (_positions.length) {
|
||||||
|
const track = new uTrack(_positions[0].trackid, _positions[0].trackname, user);
|
||||||
|
track.fromJson(_positions);
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch tracks for given user
|
||||||
|
* @throws
|
||||||
|
* @param {uUser} user
|
||||||
|
* @return {Promise<uTrack[], Error>}
|
||||||
|
*/
|
||||||
|
static fetchList(user) {
|
||||||
|
return uAjax.get('utils/gettracks.php', { userid: user.id }).then(
|
||||||
|
/**
|
||||||
|
* @param {Array.<{id: number, name: string}>} _tracks
|
||||||
|
* @return {uTrack[]}
|
||||||
|
*/
|
||||||
|
(_tracks) => {
|
||||||
|
const tracks = [];
|
||||||
|
for (const track of _tracks) {
|
||||||
|
tracks.push(new uTrack(track.id, track.name, user));
|
||||||
|
}
|
||||||
|
return tracks;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export to file
|
||||||
|
* @param {string} type File type
|
||||||
|
*/
|
||||||
|
export(type) {
|
||||||
|
if (this.hasPositions) {
|
||||||
|
const url = `utils/export.php?type=${type}&userid=${this.user.id}&trackid=${this.id}`;
|
||||||
|
uUtils.openUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports tracks submited with HTML form and returns last imported track id
|
||||||
|
* @param {HTMLFormElement} form
|
||||||
|
* @param {uUser} user
|
||||||
|
* @return {Promise<uTrack[], Error>}
|
||||||
|
*/
|
||||||
|
static import(form, user) {
|
||||||
|
return uAjax.post('utils/import.php', form)
|
||||||
|
.then(
|
||||||
|
/**
|
||||||
|
* @param {Array.<{id: number, name: string}>} _tracks
|
||||||
|
* @return {uTrack[]}
|
||||||
|
*/
|
||||||
|
(_tracks) => {
|
||||||
|
const tracks = [];
|
||||||
|
for (const track of _tracks) {
|
||||||
|
tracks.push(new uTrack(track.id, track.name, user));
|
||||||
|
}
|
||||||
|
return tracks;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
return uTrack.update({
|
||||||
|
action: 'delete',
|
||||||
|
trackid: this.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMeta() {
|
||||||
|
return uTrack.update({
|
||||||
|
action: 'update',
|
||||||
|
trackid: this.id,
|
||||||
|
trackname: this.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save track data
|
||||||
|
* @param {Object} data
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
static update(data) {
|
||||||
|
return uAjax.post('utils/handletrack.php', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
recalculatePositions() {
|
||||||
|
this.clearTrackCounters();
|
||||||
|
let previous = null;
|
||||||
|
for (const position of this.positions) {
|
||||||
|
position.meters = previous ? position.distanceTo(previous) : 0;
|
||||||
|
position.seconds = previous ? position.secondsTo(previous) : 0;
|
||||||
|
this.calculatePosition(position);
|
||||||
|
previous = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate position total counters and plot data
|
||||||
|
* @param {uPosition} position
|
||||||
|
*/
|
||||||
|
calculatePosition(position) {
|
||||||
|
this.totalMeters += position.meters;
|
||||||
|
this.totalSeconds += position.seconds;
|
||||||
|
position.totalMeters = this.totalMeters;
|
||||||
|
position.totalSeconds = this.totalSeconds;
|
||||||
|
if (position.altitude != null) {
|
||||||
|
this.plotData.push({ x: position.totalMeters, y: position.altitude });
|
||||||
|
}
|
||||||
|
if (position.id > this.maxId) {
|
||||||
|
this.maxId = position.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
js/src/trackdialogmodel.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $ } from '../src/initializer.js';
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import uDialog from './dialog.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
export default class TrackDialogModel extends ViewModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {TrackViewModel} viewModel
|
||||||
|
*/
|
||||||
|
constructor(viewModel) {
|
||||||
|
super({
|
||||||
|
onTrackDelete: null,
|
||||||
|
onTrackUpdate: null,
|
||||||
|
onCancel: null,
|
||||||
|
trackname: ''
|
||||||
|
});
|
||||||
|
this.track = viewModel.state.currentTrack;
|
||||||
|
this.trackVM = viewModel;
|
||||||
|
this.model.onTrackDelete = () => this.onTrackDelete();
|
||||||
|
this.model.onTrackUpdate = () => this.onTrackUpdate();
|
||||||
|
this.model.onCancel = () => this.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const html = this.getHtml();
|
||||||
|
this.dialog = new uDialog(html);
|
||||||
|
this.dialog.show();
|
||||||
|
this.bindAll(this.dialog.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getHtml() {
|
||||||
|
return `<div class="red-button button-resolve"><b><a data-bind="onTrackDelete">${$._('deltrack')}</a></b></div>
|
||||||
|
<div>${$._('editingtrack', `<b>${uUtils.htmlEncode(this.track.name)}</b>`)}</div>
|
||||||
|
<div style="clear: both; padding-bottom: 1em;"></div>
|
||||||
|
<form id="trackForm">
|
||||||
|
<label><b>${$._('trackname')}</b></label>
|
||||||
|
<input type="text" placeholder="${$._('trackname')}" name="trackname" data-bind="trackname" value="${uUtils.htmlEncode(this.track.name)}" required>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button-reject" data-bind="onCancel" type="button">${$._('cancel')}</button>
|
||||||
|
<button class="button-resolve" data-bind="onTrackUpdate" type="submit">${$._('submit')}</button>
|
||||||
|
</div>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTrackDelete() {
|
||||||
|
if (uDialog.isConfirmed($._('trackdelwarn', uUtils.htmlEncode(this.track.name)))) {
|
||||||
|
this.track.delete().then(() => {
|
||||||
|
this.trackVM.onTrackDeleted();
|
||||||
|
this.dialog.destroy();
|
||||||
|
}).catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTrackUpdate() {
|
||||||
|
if (this.validate()) {
|
||||||
|
this.track.setName(this.model.trackname);
|
||||||
|
this.track.saveMeta()
|
||||||
|
.then(() => this.dialog.destroy())
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialog.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate form
|
||||||
|
* @return {boolean} True if valid
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
if (this.model.trackname === this.track.name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.model.trackname) {
|
||||||
|
alert($._('allrequired'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
352
js/src/trackviewmodel.js
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $, auth, config } from './initializer.js';
|
||||||
|
import TrackDialogModel from './trackdialogmodel.js';
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import uObserve from './observe.js';
|
||||||
|
import uPositionSet from './positionset.js';
|
||||||
|
import uSelect from './select.js';
|
||||||
|
import uTrack from './track.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class TrackViewModel
|
||||||
|
*/
|
||||||
|
export default class TrackViewModel extends ViewModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
*/
|
||||||
|
constructor(state) {
|
||||||
|
super({
|
||||||
|
/** @type {uTrack[]} */
|
||||||
|
trackList: [],
|
||||||
|
/** @type {string} */
|
||||||
|
currentTrackId: '',
|
||||||
|
/** @type {boolean} */
|
||||||
|
showLatest: false,
|
||||||
|
/** @type {boolean} */
|
||||||
|
autoReload: false,
|
||||||
|
/** @type {string} */
|
||||||
|
inputFile: false,
|
||||||
|
/** @type {string} */
|
||||||
|
summary: false,
|
||||||
|
// click handlers
|
||||||
|
/** @type {function} */
|
||||||
|
onReload: null,
|
||||||
|
/** @type {function} */
|
||||||
|
onExportGpx: null,
|
||||||
|
/** @type {function} */
|
||||||
|
onExportKml: null,
|
||||||
|
/** @type {function} */
|
||||||
|
onImportGpx: null,
|
||||||
|
/** @type {function} */
|
||||||
|
onTrackEdit: null
|
||||||
|
});
|
||||||
|
this.setClickHandlers();
|
||||||
|
/** @type HTMLSelectElement */
|
||||||
|
const listEl = document.querySelector('#track');
|
||||||
|
this.importEl = document.querySelector('#input-file');
|
||||||
|
this.editEl = this.getBoundElement('onTrackEdit');
|
||||||
|
this.select = new uSelect(listEl);
|
||||||
|
this.state = state;
|
||||||
|
this.timerId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {TrackViewModel}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.setObservers();
|
||||||
|
this.bindAll();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setObservers() {
|
||||||
|
this.onChanged('trackList', (list) => { this.select.setOptions(list); });
|
||||||
|
this.onChanged('currentTrackId', (listValue) => {
|
||||||
|
this.onTrackSelect(listValue);
|
||||||
|
});
|
||||||
|
this.onChanged('inputFile', (file) => {
|
||||||
|
if (file) { this.onImport(); }
|
||||||
|
});
|
||||||
|
this.onChanged('autoReload', (reload) => {
|
||||||
|
this.autoReload(reload);
|
||||||
|
});
|
||||||
|
this.onChanged('showLatest', (showLatest) => {
|
||||||
|
this.state.showLatest = showLatest;
|
||||||
|
this.onReload(true);
|
||||||
|
});
|
||||||
|
this.state.onChanged('currentUser', (user) => {
|
||||||
|
if (user) {
|
||||||
|
this.loadTrackList();
|
||||||
|
TrackViewModel.setMenuVisible(this.editEl, true);
|
||||||
|
} else {
|
||||||
|
this.model.currentTrackId = '';
|
||||||
|
this.model.trackList = [];
|
||||||
|
TrackViewModel.setMenuVisible(this.editEl, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.state.onChanged('currentTrack', (track) => {
|
||||||
|
this.renderSummary();
|
||||||
|
if (track) {
|
||||||
|
uObserve.observe(track, 'positions', () => {
|
||||||
|
this.renderSummary();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.state.onChanged('showAllUsers', (showAll) => {
|
||||||
|
if (showAll) {
|
||||||
|
this.loadAllUsersPosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.onChanged('interval', () => {
|
||||||
|
if (this.timerId) {
|
||||||
|
this.stopAutoReload();
|
||||||
|
this.startAutoReload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setClickHandlers() {
|
||||||
|
this.model.onReload = () => this.onReload();
|
||||||
|
const exportCb = (type) => () => {
|
||||||
|
if (this.state.currentTrack) {
|
||||||
|
this.state.currentTrack.export(type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.model.onExportGpx = exportCb('gpx');
|
||||||
|
this.model.onExportKml = exportCb('kml');
|
||||||
|
this.model.onImportGpx = () => this.importEl.click();
|
||||||
|
this.model.onTrackEdit = () => this.showDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload or update track view
|
||||||
|
* @param {boolean} clear Reload if true, update current track otherwise
|
||||||
|
*/
|
||||||
|
onReload(clear = false) {
|
||||||
|
if (this.state.showLatest) {
|
||||||
|
if (this.state.showAllUsers) {
|
||||||
|
this.loadAllUsersPosition();
|
||||||
|
} else if (this.state.currentUser) {
|
||||||
|
this.onUserLastPosition();
|
||||||
|
}
|
||||||
|
} else if (this.state.currentTrack instanceof uTrack) {
|
||||||
|
this.onTrackUpdate(clear);
|
||||||
|
} else if (this.state.currentTrack instanceof uPositionSet) {
|
||||||
|
this.state.currentTrack = null;
|
||||||
|
} else if (this.state.currentUser) {
|
||||||
|
this.loadTrackList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle import
|
||||||
|
*/
|
||||||
|
onImport() {
|
||||||
|
const form = this.importEl.parentElement;
|
||||||
|
const sizeMax = form.elements['MAX_FILE_SIZE'].value;
|
||||||
|
if (this.importEl.files && this.importEl.files.length === 1 && this.importEl.files[0].size > sizeMax) {
|
||||||
|
uUtils.error($._('isizefailure', sizeMax));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!auth.isAuthenticated) {
|
||||||
|
uUtils.error($._('notauthorized'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uTrack.import(form, auth.user)
|
||||||
|
.then((trackList) => {
|
||||||
|
if (trackList.length) {
|
||||||
|
if (trackList.length > 1) {
|
||||||
|
alert($._('imultiple', trackList.length));
|
||||||
|
}
|
||||||
|
this.model.trackList = trackList.concat(this.model.trackList);
|
||||||
|
this.model.currentTrackId = trackList[0].listValue;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => uUtils.error(e, `${$._('actionfailure')}\n${e.message}`))
|
||||||
|
.finally(() => {
|
||||||
|
this.model.inputFile = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle track change
|
||||||
|
* @param {string} listValue Track list selected option
|
||||||
|
*/
|
||||||
|
onTrackSelect(listValue) {
|
||||||
|
/** @type {(uTrack|undefined)} */
|
||||||
|
const track = this.model.trackList.find((_track) => _track.listValue === listValue);
|
||||||
|
if (!track) {
|
||||||
|
this.state.currentTrack = null;
|
||||||
|
} else if (!track.isEqualTo(this.state.currentTrack)) {
|
||||||
|
track.fetchPositions().then(() => {
|
||||||
|
console.log(`currentTrack id: ${track.id}, loaded ${track.length} positions`);
|
||||||
|
this.state.currentTrack = track;
|
||||||
|
if (this.model.showLatest) {
|
||||||
|
this.model.showLatest = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle track update
|
||||||
|
* @param {boolean=} clear
|
||||||
|
*/
|
||||||
|
onTrackUpdate(clear) {
|
||||||
|
if (clear) {
|
||||||
|
this.state.currentTrack.clear();
|
||||||
|
}
|
||||||
|
this.state.currentTrack.fetchPositions()
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle user last position request
|
||||||
|
*/
|
||||||
|
onUserLastPosition() {
|
||||||
|
this.state.currentUser.fetchLastPosition()
|
||||||
|
.then((_track) => {
|
||||||
|
if (_track) {
|
||||||
|
if (!this.model.trackList.find((listItem) => listItem.listValue === _track.listValue)) {
|
||||||
|
this.model.trackList.unshift(_track);
|
||||||
|
}
|
||||||
|
this.state.currentTrack = _track;
|
||||||
|
this.model.currentTrackId = _track.listValue;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle last position of all users request
|
||||||
|
*/
|
||||||
|
loadAllUsersPosition() {
|
||||||
|
uPositionSet.fetchLatest()
|
||||||
|
.then((_track) => {
|
||||||
|
if (_track) {
|
||||||
|
this.model.trackList = [];
|
||||||
|
this.model.currentTrackId = '';
|
||||||
|
this.state.currentTrack = _track;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTrackList() {
|
||||||
|
uTrack.fetchList(this.state.currentUser)
|
||||||
|
.then((_tracks) => {
|
||||||
|
this.model.trackList = _tracks;
|
||||||
|
if (_tracks.length) {
|
||||||
|
if (this.state.showLatest) {
|
||||||
|
this.onUserLastPosition();
|
||||||
|
} else {
|
||||||
|
// autoload first track in list
|
||||||
|
this.model.currentTrackId = _tracks[0].listValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.model.currentTrackId = '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog() {
|
||||||
|
const vm = new TrackDialogModel(this);
|
||||||
|
vm.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
onTrackDeleted() {
|
||||||
|
const index = this.model.trackList.indexOf(this.state.currentTrack);
|
||||||
|
this.state.currentTrack = null;
|
||||||
|
if (index !== -1) {
|
||||||
|
this.model.trackList.splice(index, 1);
|
||||||
|
if (this.model.trackList.length) {
|
||||||
|
this.model.currentTrackId = this.model.trackList[index].listValue;
|
||||||
|
} else {
|
||||||
|
this.model.currentTrackId = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} start
|
||||||
|
*/
|
||||||
|
autoReload(start) {
|
||||||
|
if (start) {
|
||||||
|
this.startAutoReload();
|
||||||
|
} else {
|
||||||
|
this.stopAutoReload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startAutoReload() {
|
||||||
|
this.timerId = setInterval(() => this.onReload(), config.interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAutoReload() {
|
||||||
|
clearInterval(this.timerId);
|
||||||
|
this.timerId = 0;
|
||||||
|
this.model.autoReload = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
* @param {boolean} visible
|
||||||
|
*/
|
||||||
|
static setMenuVisible(el, visible) {
|
||||||
|
if (el) {
|
||||||
|
if (visible) {
|
||||||
|
el.classList.remove('menu-hidden');
|
||||||
|
} else {
|
||||||
|
el.classList.add('menu-hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSummary() {
|
||||||
|
if (!this.state.currentTrack || !this.state.currentTrack.hasPositions) {
|
||||||
|
this.model.summary = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const last = this.state.currentTrack.positions[this.state.currentTrack.length - 1];
|
||||||
|
|
||||||
|
if (this.state.showLatest) {
|
||||||
|
const today = new Date();
|
||||||
|
const date = new Date(last.timestamp * 1000);
|
||||||
|
const dateTime = uUtils.getTimeString(date);
|
||||||
|
const dateString = (date.toDateString() !== today.toDateString()) ? `${dateTime.date}<br>` : '';
|
||||||
|
const timeString = `${dateTime.time}<span style="font-weight:normal">${dateTime.zone}</span>`;
|
||||||
|
this.model.summary = `
|
||||||
|
<div class="menu-title">${$._('latest')}:</div>
|
||||||
|
${dateString}
|
||||||
|
${timeString}`;
|
||||||
|
} else {
|
||||||
|
this.model.summary = `
|
||||||
|
<div class="menu-title">${$._('summary')}</div>
|
||||||
|
<div><img class="icon" alt="${$._('tdistance')}" title="${$._('tdistance')}" src="images/distance.svg"> ${$.getLocaleDistanceMajor(last.totalMeters, true)}</div>
|
||||||
|
<div><img class="icon" alt="${$._('ttime')}" title="${$._('ttime')}" src="images/time.svg"> ${$.getLocaleDuration(last.totalSeconds)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
74
js/src/ulogger.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $, config, initializer, uInitializer } from './initializer.js';
|
||||||
|
import ChartViewModel from './chartviewmodel.js';
|
||||||
|
import ConfigViewModel from './configviewmodel.js';
|
||||||
|
import MainViewModel from './mainviewmodel.js';
|
||||||
|
import MapViewModel from './mapviewmodel.js';
|
||||||
|
import TrackViewModel from './trackviewmodel.js';
|
||||||
|
import UserViewModel from './userviewmodel.js';
|
||||||
|
import uState from './state.js';
|
||||||
|
|
||||||
|
const domReady = uInitializer.waitForDom();
|
||||||
|
const initReady = initializer.initialize();
|
||||||
|
|
||||||
|
Promise.all([ domReady, initReady ])
|
||||||
|
.then(() => {
|
||||||
|
start();
|
||||||
|
})
|
||||||
|
.catch((msg) => alert(`${$._('actionfailure')}\n${msg}`));
|
||||||
|
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
const state = new uState();
|
||||||
|
|
||||||
|
const mainVM = new MainViewModel(state);
|
||||||
|
const userVM = new UserViewModel(state);
|
||||||
|
const trackVM = new TrackViewModel(state);
|
||||||
|
const mapVM = new MapViewModel(state);
|
||||||
|
const chartVM = new ChartViewModel(state);
|
||||||
|
const configVM = new ConfigViewModel(state);
|
||||||
|
mainVM.init();
|
||||||
|
userVM.init();
|
||||||
|
trackVM.init();
|
||||||
|
mapVM.init().loadMapAPI(config.mapApi);
|
||||||
|
chartVM.init();
|
||||||
|
configVM.init();
|
||||||
|
|
||||||
|
mapVM.onChanged('markerOver', (id) => {
|
||||||
|
if (id !== null) {
|
||||||
|
chartVM.onPointOver(id);
|
||||||
|
} else {
|
||||||
|
chartVM.onPointOut();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mapVM.onChanged('markerSelect', (id) => {
|
||||||
|
if (id !== null) {
|
||||||
|
chartVM.onPointSelect(id);
|
||||||
|
} else {
|
||||||
|
chartVM.onPointUnselect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chartVM.onChanged('pointSelected', (id) => {
|
||||||
|
if (id !== null) {
|
||||||
|
mapVM.api.animateMarker(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
116
js/src/user.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uAjax from './ajax.js';
|
||||||
|
import uListItem from './listitem.js';
|
||||||
|
import uTrack from './track.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class uUser
|
||||||
|
* @property {number} id
|
||||||
|
* @property {string} login
|
||||||
|
* @property {string} [password]
|
||||||
|
*/
|
||||||
|
export default class uUser extends uListItem {
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @param {string} login
|
||||||
|
*/
|
||||||
|
constructor(id, login) {
|
||||||
|
super();
|
||||||
|
if (!Number.isSafeInteger(id) || id <= 0) {
|
||||||
|
throw new Error('Invalid argument for user constructor');
|
||||||
|
}
|
||||||
|
this.id = id;
|
||||||
|
this.login = login;
|
||||||
|
this.listItem(id, login);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uUser} user
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isEqualTo(user) {
|
||||||
|
return !!user && user.id === this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<uTrack, Error>}
|
||||||
|
*/
|
||||||
|
fetchLastPosition() {
|
||||||
|
return uTrack.fetchLatest(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws
|
||||||
|
* @return {Promise<uUser[], Error>}
|
||||||
|
*/
|
||||||
|
static fetchList() {
|
||||||
|
return uAjax.get('utils/getusers.php').then((_users) => {
|
||||||
|
const users = [];
|
||||||
|
for (const user of _users) {
|
||||||
|
users.push(new uUser(user.id, user.login));
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
return uUser.update({
|
||||||
|
action: 'delete',
|
||||||
|
login: this.login
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} login
|
||||||
|
* @param {string} password
|
||||||
|
* @return {Promise<uUser>}
|
||||||
|
*/
|
||||||
|
static add(login, password) {
|
||||||
|
return uUser.update({
|
||||||
|
action: 'add',
|
||||||
|
login: login,
|
||||||
|
pass: password
|
||||||
|
}).then((user) => new uUser(user.id, login));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} data
|
||||||
|
* @return {Promise<*, Error>}
|
||||||
|
*/
|
||||||
|
static update(data) {
|
||||||
|
return uAjax.post('utils/handleuser.php', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} password
|
||||||
|
* @param {string=} oldPassword Needed when changing own password
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
setPassword(password, oldPassword) {
|
||||||
|
return uAjax.post('utils/changepass.php',
|
||||||
|
{
|
||||||
|
login: this.login,
|
||||||
|
pass: password,
|
||||||
|
oldpass: oldPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
172
js/src/userdialogmodel.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $, auth, config } from './initializer.js';
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import uDialog from './dialog.js';
|
||||||
|
import uUser from './user.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
export default class UserDialogModel extends ViewModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UserViewModel} viewModel
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
|
constructor(viewModel, type) {
|
||||||
|
super({
|
||||||
|
onUserDelete: null,
|
||||||
|
onUserUpdate: null,
|
||||||
|
onUserAdd: null,
|
||||||
|
onCancel: null,
|
||||||
|
login: null,
|
||||||
|
password: null,
|
||||||
|
password2: null,
|
||||||
|
oldPassword: null
|
||||||
|
});
|
||||||
|
this.user = viewModel.state.currentUser;
|
||||||
|
this.type = type;
|
||||||
|
this.userVM = viewModel;
|
||||||
|
this.model.onUserDelete = () => this.onUserDelete();
|
||||||
|
this.model.onUserUpdate = () => this.onUserUpdate();
|
||||||
|
this.model.onUserAdd = () => this.onUserAdd();
|
||||||
|
this.model.onCancel = () => this.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const html = this.getHtml();
|
||||||
|
this.dialog = new uDialog(html);
|
||||||
|
this.dialog.show();
|
||||||
|
this.bindAll(this.dialog.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserDelete() {
|
||||||
|
if (uDialog.isConfirmed($._('userdelwarn', uUtils.htmlEncode(this.user.login)))) {
|
||||||
|
this.user.delete().then(() => {
|
||||||
|
this.userVM.onUserDeleted();
|
||||||
|
this.dialog.destroy();
|
||||||
|
}).catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserUpdate() {
|
||||||
|
if (this.validate()) {
|
||||||
|
const user = this.type === 'pass' ? auth.user : this.user;
|
||||||
|
user.setPassword(this.model.password, this.model.oldPassword)
|
||||||
|
.then(() => this.dialog.destroy())
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserAdd() {
|
||||||
|
if (this.validate()) {
|
||||||
|
uUser.add(this.model.login, this.model.password).then((user) => {
|
||||||
|
this.userVM.onUserAdded(user);
|
||||||
|
this.dialog.destroy();
|
||||||
|
}).catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialog.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate form
|
||||||
|
* @return {boolean} True if valid
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
if (this.type === 'add') {
|
||||||
|
if (!this.model.login) {
|
||||||
|
alert($._('allrequired'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (this.type === 'pass') {
|
||||||
|
if (!this.model.oldPassword) {
|
||||||
|
alert($._('allrequired'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.model.password || !this.model.password2) {
|
||||||
|
alert($._('allrequired'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.model.password !== this.model.password2) {
|
||||||
|
alert($._('passnotmatch'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!config.passRegex.test(this.model.password)) {
|
||||||
|
alert($._('passlenmin') + '\n' + $._('passrules'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getHtml() {
|
||||||
|
let deleteButton = '';
|
||||||
|
let header = '';
|
||||||
|
let observer;
|
||||||
|
let fields;
|
||||||
|
switch (this.type) {
|
||||||
|
case 'add':
|
||||||
|
observer = 'onUserAdd';
|
||||||
|
header = `<label><b>${$._('username')}</b></label>
|
||||||
|
<input type="text" placeholder="${$._('usernameenter')}" name="login" data-bind="login" required>`;
|
||||||
|
fields = `<label><b>${$._('password')}</b></label>
|
||||||
|
<input type="password" placeholder="${$._('passwordenter')}" name="password" data-bind="password" required>
|
||||||
|
<label><b>${$._('passwordrepeat')}</b></label>
|
||||||
|
<input type="password" placeholder="${$._('passwordenter')}" name="password2" data-bind="password2" required>`;
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
observer = 'onUserUpdate';
|
||||||
|
deleteButton = `<div class="red-button button-resolve"><b><a data-bind="onUserDelete">${$._('deluser')}</a></b></div>
|
||||||
|
<div>${$._('editinguser', `<b>${uUtils.htmlEncode(this.user.login)}</b>`)}</div>
|
||||||
|
<div style="clear: both; padding-bottom: 1em;"></div>`;
|
||||||
|
fields = `<label><b>${$._('password')}</b></label>
|
||||||
|
<input type="password" placeholder="${$._('passwordenter')}" name="password" data-bind="password" required>
|
||||||
|
<label><b>${$._('passwordrepeat')}</b></label>
|
||||||
|
<input type="password" placeholder="${$._('passwordenter')}" name="password2" data-bind="password2" required>`;
|
||||||
|
break;
|
||||||
|
case 'pass':
|
||||||
|
observer = 'onUserUpdate';
|
||||||
|
fields = `<label><b>${$._('oldpassword')}</b></label>
|
||||||
|
<input type="password" placeholder="${$._('passwordenter')}" name="old-password" data-bind="oldPassword" required>
|
||||||
|
<label><b>${$._('newpassword')}</b></label>
|
||||||
|
<input type="password" placeholder="${$._('passwordenter')}" name="password" data-bind="password" required>
|
||||||
|
<label><b>${$._('newpasswordrepeat')}</b></label>
|
||||||
|
<input type="password" placeholder="${$._('passwordenter')}" name="password2" data-bind="password2" required>`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown dialog type: ${this.type}`);
|
||||||
|
}
|
||||||
|
return `${deleteButton}
|
||||||
|
<form id="userForm">
|
||||||
|
${header}
|
||||||
|
${fields}
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button-reject" type="button" data-bind="onCancel">${$._('cancel')}</button>
|
||||||
|
<button class="button-resolve" type="submit" data-bind="${observer}">${$._('submit')}</button>
|
||||||
|
</div>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
148
js/src/userviewmodel.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang as $, auth } from './initializer.js';
|
||||||
|
import UserDialogModel from './userdialogmodel.js';
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import uSelect from './select.js';
|
||||||
|
import uUser from './user.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class UserViewModel
|
||||||
|
*/
|
||||||
|
export default class UserViewModel extends ViewModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
*/
|
||||||
|
constructor(state) {
|
||||||
|
super({
|
||||||
|
/** @type {uUser[]} */
|
||||||
|
userList: [],
|
||||||
|
/** @type {string} */
|
||||||
|
currentUserId: '0',
|
||||||
|
// click handlers
|
||||||
|
/** @type {function} */
|
||||||
|
onUserEdit: null,
|
||||||
|
/** @type {function} */
|
||||||
|
onUserAdd: null,
|
||||||
|
/** @type {function} */
|
||||||
|
onPasswordChange: null
|
||||||
|
});
|
||||||
|
this.setClickHandlers();
|
||||||
|
/** @type HTMLSelectElement */
|
||||||
|
const listEl = document.querySelector('#user');
|
||||||
|
this.editEl = this.getBoundElement('onUserEdit');
|
||||||
|
this.select = new uSelect(listEl, $._('suser'), `- ${$._('allusers')} -`);
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
setClickHandlers() {
|
||||||
|
this.model.onUserEdit = () => this.showDialog('edit');
|
||||||
|
this.model.onUserAdd = () => this.showDialog('add');
|
||||||
|
this.model.onPasswordChange = () => this.showDialog('pass');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {UserViewModel}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.setObservers(this.state);
|
||||||
|
this.bindAll();
|
||||||
|
uUser.fetchList()
|
||||||
|
.then((_users) => {
|
||||||
|
this.model.userList = _users;
|
||||||
|
if (_users.length) {
|
||||||
|
let userId = _users[0].listValue;
|
||||||
|
if (auth.isAuthenticated) {
|
||||||
|
const user = this.model.userList.find((_user) => _user.listValue === auth.user.listValue);
|
||||||
|
if (user) {
|
||||||
|
userId = user.listValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.model.currentUserId = userId;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uState} state
|
||||||
|
*/
|
||||||
|
setObservers(state) {
|
||||||
|
this.onChanged('userList', (list) => {
|
||||||
|
this.select.setOptions(list);
|
||||||
|
});
|
||||||
|
this.onChanged('currentUserId', (listValue) => {
|
||||||
|
this.state.showAllUsers = listValue === uSelect.allValue;
|
||||||
|
this.state.currentUser = this.model.userList.find((_user) => _user.listValue === listValue) || null;
|
||||||
|
UserViewModel.setMenuVisible(this.editEl, this.state.currentUser !== null && !this.state.currentUser.isEqualTo(auth.user));
|
||||||
|
});
|
||||||
|
state.onChanged('showLatest', (showLatest) => {
|
||||||
|
if (showLatest) {
|
||||||
|
this.select.showAllOption();
|
||||||
|
} else {
|
||||||
|
this.select.hideAllOption();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog(action) {
|
||||||
|
const vm = new UserDialogModel(this, action);
|
||||||
|
vm.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uUser} newUser
|
||||||
|
*/
|
||||||
|
onUserAdded(newUser) {
|
||||||
|
this.model.userList.push(newUser);
|
||||||
|
this.model.userList.sort((a, b) => ((a.login > b.login) ? 1 : -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserDeleted() {
|
||||||
|
const index = this.model.userList.indexOf(this.state.currentUser);
|
||||||
|
this.state.currentUser = null;
|
||||||
|
if (index !== -1) {
|
||||||
|
this.model.userList.splice(index, 1);
|
||||||
|
if (this.model.userList.length) {
|
||||||
|
this.model.currentUserId = this.model.userList[index].listValue;
|
||||||
|
} else {
|
||||||
|
this.model.currentUserId = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
* @param {boolean} visible
|
||||||
|
*/
|
||||||
|
static setMenuVisible(el, visible) {
|
||||||
|
if (el) {
|
||||||
|
if (visible) {
|
||||||
|
el.classList.remove('menu-hidden');
|
||||||
|
} else {
|
||||||
|
el.classList.add('menu-hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
329
js/src/utils.js
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
/*
|
||||||
|
* μlogger
|
||||||
|
*
|
||||||
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class uUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cookie
|
||||||
|
* @param {string} name
|
||||||
|
* @param {(string|number)} value
|
||||||
|
* @param {?number=} days Default validity is 30 days, null = never expire
|
||||||
|
*/
|
||||||
|
static setCookie(name, value, days = 30) {
|
||||||
|
let expires = '';
|
||||||
|
if (days) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
expires = `; expires=${date.toUTCString()}`;
|
||||||
|
}
|
||||||
|
document.cookie = `ulogger_${name}=${value}${expires}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sprintf, naive approach, only %s, %d supported
|
||||||
|
* @param {string} fmt String
|
||||||
|
* @param {...(string|number)=} params Optional parameters
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static sprintf(fmt, ...params) {
|
||||||
|
let i = 0;
|
||||||
|
const ret = fmt.replace(/%%|%s|%d/g, (match) => {
|
||||||
|
if (match === '%%') {
|
||||||
|
return '%';
|
||||||
|
} else if (match === '%d' && isNaN(params[i])) {
|
||||||
|
throw new Error(`Wrong format specifier ${match} for ${params[i]} argument`);
|
||||||
|
}
|
||||||
|
if (typeof params[i] === 'undefined') {
|
||||||
|
throw new Error(`Missing argument for format specifier ${match}`);
|
||||||
|
}
|
||||||
|
return params[i++];
|
||||||
|
});
|
||||||
|
if (i < params.length) {
|
||||||
|
throw new Error(`Unused argument for format specifier ${fmt}`);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add script tag
|
||||||
|
* @param {string} url attribute
|
||||||
|
* @param {string} id attribute
|
||||||
|
* @param {Function=} onload
|
||||||
|
* @param {Function=} onerror
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line max-params
|
||||||
|
static addScript(url, id, onload, onerror) {
|
||||||
|
if (id && document.getElementById(id)) {
|
||||||
|
if (onload instanceof Function) {
|
||||||
|
onload();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = document.createElement('script');
|
||||||
|
tag.type = 'text/javascript';
|
||||||
|
tag.src = url;
|
||||||
|
if (id) {
|
||||||
|
tag.id = id;
|
||||||
|
}
|
||||||
|
tag.async = true;
|
||||||
|
if (onload instanceof Function) {
|
||||||
|
tag.onload = onload;
|
||||||
|
}
|
||||||
|
if (onerror instanceof Function) {
|
||||||
|
tag.onerror = () => onerror(new Error(`error loading ${id} script`));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load script with timeout
|
||||||
|
* @param {string} url URL
|
||||||
|
* @param {string} id Element id
|
||||||
|
* @param {number=} ms Timeout in ms
|
||||||
|
* @return {Promise<void, Error>}
|
||||||
|
*/
|
||||||
|
static loadScript(url, id, ms = 10000) {
|
||||||
|
const scriptLoaded = new Promise(
|
||||||
|
(resolve, reject) => uUtils.addScript(url, id, resolve, reject));
|
||||||
|
const timeout = this.timeoutPromise(ms);
|
||||||
|
return Promise.race([ scriptLoaded, timeout ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static timeoutPromise(ms) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tid = setTimeout(() => {
|
||||||
|
clearTimeout(tid);
|
||||||
|
reject(new Error(`timeout (${ms} ms).`));
|
||||||
|
}, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode string for HTML
|
||||||
|
* @param {string} s
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static htmlEncode(s) {
|
||||||
|
return s.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hex string and opacity to an rgba string
|
||||||
|
* @param {string} hex
|
||||||
|
* @param {number} opacity
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static hexToRGBA(hex, opacity) {
|
||||||
|
return `rgba(${(hex = hex.replace('#', ''))
|
||||||
|
.match(new RegExp(`(.{${hex.length / 3}})`, 'g'))
|
||||||
|
.map((l) => parseInt(hex.length % 2 ? l + l : l, 16))
|
||||||
|
.concat(opacity || 1).join(',')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add link tag with type css
|
||||||
|
* @param {string} url attribute
|
||||||
|
* @param {string} id attribute
|
||||||
|
*/
|
||||||
|
static addCss(url, id) {
|
||||||
|
if (id && document.getElementById(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = document.createElement('link');
|
||||||
|
tag.type = 'text/css';
|
||||||
|
tag.rel = 'stylesheet';
|
||||||
|
tag.href = url;
|
||||||
|
if (id) {
|
||||||
|
tag.id = id;
|
||||||
|
}
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove HTML element
|
||||||
|
* @param {string} id Element ID
|
||||||
|
*/
|
||||||
|
static removeElementById(id) {
|
||||||
|
const tag = document.getElementById(id);
|
||||||
|
if (tag && tag.parentNode) {
|
||||||
|
tag.parentNode.removeChild(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} html HTML representing a single element
|
||||||
|
* @return {Node}
|
||||||
|
*/
|
||||||
|
static nodeFromHtml(html) {
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = html;
|
||||||
|
return template.content.firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} html HTML representing a single element
|
||||||
|
* @return {NodeList}
|
||||||
|
*/
|
||||||
|
static nodesFromHtml(html) {
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = html;
|
||||||
|
return template.content.childNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {NodeList} nodeList
|
||||||
|
* @param {string} selector
|
||||||
|
* @return {?Element}
|
||||||
|
*/
|
||||||
|
static querySelectorInList(nodeList, selector) {
|
||||||
|
for (const node of nodeList) {
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
const el = node.querySelector(selector);
|
||||||
|
if (el) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws On invalid input
|
||||||
|
* @param {*} input
|
||||||
|
* @param {boolean=} isNullable
|
||||||
|
* @return {(null|number)}
|
||||||
|
*/
|
||||||
|
static getFloat(input, isNullable = false) {
|
||||||
|
return uUtils.getParsed(input, isNullable, 'float');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws On invalid input
|
||||||
|
* @param {*} input
|
||||||
|
* @param {boolean=} isNullable
|
||||||
|
* @return {(null|number)}
|
||||||
|
*/
|
||||||
|
static getInteger(input, isNullable = false) {
|
||||||
|
return uUtils.getParsed(input, isNullable, 'int');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws On invalid input
|
||||||
|
* @param {*} input
|
||||||
|
* @param {boolean=} isNullable
|
||||||
|
* @return {(null|string)}
|
||||||
|
*/
|
||||||
|
static getString(input, isNullable = false) {
|
||||||
|
return uUtils.getParsed(input, isNullable, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws On invalid input
|
||||||
|
* @param {*} input
|
||||||
|
* @param {boolean} isNullable
|
||||||
|
* @param {string} type
|
||||||
|
* @return {(null|number|string)}
|
||||||
|
*/
|
||||||
|
static getParsed(input, isNullable, type) {
|
||||||
|
if (isNullable && input === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let output;
|
||||||
|
switch (type) {
|
||||||
|
case 'float':
|
||||||
|
output = parseFloat(input);
|
||||||
|
break;
|
||||||
|
case 'int':
|
||||||
|
output = Math.round(parseFloat(input));
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
output = String(input);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown type');
|
||||||
|
}
|
||||||
|
if (typeof input === 'undefined' || input === null ||
|
||||||
|
(type !== 'string' && isNaN(output))) {
|
||||||
|
throw new Error('Invalid value');
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date to date, time and time zone strings
|
||||||
|
* Simplify zone name, eg.
|
||||||
|
* date: 2017-06-14, time: 11:42:19, zone: GMT+2 CEST
|
||||||
|
* @param {Date} date
|
||||||
|
* @return {{date: string, time: string, zone: string}}
|
||||||
|
*/
|
||||||
|
static getTimeString(date) {
|
||||||
|
let timeZone = '';
|
||||||
|
const dateStr = `${date.getFullYear()}-${(`0${date.getMonth() + 1}`).slice(-2)}-${(`0${date.getDate()}`).slice(-2)}`;
|
||||||
|
const timeStr = date.toTimeString().replace(/^\s*([^ ]+)([^(]*)(\([^)]*\))*/,
|
||||||
|
// eslint-disable-next-line max-params
|
||||||
|
(_, hours, zone, dst) => {
|
||||||
|
if (zone) {
|
||||||
|
timeZone = zone.replace(/(0(?=[1-9]00))|(00\b)/g, '');
|
||||||
|
if (dst && (/[A-Z]/).test(dst)) {
|
||||||
|
timeZone += dst.match(/\b[A-Z]+/g).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hours;
|
||||||
|
});
|
||||||
|
return { date: dateStr, time: timeStr, zone: timeZone };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
static openUrl(url) {
|
||||||
|
window.location.assign(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(Error|string)} e
|
||||||
|
* @param {string=} message
|
||||||
|
*/
|
||||||
|
static error(e, message) {
|
||||||
|
let details;
|
||||||
|
if (e instanceof Error) {
|
||||||
|
details = `${e.name}: ${e.message} (${e.stack})`;
|
||||||
|
} else {
|
||||||
|
details = e;
|
||||||
|
message = e;
|
||||||
|
}
|
||||||
|
console.error(details);
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Degrees to radians
|
||||||
|
* @param {number} degrees
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
static deg2rad(degrees) {
|
||||||
|
return degrees * Math.PI / 180;
|
||||||
|
}
|
||||||
|
}
|