Merge branch 'next'

This commit is contained in:
Bartek Fabiszewski 2020-02-17 09:41:59 +01:00
commit 7e225866b1
165 changed files with 26414 additions and 2585 deletions

View File

@ -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: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
sed -i "s/^\$dbuser = .*$//" /var/www/html/config.php
sed -i "s/^\$dbpass = .*$//" /var/www/html/config.php
@ -49,12 +54,12 @@ else
mysqld_safe --datadir=/data &
mysqladmin --silent --wait=30 ping
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} -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 "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 "INSERT INTO users (login, password) VALUES ('admin', '\$2y\$10\$7OvZrKgonVZM9lkzrTbiou.CVhO3HjPk5y0W9L68fVwPs/osBRIMq')" ulogger
mysqladmin -u root -p${DB_ROOT_PASS} shutdown
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 "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 "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
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
fi

343
.eslintrc.js Normal file
View 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"
]
}
};

View File

@ -36,7 +36,7 @@ abstract class BaseDatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
protected $testAccuracy = 10;
protected $testProvider = "gps";
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"
protected function getSetUpOperation() {
@ -180,7 +180,11 @@ abstract class BaseDatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
protected function addTestUser($user = NULL, $pass = NULL) {
if (is_null($user)) { $user = $this->testUser; }
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($trackName)) { $trackName = $this->testTrackName; }
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;
}
/**

View File

@ -224,8 +224,7 @@ class ClientAPITest extends UloggerAPITestCase {
'bearing' => $this->testBearing,
'accuracy' => $this->testAccuracy,
'provider' => $this->testProvider,
'comment' => $this->testComment,
'imageid' => $this->testImageId
'comment' => $this->testComment
],
];
$response = $this->http->post('/client/index.php', $options);
@ -246,15 +245,115 @@ class ClientAPITest extends UloggerAPITestCase {
"accuracy" => $this->testAccuracy,
"provider" => $this->testProvider,
"comment" => $this->testComment,
"image_id" => $this->testImageId
"image" => null
];
$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_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");
}
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() {
$this->assertTrue($this->authenticate(), "Authentication failed");
@ -275,7 +374,7 @@ class ClientAPITest extends UloggerAPITestCase {
'accuracy' => $this->testAccuracy,
'provider' => $this->testProvider,
'comment' => $this->testComment,
'imageid' => $this->testImageId
'imageid' => $this->testImage
],
];
$response = $this->http->post('/client/index.php', $options);
@ -306,7 +405,7 @@ class ClientAPITest extends UloggerAPITestCase {
'accuracy' => $this->testAccuracy,
'provider' => $this->testProvider,
'comment' => $this->testComment,
'imageid' => $this->testImageId
'imageid' => $this->testImage
],
];
@ -343,7 +442,7 @@ class ClientAPITest extends UloggerAPITestCase {
'accuracy' => $this->testAccuracy,
'provider' => $this->testProvider,
'comment' => $this->testComment,
'imageid' => $this->testImageId
'imageid' => $this->testImage
],
];

View File

@ -1,4 +1,4 @@
<?php
<?php /** @noinspection HtmlUnknownAttribute */
use Psr\Http\Message\ResponseInterface;
@ -54,12 +54,13 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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");
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
$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(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
@ -88,12 +89,12 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$actual = $this->getConnection()->createQueryTable(
"positions",
"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");
@ -110,7 +111,7 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$this->assertTableContains($expected, $actual, "Wrong actual table data");
}
@ -170,12 +171,14 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(0, (int) $xml->error, "Wrong error status");
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 1, "Wrong count of tracks");
$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("positions"), "Wrong row count");
@ -204,12 +207,12 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$actual = $this->getConnection()->createQueryTable(
"positions",
"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");
}
@ -241,7 +244,7 @@ class ImportTest extends UloggerAPITestCase {
<ele>' . $this->testAltitude . '</ele>
<time>' . gmdate("Y-m-d\TH:i:s\Z", $this->testTimestamp) . '</time>
<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>
<ulogger:speed>' . $this->testSpeed . '</ulogger:speed>
<ulogger:bearing>' . $this->testBearing . '</ulogger:bearing>
@ -270,12 +273,14 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(0, (int) $xml->error, "Wrong error status");
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 1, "Wrong count of tracks");
$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("positions"), "Wrong row count");
@ -303,13 +308,13 @@ class ImportTest extends UloggerAPITestCase {
"bearing" => $this->testBearing,
"accuracy" => $this->testAccuracy,
"provider" => $this->testProvider,
"comment" => null,
"image_id" => null
"comment" => $this->testComment,
"image" => null
];
$actual = $this->getConnection()->createQueryTable(
"positions",
"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");
}
@ -351,12 +356,14 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(0, (int) $xml->error, "Wrong error status");
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 1, "Wrong count of tracks");
$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("positions"), "Wrong row count");
@ -385,12 +392,12 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$actual = $this->getConnection()->createQueryTable(
"positions",
"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");
}
@ -438,12 +445,14 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(0, (int) $xml->error, "Wrong error status: $xml->message");
$this->assertEquals(1, (int) $xml->trackid, "Wrong error message");
$this->assertEquals(1, (int) $xml->trackcnt, "Wrong error message");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 1, "Wrong count of tracks");
$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(2, $this->getConnection()->getRowCount("positions"), "Wrong row count");
@ -472,12 +481,12 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$actual = $this->getConnection()->createQueryTable(
"positions",
"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");
$expected = [
@ -493,7 +502,7 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$this->assertTableContains($expected, $actual, "Wrong actual table data");
}
@ -543,12 +552,18 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(0, (int) $xml->error, "Wrong error status: $xml->message");
$this->assertEquals(2, (int) $xml->trackid, "Wrong error message");
$this->assertEquals(2, (int) $xml->trackcnt, "Wrong error message");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 2, "Wrong count of tracks");
$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("positions"), "Wrong row count");
@ -584,12 +599,12 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$actual = $this->getConnection()->createQueryTable(
"positions",
"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");
$expected = [
@ -605,7 +620,7 @@ class ImportTest extends UloggerAPITestCase {
"accuracy" => null,
"provider" => "gps",
"comment" => null,
"image_id" => null
"image" => null
];
$this->assertTableContains($expected, $actual, "Wrong actual table data");
}
@ -647,11 +662,11 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(1, (int) $xml->error, "Wrong error status");
$this->assertEquals($lang["iparsefailure"], (string) $xml->message, "Wrong error status");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(1, (int) $json->error, "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("positions"), "Wrong row count");
@ -694,11 +709,11 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(1, (int) $xml->error, "Wrong error status");
$this->assertEquals($lang["iparsefailure"], (string) $xml->message, "Wrong error status");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(1, (int) $json->error, "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("positions"), "Wrong row count");
@ -735,11 +750,11 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(1, (int) $xml->error, "Wrong error status");
$this->assertEquals($lang["iparsefailure"], (string) $xml->message, "Wrong error status");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(1, (int) $json->error, "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("positions"), "Wrong row count");
@ -780,29 +795,16 @@ class ImportTest extends UloggerAPITestCase {
$response = $this->http->post("/utils/import.php", $options);
$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->assertEquals(1, (int) $xml->error, "Wrong error status");
$this->assertEquals(0, strpos((string) $xml->message, $lang["iparsefailure"]), "Wrong error status");
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(1, (int) $json->error, "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("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) {
$stream = tmpfile();
fwrite($stream, $string);

View File

@ -28,20 +28,20 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->get("/utils/getpositions.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 2, "Wrong count of positions");
$position = $xml->position[0];
$this->assertEquals((int) $position["id"], 1, "Wrong position id");
$position = $json[0];
$this->assertEquals((int) $position->id, 1, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
$this->assertEquals((string) $position->username, $this->testAdminUser, "Wrong username");
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
$position = $xml->position[1];
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
$position = $json[1];
$this->assertEquals((int) $position->id, 2, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 2, "Wrong count of positions");
$position = $xml->position[0];
$this->assertEquals((int) $position["id"], 1, "Wrong position id");
$position = $json[0];
$this->assertEquals((int) $position->id, 1, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
$this->assertEquals((string) $position->username, $this->testUser, "Wrong username");
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
$position = $xml->position[1];
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
$position = $json[1];
$this->assertEquals((int) $position->id, 2, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->position->count(), 0, "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 0, "Wrong count of positions");
}
public function testGetPositionsOtherUserByAdmin() {
@ -131,20 +131,20 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->get("/utils/getpositions.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 2, "Wrong count of positions");
$position = $xml->position[0];
$this->assertEquals((int) $position["id"], 1, "Wrong position id");
$position = $json[0];
$this->assertEquals((int) $position->id, 1, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$this->assertEquals((int) $position->timestamp, $this->testTimestamp, "Wrong timestamp");
$this->assertEquals((string) $position->username, $this->testUser, "Wrong username");
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
$position = $xml->position[1];
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
$position = $json[1];
$this->assertEquals((int) $position->id, 2, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->position->count(), 1, "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 1, "Wrong count of positions");
$position = $xml->position[0];
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
$position = $json[0];
$this->assertEquals((int) $position->id, 2, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->position->count(), 2, "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 2, "Wrong count of positions");
$position = $xml->position[0];
$this->assertEquals((int) $position["id"], 2, "Wrong position id");
$position = $json[0];
$this->assertEquals((int) $position->id, 2, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$this->assertEquals((int) $position->timestamp, $this->testTimestamp + 3, "Wrong timestamp");
$this->assertEquals((string) $position->username, $this->testAdminUser, "Wrong username");
$this->assertEquals((string) $position->trackname, $this->testTrackName, "Wrong trackname");
$position = $xml->position[1];
$this->assertEquals((int) $position["id"], 3, "Wrong position id");
$position = $json[1];
$this->assertEquals((int) $position->id, 3, "Wrong position id");
$this->assertEquals((float) $position->latitude, $this->testLat, "Wrong latitude");
$this->assertEquals((float) $position->longitude, $this->testLon, "Wrong longitude");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals(0, $xml->position->count(), "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertCount(0, $json, "Wrong count of positions");
}
public function testGetPositionsNoUserId() {
@ -272,10 +272,9 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->get("/utils/getpositions.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals(0, $xml->position->count(), "Wrong count of positions");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertCount(0, $json, "Wrong count of positions");
}
public function testGetPositionsNoAuth() {
@ -291,10 +290,10 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->get("/utils/getpositions.php", $options);
$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->assertEquals($xml->position->count(), 0, "Wrong count of positions");
$this->assertNotNull($json, "JSON object is null");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->track->count(), 2, "Wrong count of tracks");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 2, "Wrong count of tracks");
$track = $xml->track[0];
$this->assertEquals((int) $track->trackid, $this->testTrackId2, "Wrong track id");
$this->assertEquals((string) $track->trackname, $this->testTrackName . "2", "Wrong track name");
$track = $json[0];
$this->assertEquals((int) $track->id, $this->testTrackId2, "Wrong track id");
$this->assertEquals((string) $track->name, $this->testTrackName . "2", "Wrong track name");
$track = $xml->track[1];
$this->assertEquals((int) $track->trackid, $this->testTrackId, "Wrong track id");
$this->assertEquals((string) $track->trackname, $this->testTrackName, "Wrong track name");
$track = $json[1];
$this->assertEquals((int) $track->id, $this->testTrackId, "Wrong track id");
$this->assertEquals((string) $track->name, $this->testTrackName, "Wrong track name");
}
public function testGetTracksUser() {
@ -347,18 +346,18 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->get("/utils/gettracks.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->track->count(), 2, "Wrong count of tracks");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 2, "Wrong count of tracks");
$track = $xml->track[0];
$this->assertEquals((int) $track->trackid, $this->testTrackId2, "Wrong track id");
$this->assertEquals((string) $track->trackname, $this->testTrackName . "2", "Wrong track name");
$track = $json[0];
$this->assertEquals((int) $track->id, $this->testTrackId2, "Wrong track id");
$this->assertEquals((string) $track->name, $this->testTrackName . "2", "Wrong track name");
$track = $xml->track[1];
$this->assertEquals((int) $track->trackid, $this->testTrackId, "Wrong track id");
$this->assertEquals((string) $track->trackname, $this->testTrackName, "Wrong track name");
}
$track = $json[1];
$this->assertEquals((int) $track->id, $this->testTrackId, "Wrong track id");
$this->assertEquals((string) $track->name, $this->testTrackName, "Wrong track name");
}
public function testGetTracksOtherUser() {
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals($xml->track->count(), 0, "Wrong count of tracks");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 0, "Wrong count of tracks");
}
public function testGetTracksNoUserId() {
@ -397,9 +396,9 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->get("/utils/gettracks.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals($xml->track->count(), 0, "Wrong count of tracks");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 0, "Wrong count of tracks");
}
public function testGetTracksNoAuth() {
@ -416,9 +415,9 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->get("/utils/gettracks.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals($xml->track->count(), 0, "Wrong count of tracks");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(count($json), 0, "Wrong count of tracks");
}
@ -428,15 +427,19 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"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);
$this->assertEquals(401, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, "Unauthorized", "Wrong error message");
$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, "Unauthorized", "Wrong error message");
}
public function testChangePassEmpty() {
@ -449,13 +452,13 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->post("/utils/changepass.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, "Empty password", "Wrong error message");
$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 password", "Wrong error message");
}
public function testChangePassNoUser() {
public function testChangePassUserUnknown() {
$this->assertTrue($this->authenticate(), "Authentication failed");
$options = [
@ -468,10 +471,28 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->post("/utils/changepass.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, "User unknown", "Wrong error message");
$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, "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() {
@ -480,6 +501,7 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testAdminUser,
"oldpass" => "badpass",
"pass" => "newpass",
],
@ -487,10 +509,10 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->post("/utils/changepass.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, "Wrong old password", "Wrong error message");
$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, "Wrong old password", "Wrong error message");
}
public function testChangePassNoOldpass() {
@ -499,16 +521,17 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testAdminUser,
"pass" => "newpass",
],
];
$response = $this->http->post("/utils/changepass.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, "Wrong old password", "Wrong error message");
$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, "Wrong old password", "Wrong error message");
}
public function testChangePassSelfAdmin() {
@ -519,6 +542,7 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testAdminUser,
"oldpass" => $this->testAdminPass,
"pass" => $newPass,
],
@ -526,9 +550,8 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->post("/utils/changepass.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertTrue(password_verify($newPass, $this->pdoGetColumn("SELECT password FROM users")), "Wrong actual password hash");
}
@ -541,6 +564,7 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testUser,
"oldpass" => $this->testPass,
"pass" => $newPass,
],
@ -548,9 +572,8 @@ class InternalAPITest extends UloggerAPITestCase {
$response = $this->http->post("/utils/changepass.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is not false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, "Unauthorized", "Wrong error message");
$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, "Unauthorized", "Wrong error message");
}
/* handletrack.php */
@ -617,9 +639,8 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handletrack.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$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");
}
@ -640,9 +661,8 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handletrack.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$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");
}
@ -663,10 +683,10 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handletrack.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
}
public function testHandleTrackUpdate() {
@ -686,9 +706,8 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handletrack.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 0, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(2, $this->getConnection()->getRowCount("tracks"), "Wrong row count");
$row1 = [
"id" => $trackId2,
@ -727,10 +746,10 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handletrack.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
}
public function testHandleTrackMissingAction() {
@ -767,10 +786,10 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handletrack.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
}
@ -785,10 +804,10 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handleuser.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
}
public function testHandleUserNonAdmin() {
@ -803,10 +822,10 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handleuser.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals((int) $xml->error, 1, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$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, $lang["servererror"], "Wrong error message");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(2, $this->getConnection()->getRowCount("users"), "Wrong row count");
$expected = [
"login" => $this->testUser,
@ -903,10 +921,10 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handleuser.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["userexists"], "Wrong error message");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(1, (int) $json->error, "Wrong error status");
$this->assertEquals((string) $json->message, $lang["userexists"], "Wrong error message");
$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);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$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");
}
@ -941,10 +958,10 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handleuser.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals(1, (int) $xml->error, "Wrong error status");
$this->assertEquals((string) $xml->message, $lang["servererror"], "Wrong error message");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$this->assertEquals(1, (int) $json->error, "Wrong error status");
$this->assertEquals((string) $json->message, $lang["servererror"], "Wrong error message");
$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");
}
@ -960,25 +977,11 @@ class InternalAPITest extends UloggerAPITestCase {
];
$response = $this->http->post("/utils/handleuser.php", $options);
$this->assertEquals(200, $response->getStatusCode(), "Unexpected status code");
$xml = $this->getXMLfromResponse($response);
$this->assertTrue($xml !== false, "XML object is false");
$this->assertEquals(0, (int) $xml->error, "Wrong error status");
$json = json_decode($response->getBody());
$this->assertNotNull($json, "JSON object is null");
$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;
}
}
?>

View File

@ -11,15 +11,15 @@ class PositionTest extends UloggerDatabaseTestCase {
$trackId = $this->addTestTrack($userId);
$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->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->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");
$expected = [
"id" => $posId,
@ -34,11 +34,11 @@ class PositionTest extends UloggerDatabaseTestCase {
"accuracy" => $this->testAccuracy,
"provider" => $this->testProvider,
"comment" => $this->testComment,
"image_id" => $this->testImageId
"image" => $this->testImage
];
$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_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");

View File

@ -41,16 +41,16 @@ class TrackTest extends UloggerDatabaseTestCase {
$this->assertEquals(1, $this->getConnection()->getRowCount('tracks'), "Wrong row count");
$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->assertFalse($posId, "Adding position with nonexistant track should fail");
$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->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");
$expected = [
"id" => $posId,
@ -65,11 +65,11 @@ class TrackTest extends UloggerDatabaseTestCase {
"accuracy" => $this->testAccuracy,
"provider" => $this->testProvider,
"comment" => $this->testComment,
"image_id" => $this->testImageId
"image" => $this->testImage
];
$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_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");

View File

@ -43,6 +43,7 @@ before_install:
;;
esac
- composer install
- npm install
- until netstat -atn 2>/dev/null | grep '8080.*LISTEN'; do sleep 1; done
after_success:
@ -56,9 +57,14 @@ after_success:
tx push -s --no-interactive
fi
after_failure:
- docker logs ulogger
script:
- ./vendor/bin/phpunit -c .tests/phpunit.xml
- npm test
- npm run lint:js
- npm run lint:css
addons:
coverity_scan:

View File

@ -37,7 +37,6 @@ RUN chown nginx.nginx /etc/nginx/conf.d/default.conf
RUN rm -rf /var/www/html
RUN mkdir -p /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}"

View File

@ -21,7 +21,6 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews
- user authentication
- Google Maps
- OpenLayers (OpenStreet and other layers)
- ajax
- user preferences stored in cookies
- simple admin menu
- export tracks to gpx and kml
@ -30,6 +29,8 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews
## Install
- Download zipped archive or clone the repository on your computer
- 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 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`
@ -51,6 +52,7 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews
## Tests
- Install tests dependecies.
- `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.
- `docker build -t 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_PASS=secret2`
- `ULOGGER_URL="http://127.0.0.1:8080"`
- Run tests
- PHP tests
- `./vendor/bin/phpunit -c .tests/phpunit.xml`
## Todo
- improve track editing
- track display filters (accurracy, provider)
- improve interface on mobile devices
- JS tests
- `npm test`
- Other tests
- `npm run lint:js`
- `npm run lint:css`
## Translations
- translations may be contributed via [Transifex](https://www.transifex.com/bfabiszewski/ulogger/).

View File

@ -55,7 +55,7 @@
exitWithError("Unauthorized");
}
switch ($action) {
switch ($action) {
// action: authorize
case "auth":
$login = uUtils::postString('user');
@ -111,16 +111,21 @@
$accuracy = uUtils::postInt('accuracy');
$provider = uUtils::postString('provider');
$comment = uUtils::postString('comment');
$imageId = uUtils::postInt('imageid');
$imageMeta = uUtils::requestFile('image');
$trackId = uUtils::postInt('trackid');
if (!is_float($lat) || !is_float($lon) || !is_int($timestamp) || !is_int($trackId)) {
exitWithError("Missing required parameter");
}
$image = null;
if (!empty($imageMeta)) {
$image = uUpload::add($imageMeta, $trackId);
}
require_once(ROOT_DIR . "/helpers/position.php");
$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) {
exitWithError("Server error");

40
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "e0409bcb302c1bef7caa031dafc841a9",
"content-hash": "ffb1c6d77d755002ea20d1c1c6338b43",
"packages": [
{
"name": "ulrichsg/getopt-php",
@ -1656,16 +1656,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.10.0",
"version": "v1.13.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
"shasum": ""
},
"require": {
@ -1677,7 +1677,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
"dev-master": "1.13-dev"
}
},
"autoload": {
@ -1693,13 +1693,13 @@
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
},
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
@ -1710,20 +1710,20 @@
"polyfill",
"portable"
],
"time": "2018-08-06T14:22:27+00:00"
"time": "2019-11-27T13:56:44+00:00"
},
{
"name": "symfony/yaml",
"version": "v3.4.22",
"version": "v3.4.36",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d"
"reference": "dab657db15207879217fc81df4f875947bf68804"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ba11776e9e6c15ad5759a07bffb15899bac75c2d",
"reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d",
"url": "https://api.github.com/repos/symfony/yaml/zipball/dab657db15207879217fc81df4f875947bf68804",
"reference": "dab657db15207879217fc81df4f875947bf68804",
"shasum": ""
},
"require": {
@ -1769,7 +1769,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2019-01-16T10:59:17+00:00"
"time": "2019-10-24T15:33:53+00:00"
},
{
"name": "vlucas/phpdotenv",
@ -1880,6 +1880,12 @@
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform": {
"ext-json": "*",
"ext-pdo": "*",
"ext-xmlwriter": "*",
"ext-simplexml": "*",
"ext-libxml": "*"
},
"platform-dev": []
}

1
css/chartist.min.css vendored Normal file

File diff suppressed because one or more lines are too long

224
css/fonts.css Normal file
View 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;
}

View File

@ -19,238 +19,360 @@
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
padding: 0;
background-color: #666;
}
a {
color: #bce;
cursor: pointer;
text-decoration: none;
color: #bce;
}
:link, :visited {
color: #bce;
}
select {
width: 150px;
font-weight: normal;
width: 150px;
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,
#login input {
width: 150px;
text-align: center;
border: 1px solid black;
}
#menu input[type = "submit"],
#login input[type = "submit"] {
background-color: black;
#menu input[type="submit"],
#login input[type="submit"] {
color: white;
border: 1px solid white;
}
#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;
background-color: black;
}
#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;
}
#summary div {
padding-top: .3em;
.section:first-child {
padding-top: 1em;
}
#input-file {
display: none;
}
#summary div {
padding-top: 0.3em;
}
#summary div img {
margin-bottom: -2px;
}
#login {
font-family: 'Open Sans', Verdana, sans-serif;
font-family: "Open Sans", Verdana, sans-serif;
font-size: 0.8em;
position: relative;
top: 10%;
background-color: #444;
width: 30%;
min-width: 200px;
margin: auto;
padding: 30px;
font-size: 0.8em;
text-align: center;
color: white;
background-color: #444;
}
#title {
font-size: 1.3em;
padding-bottom: 0.5em;
padding-top: 0.6em;
padding-bottom: 0.5em;
}
#subtitle {
padding-bottom: 2em;
}
#error {
padding-top: 1.2em;
color: yellow;
}
#popup {
font-family: 'Open Sans', Verdana, sans-serif;
font-family: "Open Sans", Verdana, sans-serif;
max-width: 25em;
background-color: #666;
}
#pheader {
font-size: 0.9rem;
float: left;
font-size: .9rem;
color: #297b9a;
padding-bottom: .5rem;
padding-bottom: 0.5rem;
color: #bce;
}
#pheader div {
float: left;
padding-right: 2em;
}
#pbody {
clear: both;
padding-top: .2rem;
border-top: 1px solid #6cdae7;;
font-size: .8rem;
white-space: nowrap;
#pheader div img {
background-image: radial-gradient(circle closest-side, #bfbfbc, #666);
}
#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 {
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 {
display: inline-block;
padding-top: 5px;
padding-right: 20px;
}
#pbody .smaller {
color: gray;
font-size: .9em;
}
#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;
#pleft img {
background-image: radial-gradient(circle closest-side, #bfbfbc, #666);
}
#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;
}
.mi {
color:white;
padding-right:0.1em;
font-style:italic;
font-style: italic;
padding-right: 0.1em;
color: white;
}
#modal {
font-family: 'Open Sans', Verdana, sans-serif;
display: block;
font-family: "Open Sans", Verdana, sans-serif;
position: fixed;
z-index: 10010;
left: 0;
top: 0;
left: 0;
display: block;
overflow: auto;
width: 100%;
height: 100%;
overflow: auto;
background-color: black; /* fallback */
background-color: rgba(0,0,0,0.4);
padding-top: 10%;
background-color: black; /* fallback */
background-color: rgba(0, 0, 0, 0.4);
}
#modal-header {
top: 20px;
position: relative;
text-align: right;
margin: 0 auto;
top: 20px;
width: 40%;
min-width: 300px;
margin: 0 auto;
text-align: right;
}
#modal-header button {
background-color: rgba(0, 0, 0, 0);
border: none;
background-color: rgba(0, 0, 0, 0);
}
#modal-body {
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%;
min-width: 300px;
margin: 0 auto 15% auto;
padding: 1em;
border-radius: 10px;
color: white;
border: 1px solid #888;
-moz-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 {
@ -258,27 +380,53 @@ select {
}
#modal input[type=text], #modal input[type=password] {
width: 100%;
padding: 0.4em;
margin: 0.8em 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
border-radius: 5px;
width: 100%;
margin: 0.8em 0;
padding: 0.4em;
border: 1px solid #ccc;
-moz-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 {
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;
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 {
@ -286,131 +434,205 @@ button {
}
.red-button {
color: white;
float: right;
background-color: red;
padding: .1em .4em;
border-radius: 10px;
padding: 0.1em 0.4em;
color: white;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
background-color: red;
}
.dropdown {
display: none;
#user-menu {
position: absolute;
background-color: gray;
padding: 1em;
width: 130px;
border: 1px solid #888;
}
.dropdown a {
display: block;
padding-bottom: .5em;
padding-top: .5em;
width: 130px;
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 {
animation: blink 1s linear infinite;
}
@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 */
.ol-popup {
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;
left: -50px;
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%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
.ol-popup::after, .ol-popup::before {
position: absolute;
top: 100%;
width: 0;
height: 0;
content: " ";
pointer-events: none;
border: solid transparent;
}
.ol-popup:after {
border-top-color: white;
border-width: 10px;
.ol-popup::after {
left: 48px;
margin-left: -10px;
border-width: 10px;
border-top-color: #666;
}
.ol-popup:before {
border-top-color: #cccccc;
border-width: 11px;
.ol-popup::before {
left: 48px;
margin-left: -11px;
border-width: 11px;
border-top-color: #ccc;
}
.ol-popup-closer {
text-decoration: none;
position: absolute;
top: 2px;
right: 8px;
top: -5px;
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 {
display: none;
position: absolute;
bottom: 12px;
left: 10px;
display: none;
min-width: 200px;
}
.ol-layerswitcher {
font-family: sans-serif;
font-size: 0.9em;
font-weight: bold;
margin: 1px;
padding: 0.5em;
color: #fff;
background-color: rgba(0, 60, 136, .5);
border: none;
border-radius: 2px;
font-family: sans-serif;
font-weight: bold;
font-size: .9em;
padding: 0.5em;
background-color: rgba(0, 60, 136, 0.5);
}
.ol-layerswitcher:hover {
background-color: rgba(0, 60, 136, .7)
background-color: rgba(0, 60, 136, 0.7);
}
.ol-layerswitcher label {
display: block;
clear: both;
margin: .5em 0;
margin: 0.5em 0;
cursor: pointer;
}
.ol-layerswitcher label:hover {
color: #c8dcf2;
}
.ol-layerswitcher input {
margin-right: 1em;
}
label.ol-datalayer {
margin-top: 1.5em;
}
.ol-datalayer ~ .ol-datalayer {
margin-top: .5em;
margin-top: 0.5em;
}
.ol-switcher-button {
top: 6.6em;
left: .5em;
left: 0.5em;
}
.ol-touch .ol-switcher-button {
top: 10em;
}
}

202
fonts/OpenSans.LICENSE Normal file
View 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
View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -27,6 +27,7 @@
*/
class uAuth {
/** @var bool Is user authenticated */
private $isAuthenticated = false;
/** @var null|uUser */
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
*

View File

@ -27,59 +27,108 @@
* Handles config values
*/
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";
// gmaps key
/**
* @var string|null Google maps key
*/
static $gkey = null;
// openlayers additional map layers
/**
* @var array Openlayers additional map layers
*/
static $ol_layers = [];
// default coordinates for initial map
/**
* @var float Default latitude for initial map
*/
static $init_latitude = 52.23;
/**
* @var float Default longitude for initial map
*/
static $init_longitude = 21.01;
// MySQL config
static $dbdsn = ""; // database dsn
static $dbuser = ""; // database user
static $dbpass = ""; // database pass
static $dbprefix = ""; // optional table names prefix, eg. "ulogger_"
/**
* @var string Database dsn
*/
static $dbdsn = "";
/**
* @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;
// all users tracks are visible to authenticated user
/**
* @var bool All users tracks are visible to authenticated user
*/
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 = "";
// miniumum required length of user password
/**
* @var int Miniumum required length of user password
*/
static $pass_lenmin = 12;
// required strength of user password
// 0 = no requirements,
// 1 = require mixed case letters (lower and upper),
// 2 = require mixed case and numbers
// 3 = require mixed case, numbers and non-alphanumeric characters
/**
* @var int Required strength of user password
* 0 = no requirements,
* 1 = require mixed case letters (lower and upper),
* 2 = require mixed case and numbers
* 3 = require mixed case, numbers and non-alphanumeric characters
*/
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;
// Default language
/**
* @var string Default language code
*/
static $lang = "en";
// units
/**
* @var string Default units
*/
static $units = "metric";
/**
* @var int Stroke weight
*/
static $strokeWeight = 2;
/**
* @var string Stroke color
*/
static $strokeColor = '#ff0000';
/**
* @var int Stroke opacity
*/
static $strokeOpacity = 1;
private static $fileLoaded = false;
@ -109,7 +158,7 @@
include_once($configFile);
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($init_latitude)) { self::$init_latitude = $init_latitude; }
if (isset($init_longitude)) { self::$init_longitude = $init_longitude; }

View File

@ -17,8 +17,9 @@
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
require_once(ROOT_DIR . "/helpers/db.php");
require_once(ROOT_DIR . "/helpers/track.php");
require_once(ROOT_DIR . "/helpers/db.php");
require_once(ROOT_DIR . "/helpers/track.php");
require_once(ROOT_DIR . "/helpers/upload.php");
/**
* Positions handling
@ -51,9 +52,9 @@
/** @param String Provider */
public $provider;
/** @param String Comment */
public $comment; // not used yet
/** @param int Image id */
public $imageId; // not used yet
public $comment;
/** @param String Image path */
public $image;
public $isValid = false;
@ -66,11 +67,11 @@
if (!empty($positionId)) {
$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.comment, p.image_id, u.login, t.name
p.comment, p.image, u.login, t.name
FROM " . self::db()->table('positions') . " p
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)
WHERE id = ? LIMIT 1";
WHERE p.id = ? LIMIT 1";
$params = [ $positionId ];
try {
$this->loadWithQuery($query, $params);
@ -90,6 +91,15 @@
return uDb::getInstance();
}
/**
* Has image
*
* @return bool True if has image
*/
public function hasImage() {
return !empty($this->image);
}
/**
* Add position
*
@ -104,27 +114,27 @@
* @param int $accuracy Optional
* @param string $provider Optional
* @param string $comment Optional
* @param int $imageId Optional
* @param int $image Optional
* @return int|bool New position id in database, false on error
*/
public static function add($userId, $trackId, $timestamp, $lat, $lon,
$altitude = NULL, $speed = NULL, $bearing = NULL, $accuracy = NULL,
$provider = NULL, $comment = NULL, $imageId = NULL) {
$provider = NULL, $comment = NULL, $image = NULL) {
$positionId = false;
if (is_numeric($lat) && is_numeric($lon) && is_numeric($timestamp) && is_numeric($userId) && is_numeric($trackId)) {
$track = new uTrack($trackId);
if ($track->isValid && $track->userId == $userId) {
if ($track->isValid && $track->userId === $userId) {
try {
$table = self::db()->table('positions');
$query = "INSERT INTO $table
(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('?') . ", ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = self::db()->prepare($query);
$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);
$positionId = self::db()->lastInsertId("${table}_id_seq");
$positionId = (int) self::db()->lastInsertId("${table}_id_seq");
} catch (PDOException $e) {
// TODO: handle error
syslog(LOG_ERR, $e->getMessage());
@ -134,6 +144,70 @@
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
*
@ -151,6 +225,7 @@
$where .= " AND track_id = ?";
$args[] = $trackId;
}
self::removeImages($userId, $trackId);
try {
$query = "DELETE FROM " . self::db()->table('positions') . " $where";
$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,
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
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)
@ -205,7 +280,7 @@
public static function getLastAllUsers() {
$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.comment, p.image_id, u.login, t.name
p.comment, p.image, u.login, t.name
FROM " . self::db()->table('positions') . " p
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)
@ -224,25 +299,30 @@
} catch (PDOException $e) {
// TODO: handle exception
syslog(LOG_ERR, $e->getMessage());
$positionsArr = false;
}
return $positionsArr;
}
/**
* Get array of all positions
*
* @param int $userId Optional limit to given user id
* @param int $trackId Optional limit to given track id
* @return uPosition[]|bool Array of uPosition positions, false on error
*/
public static function getAll($userId = NULL, $trackId = NULL) {
$rules = [];
/**
* Get array of all positions
*
* @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 getAll($userId = NULL, $trackId = NULL, $afterId = NULL, $rules = []) {
if (!empty($userId)) {
$rules[] = "p.user_id = " . self::db()->quote($userId);
}
if (!empty($trackId)) {
$rules[] = "p.track_id = " . self::db()->quote($trackId);
}
if (!empty($afterId)) {
$rules[] = "p.id > " . self::db()->quote($afterId);
}
if (!empty($rules)) {
$where = "WHERE " . implode(" AND ", $rules);
} else {
@ -250,7 +330,7 @@
}
$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.comment, p.image_id, u.login, t.name
p.comment, p.image, u.login, t.name
FROM " . self::db()->table('positions') . " p
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)
@ -265,10 +345,53 @@
} catch (PDOException $e) {
// TODO: handle exception
syslog(LOG_ERR, $e->getMessage());
$positionsArr = false;
}
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
*
@ -282,7 +405,7 @@
$lon2 = deg2rad($target->longitude);
$latD = $lat2 - $lat1;
$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;
}
@ -318,7 +441,7 @@
$position->accuracy = $row['accuracy'];
$position->provider = $row['provider'];
$position->comment = $row['comment'];
$position->imageId = $row['image_id'];
$position->image = $row['image'];
$position->isValid = true;
return $position;
}
@ -346,7 +469,7 @@
$stmt->bindColumn('accuracy', $this->accuracy, PDO::PARAM_INT);
$stmt->bindColumn('provider', $this->provider);
$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('name', $this->trackName);
if ($stmt->fetch(PDO::FETCH_BOUND)) {

View File

@ -84,7 +84,7 @@
$stmt = self::db()->prepare($query);
$params = [ $userId, $name, $comment ];
$stmt->execute($params);
$trackId = self::db()->lastInsertId("${table}_id_seq");
$trackId = (int) self::db()->lastInsertId("${table}_id_seq");
} catch (PDOException $e) {
// TODO: handle exception
syslog(LOG_ERR, $e->getMessage());
@ -158,7 +158,7 @@
$ret = false;
if (empty($name)) { $name = $this->name; }
if (is_null($comment)) { $comment = $this->comment; }
if ($comment == "") { $comment = NULL; }
if ($comment === "") { $comment = NULL; }
if ($this->isValid) {
try {
$query = "UPDATE " . self::db()->table('tracks') . " SET name = ?, comment = ? WHERE id = ?";
@ -184,21 +184,17 @@
*/
public static function deleteAll($userId) {
$ret = false;
if (!empty($userId)) {
// remove all positions
if (uPosition::deleteAll($userId) === true) {
// remove all tracks
try {
$query = "DELETE FROM " . self::db()->table('tracks') . " WHERE user_id = ?";
$stmt = self::db()->prepare($query);
$stmt->execute([ $userId ]);
$ret = true;
} catch (PDOException $e) {
// TODO: handle exception
syslog(LOG_ERR, $e->getMessage());
}
if (!empty($userId) && uPosition::deleteAll($userId) === true) {
// remove all tracks
try {
$query = "DELETE FROM " . self::db()->table('tracks') . " WHERE user_id = ?";
$stmt = self::db()->prepare($query);
$stmt->execute([ $userId ]);
$ret = true;
} catch (PDOException $e) {
// TODO: handle exception
syslog(LOG_ERR, $e->getMessage());
}
}
return $ret;
}

166
helpers/upload.php Normal file
View 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;
}
}

View File

@ -84,7 +84,7 @@
$query = "INSERT INTO $table (login, password) VALUES (?, ?)";
$stmt = self::db()->prepare($query);
$stmt->execute([ $login, $hash ]);
$userid = self::db()->lastInsertId("${table}_id_seq");
$userid = (int) self::db()->lastInsertId("${table}_id_seq");
} catch (PDOException $e) {
// TODO: handle exception
syslog(LOG_ERR, $e->getMessage());
@ -140,6 +140,7 @@
$stmt = self::db()->prepare($query);
$stmt->execute([ $hash, $this->login ]);
$ret = true;
$this->hash = $hash;
} catch (PDOException $e) {
// TODO: handle exception
syslog(LOG_ERR, $e->getMessage());
@ -194,7 +195,7 @@
/**
* 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() {
try {

View File

@ -32,7 +32,7 @@
$upload_max_filesize = self::iniGetBytes('upload_max_filesize');
$post_max_size = self::iniGetBytes('post_max_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 = -1 means no limit
if ($memory_limit < 0) { $memory_limit = $post_max_size; }
@ -45,10 +45,11 @@
*
* @param string $iniParam Ini parameter name
* @return int Bytes
* @noinspection PhpMissingBreakStatementInspection
*/
private static function iniGetBytes($iniParam) {
$iniStr = ini_get($iniParam);
$val = floatval($iniStr);
$val = (float) $iniStr;
$suffix = substr(trim($iniStr), -1);
if (ctype_alpha($suffix)) {
switch (strtolower($suffix)) {
@ -89,22 +90,17 @@
* @param array|null $extra Optional array of extra parameters
*/
private static function exitWithStatus($isError, $extra = NULL) {
header("Content-type: text/xml");
$xml = new XMLWriter();
$xml->openURI("php://output");
$xml->startDocument("1.0");
$xml->setIndent(true);
$xml->startElement("root");
$xml->writeElement("error", (int) $isError);
$output = [];
if ($isError) {
$output["error"] = true;
}
if (!empty($extra)) {
foreach ($extra as $key => $value) {
$xml->writeElement($key, $value);
$output[$key] = $value;
}
}
$xml->endElement();
$xml->endDocument();
$xml->flush();
header("Content-type: application/json");
echo json_encode($output);
exit;
}
@ -115,9 +111,9 @@
* @return string URL
*/
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
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://";
}
$host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : "";
@ -165,29 +161,47 @@
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) {
if (is_string(($val = self::requestValue($name, $default, $type)))) {
return trim($val);
} else {
return $val;
}
return $val;
}
private static function requestInt($name, $default, $type) {
if (is_float(($val = self::requestValue($name, $default, $type, FILTER_VALIDATE_FLOAT)))) {
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) {
$input = filter_input($type, $name, $filters, $flags);
if ($input !== false && !is_null($input)) {
if ($input !== false && $input !== null) {
return $input;
} else {
return $default;
}
return $default;
}
}

1
images/bearing.svg Normal file
View 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

View File

@ -1,4 +1,4 @@
<svg
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>

Before

Width:  |  Height:  |  Size: 253 B

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 B

1
images/position.svg Normal file
View 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

View File

@ -1,4 +1,4 @@
<svg
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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,4 +1,4 @@
<svg
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>

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 301 B

View File

@ -1,4 +1,4 @@
<svg
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>

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 840 B

271
index.php
View File

@ -17,12 +17,12 @@
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
require_once(__DIR__ . "/helpers/auth.php");
require_once(ROOT_DIR . "/helpers/config.php");
require_once(ROOT_DIR . "/helpers/position.php");
require_once(ROOT_DIR . "/helpers/track.php");
require_once(ROOT_DIR . "/helpers/utils.php");
require_once(ROOT_DIR . "/helpers/lang.php");
require_once(__DIR__ . '/helpers/auth.php');
require_once(ROOT_DIR . '/helpers/config.php');
require_once(ROOT_DIR . '/helpers/position.php');
require_once(ROOT_DIR . '/helpers/track.php');
require_once(ROOT_DIR . '/helpers/utils.php');
require_once(ROOT_DIR . '/helpers/lang.php');
$login = uUtils::postString('user');
$pass = uUtils::postPass('pass');
@ -32,202 +32,127 @@
$langsArr = uLang::getLanguages();
$auth = new uAuth();
if ($action == "auth") {
if ($action === 'auth') {
$auth->checkLogin($login, $pass);
}
if (!$auth->isAuthenticated() && $action == "auth") {
$auth->exitWithRedirect("login.php?auth_error=1");
if ($action === 'auth' && !$auth->isAuthenticated()) {
$auth->exitWithRedirect('login.php?auth_error=1');
}
if (!$auth->isAuthenticated() && uConfig::$require_authentication) {
$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;
if (uConfig::$require_authentication && !$auth->isAuthenticated()) {
$auth->exitWithRedirect('login.php');
}
?>
<!DOCTYPE html>
<html>
<html lang="<?= uConfig::$lang ?>">
<head>
<title><?= $lang["title"] ?></title>
<?php include("meta.php"); ?>
<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>
<title><?= $lang['title'] ?></title>
<?php include('meta.php'); ?>
<script src="js/dist/bundle.js"></script>
</head>
<body onload="loadMapAPI();">
<div id="menu">
<div id="menu-content">
<body>
<div id="container">
<div id="menu">
<div id="menu-content">
<?php if ($auth->isAuthenticated()): ?>
<div id="user_menu">
<a href="javascript:void(0);" onclick="userMenu()"><img class="icon" alt="<?= $lang["user"] ?>" src="images/user.svg"> <?= htmlspecialchars($auth->user->login) ?></a>
<div id="user_dropdown" class="dropdown">
<a href="javascript:void(0)" onclick="changePass()"><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>
<?php if ($auth->isAuthenticated()): ?>
<div>
<a data-bind="onShowUserMenu"><img class="icon" alt="<?= $lang['user'] ?>" src="images/user.svg"> <?= htmlspecialchars($auth->user->login) ?></a>
<div id="user-menu" class="menu-hidden">
<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>
</div>
</div>
</div>
<?php else: ?>
<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 else: ?>
<a href="login.php"><img class="icon" alt="<?= $lang['login'] ?>" src="images/key.svg"> <?= $lang['login'] ?></a>
<?php endif; ?>
</div>
<div id="track">
<div class="menutitle"><?= $lang["track"] ?></div>
<form>
<select name="track" onchange="selectTrack(this)">
<?php foreach ($tracksArr as $aTrack): ?>
<option value="<?= $aTrack->id ?>"><?= htmlspecialchars($aTrack->name) ?></option>
<?php endforeach; ?>
<div class="section">
<label for="user"><?= $lang['user'] ?></label>
<select id="user" data-bind="currentUserId" name="user"></select>
</div>
<div class="section">
<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>
<input id="latest" type="checkbox" onchange="toggleLatest();"> <?= $lang["latest"] ?><br>
<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>
<div id="summary"></div>
<div id="other">
<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);">
<div>
<label for="lang"><?= $lang['language'] ?></label>
<select id="lang" name="lang" data-bind="lang">
<?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; ?>
</select>
</form>
</div>
</div>
<div id="units">
<div class="menutitle"><?= $lang["units"] ?></div>
<form>
<select name="units" onchange="setUnits(this.options[this.selectedIndex].value);">
<option value="metric"<?= (uConfig::$units == "metric") ? " selected" : "" ?>><?= $lang["metric"] ?></option>
<option value="imperial"<?= (uConfig::$units == "imperial") ? " selected" : "" ?>><?= $lang["imperial"] ?></option>
<option value="nautical"<?= (uConfig::$units == "nautical") ? " selected" : "" ?>><?= $lang["nautical"] ?></option>
<div class="section">
<label for="units"><?= $lang['units'] ?></label>
<select id="units" name="units" data-bind="units">
<option value="metric"<?= (uConfig::$units === 'metric') ? ' selected' : '' ?>><?= $lang['metric'] ?></option>
<option value="imperial"<?= (uConfig::$units === 'imperial') ? ' selected' : '' ?>><?= $lang['imperial'] ?></option>
<option value="nautical"<?= (uConfig::$units === 'nautical') ? ' selected' : '' ?>><?= $lang['nautical'] ?></option>
</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 id="admin_menu">
<div class="menutitle u"><?= $lang["adminmenu"] ?></div>
<?php if ($auth->isAdmin()): ?>
<a class="menulink" href="javascript:void(0);" onclick="addUser()"><?= $lang["adduser"] ?></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 class="section">
<div class="menu-title"><?= $lang['export'] ?></div>
<a id="export-kml" class="menu-link" data-bind="onExportKml">kml</a>
<a id="export-gpx" class="menu-link" data-bind="onExportGpx">gpx</a>
</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 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="map-canvas"></div>
<div id="bottom">
<div id="chart"></div>
<div id="close"><a href="javascript:void(0);" onclick="toggleChart(0);"><?= $lang["close"] ?></a></div>
<div id="main">
<div id="map-canvas"></div>
<div id="bottom">
<div id="chart"></div>
<a id="chart-close" data-bind="onChartToggle"><?= $lang['close'] ?></a>
</div>
</div>
</div>
</div>
</body>
</html>

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// 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, '');
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/**
* 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;
}
}

Some files were not shown because too many files have changed in this diff Show More