es6 initial changes
This commit is contained in:
parent
7f1170187c
commit
1339682b66
@ -426,7 +426,11 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [ "userid" => $this->testUserId ],
|
"form_params" => [
|
||||||
|
"login" => $this->testUser,
|
||||||
|
"pass" => $this->testPass,
|
||||||
|
"oldpass" => $this->testPass
|
||||||
|
],
|
||||||
];
|
];
|
||||||
$response = $this->http->post("/utils/changepass.php", $options);
|
$response = $this->http->post("/utils/changepass.php", $options);
|
||||||
$this->assertEquals(401, $response->getStatusCode(), "Unexpected status code");
|
$this->assertEquals(401, $response->getStatusCode(), "Unexpected status code");
|
||||||
@ -453,7 +457,7 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$this->assertEquals((string) $xml->message, "Empty password", "Wrong error message");
|
$this->assertEquals((string) $xml->message, "Empty password", "Wrong error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testChangePassNoUser() {
|
public function testChangePassUserUnknown() {
|
||||||
$this->assertTrue($this->authenticate(), "Authentication failed");
|
$this->assertTrue($this->authenticate(), "Authentication failed");
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
@ -472,12 +476,31 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$this->assertEquals((string) $xml->message, "User unknown", "Wrong error message");
|
$this->assertEquals((string) $xml->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");
|
||||||
|
|
||||||
|
$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 login", "Wrong error message");
|
||||||
|
}
|
||||||
|
|
||||||
public function testChangePassWrongOldpass() {
|
public function testChangePassWrongOldpass() {
|
||||||
$this->assertTrue($this->authenticate(), "Authentication failed");
|
$this->assertTrue($this->authenticate(), "Authentication failed");
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testAdminUser,
|
||||||
"oldpass" => "badpass",
|
"oldpass" => "badpass",
|
||||||
"pass" => "newpass",
|
"pass" => "newpass",
|
||||||
],
|
],
|
||||||
@ -497,6 +520,7 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testAdminUser,
|
||||||
"pass" => "newpass",
|
"pass" => "newpass",
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@ -517,6 +541,7 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testAdminUser,
|
||||||
"oldpass" => $this->testAdminPass,
|
"oldpass" => $this->testAdminPass,
|
||||||
"pass" => $newPass,
|
"pass" => $newPass,
|
||||||
],
|
],
|
||||||
@ -539,6 +564,7 @@ class InternalAPITest extends UloggerAPITestCase {
|
|||||||
$options = [
|
$options = [
|
||||||
"http_errors" => false,
|
"http_errors" => false,
|
||||||
"form_params" => [
|
"form_params" => [
|
||||||
|
"login" => $this->testUser,
|
||||||
"oldpass" => $this->testPass,
|
"oldpass" => $this->testPass,
|
||||||
"pass" => $newPass,
|
"pass" => $newPass,
|
||||||
],
|
],
|
||||||
|
1
css/chartist.min.css
vendored
Normal file
1
css/chartist.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -290,6 +290,10 @@ button {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
#cancel {
|
#cancel {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -230,14 +230,15 @@
|
|||||||
return $positionsArr;
|
return $positionsArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get array of all positions
|
* Get array of all positions
|
||||||
*
|
*
|
||||||
* @param int $userId Optional limit to given user id
|
* @param int $userId Optional limit to given user id
|
||||||
* @param int $trackId Optional limit to given track id
|
* @param int $trackId Optional limit to given track id
|
||||||
* @return array|bool Array of uPosition positions, false on error
|
* @param int $afterId Optional limit to positions with id greater then given id
|
||||||
*/
|
* @return array|bool Array of uPosition positions, false on error
|
||||||
public static function getAll($userId = NULL, $trackId = NULL) {
|
*/
|
||||||
|
public static function getAll($userId = NULL, $trackId = NULL, $afterId = NULL) {
|
||||||
$rules = [];
|
$rules = [];
|
||||||
if (!empty($userId)) {
|
if (!empty($userId)) {
|
||||||
$rules[] = "p.user_id = " . self::db()->quote($userId);
|
$rules[] = "p.user_id = " . self::db()->quote($userId);
|
||||||
@ -245,6 +246,9 @@
|
|||||||
if (!empty($trackId)) {
|
if (!empty($trackId)) {
|
||||||
$rules[] = "p.track_id = " . self::db()->quote($trackId);
|
$rules[] = "p.track_id = " . self::db()->quote($trackId);
|
||||||
}
|
}
|
||||||
|
if (!empty($trackId)) {
|
||||||
|
$rules[] = "p.id > " . self::db()->quote($afterId);
|
||||||
|
}
|
||||||
if (!empty($rules)) {
|
if (!empty($rules)) {
|
||||||
$where = "WHERE " . implode(" AND ", $rules);
|
$where = "WHERE " . implode(" AND ", $rules);
|
||||||
} else {
|
} else {
|
||||||
|
61
index.php
61
index.php
@ -44,7 +44,6 @@
|
|||||||
$auth->exitWithRedirect("login.php");
|
$auth->exitWithRedirect("login.php");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$displayUserId = NULL;
|
$displayUserId = NULL;
|
||||||
$usersArr = [];
|
$usersArr = [];
|
||||||
if ($auth->isAdmin() || uConfig::$public_tracks) {
|
if ($auth->isAdmin() || uConfig::$public_tracks) {
|
||||||
@ -77,64 +76,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<title><?= $lang["title"] ?></title>
|
<title><?= $lang["title"] ?></title>
|
||||||
<?php include("meta.php"); ?>
|
<?php include("meta.php"); ?>
|
||||||
<script>
|
<script type="module" src="js/ulogger.js"></script>
|
||||||
/** @namespace uLogger */
|
<!-- <script src="dist/ulogger.js"></script>-->
|
||||||
var uLogger = window.uLogger || {};
|
|
||||||
/** @type {number} userId */
|
|
||||||
uLogger.userId = <?= json_encode($displayUserId ? $displayUserId : -1) ?>;
|
|
||||||
/** @type {number} trackId */
|
|
||||||
uLogger.trackId = <?= json_encode($displayTrackId ? $displayTrackId : -1) ?>;
|
|
||||||
|
|
||||||
/** @type {uLogger.config} */
|
|
||||||
uLogger.config = {
|
|
||||||
/** @type {number} */
|
|
||||||
interval: <?= json_encode(uConfig::$interval) ?>,
|
|
||||||
/** @type {string} */
|
|
||||||
units: <?= json_encode(uConfig::$units) ?>,
|
|
||||||
/** @type {string} */
|
|
||||||
mapapi: <?= json_encode(uConfig::$mapapi) ?>,
|
|
||||||
/** @type {?string} */
|
|
||||||
gkey: <?= json_encode(uConfig::$gkey) ?>,
|
|
||||||
/** @type {Object.<string, string>} */
|
|
||||||
ol_layers: <?= json_encode(uConfig::$ol_layers) ?>,
|
|
||||||
/** @type {number} */
|
|
||||||
init_latitude: <?= json_encode(uConfig::$init_latitude) ?>,
|
|
||||||
/** @type {number} */
|
|
||||||
init_longitude: <?= json_encode(uConfig::$init_longitude) ?>,
|
|
||||||
/** @type {boolean} */
|
|
||||||
admin: <?= json_encode($auth->isAdmin()) ?>,
|
|
||||||
/** @type {?string} */
|
|
||||||
auth: <?= json_encode($auth->isAuthenticated() ? $auth->user->login : NULL) ?>,
|
|
||||||
/** @type {RegExp} */
|
|
||||||
pass_regex: <?= uConfig::passRegex() ?>,
|
|
||||||
/** @type {number} */
|
|
||||||
strokeWeight: <?= json_encode(uConfig::$strokeWeight) ?>,
|
|
||||||
/** @type {string} */
|
|
||||||
strokeColor: <?= json_encode(uConfig::$strokeColor) ?>,
|
|
||||||
/** @type {number} */
|
|
||||||
strokeOpacity: <?= json_encode(uConfig::$strokeOpacity) ?>
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {uLogger.lang} */
|
|
||||||
uLogger.lang = {
|
|
||||||
/** @type {Object.<string, string>} */
|
|
||||||
strings: <?= json_encode($lang) ?>
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script src="js/main.js"></script>
|
|
||||||
<?php if ($auth->isAdmin()): ?>
|
|
||||||
<script src="js/admin.js"></script>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($auth->isAuthenticated()): ?>
|
|
||||||
<script src="js/track.js"></script>
|
|
||||||
<?php endif; ?>
|
|
||||||
<script src="js/pass.js"></script>
|
|
||||||
<script src="js/api_gmaps.js"></script>
|
|
||||||
<script src="js/api_openlayers.js"></script>
|
|
||||||
<script src="//www.google.com/jsapi"></script>
|
|
||||||
<script>
|
|
||||||
google.load('visualization', '1', { packages:['corechart'] });
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
141
js/admin.js
141
js/admin.js
@ -1,141 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @namespace */
|
|
||||||
var uLogger = window.uLogger || {};
|
|
||||||
(function (ul) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef uLogger.admin
|
|
||||||
* @memberOf uLogger
|
|
||||||
* @type {Object}
|
|
||||||
* @property {function} addUser
|
|
||||||
* @property {function} editUser
|
|
||||||
* @property {function} submitUser
|
|
||||||
*/
|
|
||||||
ul.admin = (function (ns) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show add user dialog
|
|
||||||
*/
|
|
||||||
function addUser() {
|
|
||||||
var form = '<form id="userForm" method="post" onsubmit="uLogger.admin.submitUser(\'add\'); return false">';
|
|
||||||
form += '<label><b>' + ns.lang.strings['username'] + '</b></label><input type="text" placeholder="' + ns.lang.strings['usernameenter'] + '" name="login" required>';
|
|
||||||
form += '<label><b>' + ns.lang.strings['password'] + '</b></label><input type="password" placeholder="' + ns.lang.strings['passwordenter'] + '" name="pass" required>';
|
|
||||||
form += '<label><b>' + ns.lang.strings['passwordrepeat'] + '</b></label><input type="password" placeholder="' + ns.lang.strings['passwordenter'] + '" name="pass2" required>';
|
|
||||||
form += '<div class="buttons"><button type="button" onclick="uLogger.ui.removeModal()">' + ns.lang.strings['cancel'] + '</button><button type="submit">' + ns.lang.strings['submit'] + '</button></div>';
|
|
||||||
form += '</form>';
|
|
||||||
ns.ui.showModal(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show edit user dialog
|
|
||||||
*/
|
|
||||||
function editUser() {
|
|
||||||
var userForm = ns.ui.userSelect;
|
|
||||||
var userLogin = (userForm) ? userForm.options[userForm.selectedIndex].text : ns.config.auth;
|
|
||||||
if (userLogin === ns.config.auth) {
|
|
||||||
alert(ns.lang.strings['selfeditwarn']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var message = '<div style="float:left">' + ns.sprintf(ns.lang.strings['editinguser'], '<b>' + ns.htmlEncode(userLogin) + '</b>') + '</div>';
|
|
||||||
message += '<div class="red-button"><b><a href="javascript:void(0);" onclick="uLogger.admin.submitUser(\'delete\'); return false">' + ns.lang.strings['deluser'] + '</a></b></div>';
|
|
||||||
message += '<div style="clear: both; padding-bottom: 1em;"></div>';
|
|
||||||
|
|
||||||
var form = '<form id="userForm" method="post" onsubmit="uLogger.admin.submitUser(\'update\'); return false">';
|
|
||||||
form += '<input type="hidden" name="login" value="' + ns.htmlEncode(userLogin) + '">';
|
|
||||||
form += '<label><b>' + ns.lang.strings['password'] + '</b></label><input type="password" placeholder="' + ns.lang.strings['passwordenter'] + '" name="pass" required>';
|
|
||||||
form += '<label><b>' + ns.lang.strings['passwordrepeat'] + '</b></label><input type="password" placeholder="' + ns.lang.strings['passwordenter'] + '" name="pass2" required>';
|
|
||||||
form += '<div class="buttons"><button type="button" onclick="uLogger.ui.removeModal()">' + ns.lang.strings['cancel'] + '</button><button type="submit">' + ns.lang.strings['submit'] + '</button></div>';
|
|
||||||
form += '</form>';
|
|
||||||
ns.ui.showModal(message + form);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show confirmation dialog
|
|
||||||
* @param {string} login
|
|
||||||
* @returns {boolean} True if confirmed
|
|
||||||
*/
|
|
||||||
function confirmedDelete(login) {
|
|
||||||
return confirm(ns.sprintf(ns.lang.strings['userdelwarn'], '"' + login + '"'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit user form
|
|
||||||
* @param {string} action Add, delete, update
|
|
||||||
*/
|
|
||||||
function submitUser(action) {
|
|
||||||
var form = document.getElementById('userForm');
|
|
||||||
var login = form.elements['login'].value.trim();
|
|
||||||
if (!login) {
|
|
||||||
alert(ns.lang.strings['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(ns.lang.strings['allrequired']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pass !== pass2) {
|
|
||||||
alert(ns.lang.strings['passnotmatch']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ns.config.pass_regex.test(pass)) {
|
|
||||||
alert(ns.lang.strings['passlenmin'] + '\n' + ns.lang.strings['passrules']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (!confirmedDelete(login)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ns.post('utils/handleuser.php',
|
|
||||||
{
|
|
||||||
action: action,
|
|
||||||
login: login,
|
|
||||||
pass: pass
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function () {
|
|
||||||
ns.ui.removeModal();
|
|
||||||
alert(ns.lang.strings['actionsuccess']);
|
|
||||||
if (action === 'delete') {
|
|
||||||
var f = ns.ui.userSelect;
|
|
||||||
f.remove(f.selectedIndex);
|
|
||||||
ns.selectUser(f);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: function (message) {
|
|
||||||
alert(ns.lang.strings['actionfailure'] + '\n' + message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
return {
|
|
||||||
addUser: addUser,
|
|
||||||
editUser: editUser,
|
|
||||||
submitUser: submitUser
|
|
||||||
}
|
|
||||||
|
|
||||||
})(ul);
|
|
||||||
|
|
||||||
})(uLogger);
|
|
98
js/ajax.js
Normal file
98
js/ajax.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
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'
|
||||||
|
* @param {HTMLElement} [options.loader] Optional element to animate during loading
|
||||||
|
* @return {Promise<Document, string>}
|
||||||
|
*/
|
||||||
|
static ajax(url, data, options) {
|
||||||
|
const params = [];
|
||||||
|
data = data || {};
|
||||||
|
options = options || {};
|
||||||
|
let method = options.method || 'GET';
|
||||||
|
const loader = options.loader;
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState !== 4) { return; }
|
||||||
|
let message = '';
|
||||||
|
let error = true;
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const xml = xhr.responseXML;
|
||||||
|
if (xml) {
|
||||||
|
const root = xml.getElementsByTagName('root');
|
||||||
|
if (root.length && uUtils.getNode(root[0], 'error') !== '1') {
|
||||||
|
if (resolve && typeof resolve === 'function') {
|
||||||
|
resolve(xml);
|
||||||
|
}
|
||||||
|
error = false;
|
||||||
|
} else if (root.length) {
|
||||||
|
const errorMsg = uUtils.getNode(root[0], 'message');
|
||||||
|
if (errorMsg) {
|
||||||
|
message = errorMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error && reject && typeof reject === 'function') {
|
||||||
|
reject(message);
|
||||||
|
}
|
||||||
|
if (loader) {
|
||||||
|
// UI.removeLoader(loader);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let body = null;
|
||||||
|
if (data instanceof HTMLFormElement) {
|
||||||
|
// noinspection JSCheckFunctionSignatures
|
||||||
|
body = new FormData(data);
|
||||||
|
method = 'POST';
|
||||||
|
} 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' && params.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);
|
||||||
|
if (loader) {
|
||||||
|
// UI.setLoader(loader);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
296
js/api_gmaps.js
296
js/api_gmaps.js
@ -1,296 +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
|
|
||||||
/** @namespace */
|
|
||||||
var uLogger = uLogger || {};
|
|
||||||
/** @namespace */
|
|
||||||
uLogger.mapAPI = uLogger.mapAPI || {};
|
|
||||||
/** @namespace */
|
|
||||||
uLogger.mapAPI.gmaps = (function(ns) {
|
|
||||||
|
|
||||||
/** @type {google.maps.Map} */
|
|
||||||
var map;
|
|
||||||
/** @type {google.maps.Polyline[]} */
|
|
||||||
var polies = [];
|
|
||||||
/** @type {google.maps.Marker[]} */
|
|
||||||
var markers = [];
|
|
||||||
/** @type {google.maps.InfoWindow[]} */
|
|
||||||
var popups = [];
|
|
||||||
/** @type {google.maps.InfoWindow} */
|
|
||||||
var popup;
|
|
||||||
/** @type {google.maps.PolylineOptions} */
|
|
||||||
var polyOptions;
|
|
||||||
/** @type {google.maps.MapOptions} */
|
|
||||||
var mapOptions;
|
|
||||||
/** @type {number} */
|
|
||||||
var timeoutHandle;
|
|
||||||
var name = 'gmaps';
|
|
||||||
var isLoaded = false;
|
|
||||||
var authError = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize map
|
|
||||||
*/
|
|
||||||
function init() {
|
|
||||||
var url = '//maps.googleapis.com/maps/api/js?' + ((ns.config.gkey != null) ? ('key=' + ns.config.gkey + '&') : '') + 'callback=uLogger.mapAPI.gmaps.setLoaded';
|
|
||||||
ns.addScript(url, 'mapapi_gmaps');
|
|
||||||
if (!isLoaded) {
|
|
||||||
throw new Error("Google Maps API not ready");
|
|
||||||
}
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start map engine when loaded
|
|
||||||
*/
|
|
||||||
function start() {
|
|
||||||
if (authError) {
|
|
||||||
gm_authFailure();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
google.maps.visualRefresh = true;
|
|
||||||
// noinspection JSValidateTypes
|
|
||||||
polyOptions = {
|
|
||||||
strokeColor: ns.config.strokeColor,
|
|
||||||
strokeOpacity: ns.config.strokeOpacity,
|
|
||||||
strokeWeight: ns.config.strokeWeight
|
|
||||||
};
|
|
||||||
// noinspection JSValidateTypes
|
|
||||||
mapOptions = {
|
|
||||||
center: new google.maps.LatLng(ns.config.init_latitude, ns.config.init_longitude),
|
|
||||||
zoom: 8,
|
|
||||||
mapTypeId: google.maps.MapTypeId.ROADMAP,
|
|
||||||
scaleControl: true
|
|
||||||
};
|
|
||||||
map = new google.maps.Map(ns.ui.map, mapOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up API
|
|
||||||
*/
|
|
||||||
function cleanup() {
|
|
||||||
polies = [];
|
|
||||||
markers = [];
|
|
||||||
popups = [];
|
|
||||||
map = null;
|
|
||||||
polyOptions = null;
|
|
||||||
mapOptions = null;
|
|
||||||
popup = null;
|
|
||||||
ns.clearMapCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display track
|
|
||||||
* @param {HTMLCollection} positions XML element
|
|
||||||
* @param {boolean} update Should fit bounds if true
|
|
||||||
*/
|
|
||||||
function displayTrack(positions, update) {
|
|
||||||
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 posLen = positions.length;
|
|
||||||
for (var i = 0; i < posLen; i++) {
|
|
||||||
var p = ns.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 coordinates = new google.maps.LatLng(p.latitude, p.longitude);
|
|
||||||
path.push(coordinates);
|
|
||||||
latlngbounds.extend(coordinates);
|
|
||||||
}
|
|
||||||
if (update) {
|
|
||||||
map.fitBounds(latlngbounds);
|
|
||||||
if (i === 1) {
|
|
||||||
// only one point, zoom out
|
|
||||||
var zListener =
|
|
||||||
google.maps.event.addListenerOnce(map, 'bounds_changed', function () {
|
|
||||||
if (this.getZoom()) {
|
|
||||||
this.setZoom(15);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setTimeout(function () { google.maps.event.removeListener(zListener) }, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
polies.push(poly);
|
|
||||||
|
|
||||||
ns.updateSummary(p.timestamp, totalMeters, totalSeconds);
|
|
||||||
if (p.tid !== ns.config.trackid) {
|
|
||||||
ns.config.trackid = p.tid;
|
|
||||||
ns.setTrack(ns.config.trackid);
|
|
||||||
}
|
|
||||||
ns.updateChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear map
|
|
||||||
*/
|
|
||||||
function clearMap() {
|
|
||||||
if (polies) {
|
|
||||||
for (var i = 0; i < polies.length; i++) {
|
|
||||||
polies[i].setMap(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (markers) {
|
|
||||||
for (var j = 0; j < markers.length; j++) {
|
|
||||||
google.maps.event.removeListener(popups[j].listener);
|
|
||||||
popups[j].setMap(null);
|
|
||||||
markers[j].setMap(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markers.length = 0;
|
|
||||||
polies.length = 0;
|
|
||||||
popups.lentgth = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set marker
|
|
||||||
* @param {uLogger.Position} pos
|
|
||||||
* @param {number} id
|
|
||||||
* @param {number} posLen
|
|
||||||
*/
|
|
||||||
function setMarker(pos, id, posLen) {
|
|
||||||
// marker
|
|
||||||
// noinspection JSCheckFunctionSignatures
|
|
||||||
var marker = new google.maps.Marker({
|
|
||||||
position: new google.maps.LatLng(pos.latitude, pos.longitude),
|
|
||||||
title: (new Date(pos.timestamp * 1000)).toLocaleString(),
|
|
||||||
map: map
|
|
||||||
});
|
|
||||||
if (ns.isLatest()) {
|
|
||||||
marker.setIcon('images/marker-red.png');
|
|
||||||
} else if (id === 0) {
|
|
||||||
marker.setIcon('images/marker-green.png');
|
|
||||||
} else if (id === posLen - 1) {
|
|
||||||
marker.setIcon('images/marker-red.png');
|
|
||||||
} else {
|
|
||||||
marker.setIcon('images/marker-white.png');
|
|
||||||
}
|
|
||||||
// popup
|
|
||||||
var content = ns.getPopupHtml(pos, id, posLen);
|
|
||||||
popup = new google.maps.InfoWindow();
|
|
||||||
// noinspection JSUndefinedPropertyAssignment
|
|
||||||
popup.listener = google.maps.event.addListener(marker, 'click', (function (_marker, _content) {
|
|
||||||
return function () {
|
|
||||||
popup.setContent(_content);
|
|
||||||
popup.open(map, _marker);
|
|
||||||
ns.chartShowPosition(id);
|
|
||||||
}
|
|
||||||
})(marker, content));
|
|
||||||
markers.push(marker);
|
|
||||||
popups.push(popup);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add listener on chart to show position on map
|
|
||||||
* @param {google.visualization.LineChart} chart
|
|
||||||
* @param {google.visualization.DataTable} data
|
|
||||||
*/
|
|
||||||
function addChartEvent(chart, data) {
|
|
||||||
google.visualization.events.addListener(chart, 'select', function () {
|
|
||||||
if (popup) { popup.close(); clearTimeout(timeoutHandle); }
|
|
||||||
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');
|
|
||||||
timeoutHandle = setTimeout(function () { markers[id].setIcon(icon); }, 2000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get map bounds
|
|
||||||
* eg. ((52.20105108685229, 20.789387865580238), (52.292069558807135, 21.172192736185707))
|
|
||||||
* @returns {number[]} Bounds
|
|
||||||
*/
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zoom to track extent
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zoom to bounds
|
|
||||||
* @param {number[]} bounds
|
|
||||||
*/
|
|
||||||
function zoomToBounds(bounds) {
|
|
||||||
var sw = new google.maps.LatLng(bounds[1], bounds[0]);
|
|
||||||
var ne = new google.maps.LatLng(bounds[3], bounds[2]);
|
|
||||||
var latLngBounds = new google.maps.LatLngBounds(sw, ne);
|
|
||||||
map.fitBounds(latLngBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update size
|
|
||||||
*/
|
|
||||||
function updateSize() {
|
|
||||||
// ignore for google API
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
init: init,
|
|
||||||
setLoaded: function () { isLoaded = true; },
|
|
||||||
cleanup: cleanup,
|
|
||||||
displayTrack: displayTrack,
|
|
||||||
clearMap: clearMap,
|
|
||||||
setMarker: setMarker,
|
|
||||||
addChartEvent: addChartEvent,
|
|
||||||
getBounds: getBounds,
|
|
||||||
zoomToExtent: zoomToExtent,
|
|
||||||
zoomToBounds: zoomToBounds,
|
|
||||||
updateSize: updateSize
|
|
||||||
}
|
|
||||||
|
|
||||||
})(uLogger);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback for Google Maps API
|
|
||||||
* It will be called when authentication fails
|
|
||||||
*/
|
|
||||||
function gm_authFailure() {
|
|
||||||
uLogger.mapAPI.gmaps.authError = true;
|
|
||||||
var message = uLogger.sprintf(uLogger.lang.strings['apifailure'], 'Google Maps');
|
|
||||||
message += '<br><br>' + uLogger.lang.strings['gmauthfailure'];
|
|
||||||
message += '<br><br>' + uLogger.lang.strings['gmapilink'];
|
|
||||||
uLogger.ui.showModal(message);
|
|
||||||
}
|
|
@ -1,494 +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+
|
|
||||||
/** @namespace */
|
|
||||||
var uLogger = uLogger || {};
|
|
||||||
/** @namespace */
|
|
||||||
uLogger.mapAPI = uLogger.mapAPI || {};
|
|
||||||
/** @namespace */
|
|
||||||
uLogger.mapAPI.ol = (function(ns) {
|
|
||||||
|
|
||||||
/** @type {ol.Map} */
|
|
||||||
var map;
|
|
||||||
/** @type {ol.layer.Vector} */
|
|
||||||
var layerTrack;
|
|
||||||
/** @type {ol.layer.Vector} */
|
|
||||||
var layerMarkers;
|
|
||||||
/** @type {ol.layer.Base} */
|
|
||||||
var selectedLayer;
|
|
||||||
/** @type {ol.style.Style|{}} */
|
|
||||||
var olStyles;
|
|
||||||
var name = 'openlayers';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize map
|
|
||||||
*/
|
|
||||||
function init() {
|
|
||||||
var urls = [];
|
|
||||||
urls.push('//cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList');
|
|
||||||
urls.push('js/ol.js');
|
|
||||||
for (var i = 0; i < urls.length; i++) {
|
|
||||||
ns.addScript(urls[i], 'mapapi_openlayers' + '_' + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
ns.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([ns.config.init_longitude, ns.config.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 ns.config.ol_layers) {
|
|
||||||
if (ns.config.ol_layers.hasOwnProperty(layerName)) {
|
|
||||||
var layerUrl = ns.config.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: ns.hexToRGBA(ns.config.strokeColor, ns.config.strokeOpacity),
|
|
||||||
width: ns.config.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.body.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 () {
|
|
||||||
// eslint-disable-next-line no-undefined
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
if (feature) {
|
|
||||||
var pos = feature.get('p');
|
|
||||||
var id = feature.getId();
|
|
||||||
var posLen = feature.get('posLen');
|
|
||||||
// popup show
|
|
||||||
popup.setPosition(coordinate);
|
|
||||||
popupContent.innerHTML = ns.getPopupHtml(pos, id, posLen);
|
|
||||||
map.addOverlay(popup);
|
|
||||||
ns.chartShowPosition(id);
|
|
||||||
} else {
|
|
||||||
// popup destroy
|
|
||||||
// eslint-disable-next-line no-undefined
|
|
||||||
popup.setPosition(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// change mouse cursor when over marker
|
|
||||||
map.on('pointermove', function (e) {
|
|
||||||
var hit = map.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
|
|
||||||
return layer.get('name') === 'Markers';
|
|
||||||
});
|
|
||||||
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.body.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 targetName = this.value;
|
|
||||||
map.getLayers().forEach(function (layer) {
|
|
||||||
if (layer.get('name') === targetName) {
|
|
||||||
if (layer.get('type') === 'data') {
|
|
||||||
if (layer.getVisible()) {
|
|
||||||
layer.setVisible(false);
|
|
||||||
} else {
|
|
||||||
layer.setVisible(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedLayer.setVisible(false);
|
|
||||||
selectedLayer = layer;
|
|
||||||
layer.setVisible(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var switcherButton = document.createElement('button');
|
|
||||||
var layerImg = document.createElement('img');
|
|
||||||
layerImg.src = 'images/layers.svg';
|
|
||||||
layerImg.style.width = '60%';
|
|
||||||
switcherButton.appendChild(layerImg);
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-style
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up API
|
|
||||||
*/
|
|
||||||
function cleanup() {
|
|
||||||
map = null;
|
|
||||||
layerTrack = null;
|
|
||||||
layerMarkers = null;
|
|
||||||
selectedLayer = null;
|
|
||||||
olStyles = null;
|
|
||||||
ns.removeElementById('popup');
|
|
||||||
ns.removeElementById('switcher');
|
|
||||||
ns.clearMapCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display track
|
|
||||||
* @param {HTMLCollection} positions XML element
|
|
||||||
* @param {boolean} update Should fit bounds if true
|
|
||||||
*/
|
|
||||||
function displayTrack(positions, update) {
|
|
||||||
var totalMeters = 0;
|
|
||||||
var totalSeconds = 0;
|
|
||||||
var points = [];
|
|
||||||
var posLen = positions.length;
|
|
||||||
for (var i = 0; i < posLen; i++) {
|
|
||||||
var p = ns.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);
|
|
||||||
|
|
||||||
ns.updateSummary(p.timestamp, totalMeters, totalSeconds);
|
|
||||||
if (p.tid !== ns.config.trackid) {
|
|
||||||
ns.config.trackid = p.tid;
|
|
||||||
ns.setTrack(ns.config.trackid);
|
|
||||||
}
|
|
||||||
ns.updateChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear map
|
|
||||||
*/
|
|
||||||
function clearMap() {
|
|
||||||
if (layerTrack) {
|
|
||||||
layerTrack.getSource().clear();
|
|
||||||
}
|
|
||||||
if (layerMarkers) {
|
|
||||||
layerMarkers.getSource().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set marker
|
|
||||||
* @param {uLogger.Position} pos
|
|
||||||
* @param {number} id
|
|
||||||
* @param {number} posLen
|
|
||||||
*/
|
|
||||||
function setMarker(pos, id, posLen) {
|
|
||||||
// marker
|
|
||||||
var marker = new ol.Feature({
|
|
||||||
geometry: new ol.geom.Point(ol.proj.fromLonLat([pos.longitude, pos.latitude]))
|
|
||||||
});
|
|
||||||
|
|
||||||
var iconStyle;
|
|
||||||
if (ns.isLatest()) {
|
|
||||||
iconStyle = olStyles['red'];
|
|
||||||
} else if (id === 0) {
|
|
||||||
iconStyle = olStyles['green'];
|
|
||||||
} else if (id === posLen - 1) {
|
|
||||||
iconStyle = olStyles['red'];
|
|
||||||
} else {
|
|
||||||
iconStyle = olStyles['white'];
|
|
||||||
}
|
|
||||||
marker.setStyle(iconStyle);
|
|
||||||
marker.setId(id);
|
|
||||||
marker.set('p', pos);
|
|
||||||
marker.set('posLen', posLen);
|
|
||||||
layerMarkers.getSource().addFeature(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add listener on chart to show position on map
|
|
||||||
* @param {google.visualization.LineChart} chart
|
|
||||||
* @param {google.visualization.DataTable} data
|
|
||||||
*/
|
|
||||||
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 initStyle = marker.getStyle();
|
|
||||||
var iconStyle = olStyles['gold'];
|
|
||||||
marker.setStyle(iconStyle);
|
|
||||||
setTimeout(function () {
|
|
||||||
marker.setStyle(initStyle);
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get map bounds
|
|
||||||
* eg. (20.597985430276808, 52.15547181298076, 21.363595171488573, 52.33750879522563)
|
|
||||||
* @returns {number[]} Bounds
|
|
||||||
*/
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zoom to track extent
|
|
||||||
*/
|
|
||||||
function zoomToExtent() {
|
|
||||||
map.getView().fit(layerMarkers.getSource().getExtent());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zoom to bounds
|
|
||||||
* @param {number[]} bounds
|
|
||||||
*/
|
|
||||||
function zoomToBounds(bounds) {
|
|
||||||
var extent = ol.proj.transformExtent(bounds, 'EPSG:4326', 'EPSG:900913');
|
|
||||||
map.getView().fit(extent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update size
|
|
||||||
*/
|
|
||||||
function updateSize() {
|
|
||||||
map.updateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get extent image
|
|
||||||
* @returns {HTMLImageElement}
|
|
||||||
*/
|
|
||||||
function getExtentImg() {
|
|
||||||
var extentImg = document.createElement('img');
|
|
||||||
extentImg.src = 'images/extent.svg';
|
|
||||||
extentImg.style.width = '60%';
|
|
||||||
return extentImg;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
init: init,
|
|
||||||
cleanup: cleanup,
|
|
||||||
displayTrack: displayTrack,
|
|
||||||
clearMap: clearMap,
|
|
||||||
setMarker: setMarker,
|
|
||||||
addChartEvent: addChartEvent,
|
|
||||||
getBounds: getBounds,
|
|
||||||
zoomToExtent: zoomToExtent,
|
|
||||||
zoomToBounds: zoomToBounds,
|
|
||||||
updateSize: updateSize
|
|
||||||
}
|
|
||||||
|
|
||||||
})(uLogger);
|
|
77
js/auth.js
Normal file
77
js/auth.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import UserDialog from './userdialog.js';
|
||||||
|
import { lang } from './constants.js';
|
||||||
|
import uEvent from './event.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) {
|
||||||
|
this._user = user;
|
||||||
|
this._isAuthenticated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} isAdmin
|
||||||
|
*/
|
||||||
|
set isAdmin(isAdmin) {
|
||||||
|
this._isAdmin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isAdmin() {
|
||||||
|
return this._isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isAuthenticated() {
|
||||||
|
return this._isAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {?uUser}
|
||||||
|
*/
|
||||||
|
get user() {
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uEvent} event
|
||||||
|
*/
|
||||||
|
handleEvent(event) {
|
||||||
|
if (event.type === uEvent.PASSWORD && this.isAuthenticated) {
|
||||||
|
this.changePassword();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UserDialog=} modal
|
||||||
|
*/
|
||||||
|
changePassword(modal) {
|
||||||
|
const dialog = modal || new UserDialog('pass', this.user);
|
||||||
|
dialog.show()
|
||||||
|
.then((result) => this.user.changePass(result.data.password, result.data.oldPassword))
|
||||||
|
.then(() => {
|
||||||
|
alert(lang.strings['actionsuccess']);
|
||||||
|
dialog.hide();
|
||||||
|
})
|
||||||
|
.catch((msg) => {
|
||||||
|
alert(`${lang.strings['actionfailure']}\n${msg}`);
|
||||||
|
this.changePassword(dialog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
58
js/binder.js
Normal file
58
js/binder.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import uEvent from './event.js';
|
||||||
|
|
||||||
|
export default class uBinder {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
/** @type {Map<string, uEvent>} */
|
||||||
|
this.events = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
|
addEvent(type) {
|
||||||
|
this.events.set(type, new uEvent(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
* @param {(Object|Function)} listener
|
||||||
|
*/
|
||||||
|
addEventListener(type, listener) {
|
||||||
|
if (!this.events.has(type)) {
|
||||||
|
this.addEvent(type);
|
||||||
|
}
|
||||||
|
if ((typeof listener === 'object') &&
|
||||||
|
(typeof listener.handleEvent === 'function')) {
|
||||||
|
listener = listener.handleEvent.bind(listener);
|
||||||
|
}
|
||||||
|
if (typeof listener !== 'function') {
|
||||||
|
throw new Error(`Wrong listener type: ${typeof listener}`);
|
||||||
|
}
|
||||||
|
this.events.get(type).addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
* @param {(Object|Function)} listener
|
||||||
|
*/
|
||||||
|
removeEventListener(type, listener) {
|
||||||
|
if (this.events.has(type)) {
|
||||||
|
if ((typeof listener === 'object') &&
|
||||||
|
(typeof listener.handleEvent === 'function')) {
|
||||||
|
listener = listener.handleEvent;
|
||||||
|
}
|
||||||
|
this.events.get(type).removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
* @param {*=} args
|
||||||
|
*/
|
||||||
|
dispatchEvent(type, args) {
|
||||||
|
if (this.events.has(type)) {
|
||||||
|
this.events.get(type).dispatch(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
js/chart.js
Normal file
110
js/chart.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { config, lang } from './constants.js';
|
||||||
|
import uEvent from './event.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/* global Chartist */
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
Chart.onDomLoaded();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default class Chart {
|
||||||
|
/**
|
||||||
|
* @param {uBinder} binder
|
||||||
|
*/
|
||||||
|
constructor(binder) {
|
||||||
|
binder.addEventListener(uEvent.UI_READY, this);
|
||||||
|
binder.addEventListener(uEvent.TRACK_READY, this);
|
||||||
|
this._binder = binder;
|
||||||
|
this._targetEl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Array<{x: number, y: number}>}
|
||||||
|
*/
|
||||||
|
get data() {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this._targetEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chart = new Chartist.Line(this._targetEl, {
|
||||||
|
series: [ this.data ]
|
||||||
|
}, {
|
||||||
|
lineSmooth: true,
|
||||||
|
showArea: true,
|
||||||
|
axisX: {
|
||||||
|
type: Chartist.AutoScaleAxis,
|
||||||
|
onlyInteger: true,
|
||||||
|
showLabel: false
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
Chartist.plugins.ctAxisTitle({
|
||||||
|
axisY: {
|
||||||
|
axisTitle: `${lang.strings['altitude']} (${config.unit_m})`,
|
||||||
|
axisClass: 'ct-axis-title',
|
||||||
|
offset: {
|
||||||
|
x: 0,
|
||||||
|
y: 20
|
||||||
|
},
|
||||||
|
textAnchor: 'middle',
|
||||||
|
flipTitle: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
chart.on('created', () => {
|
||||||
|
const points = document.querySelectorAll('.ct-chart-line .ct-point');
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
((id) => {
|
||||||
|
points[id].addEventListener('click', () => {
|
||||||
|
/** @todo trigger marker action */
|
||||||
|
console.log(id);
|
||||||
|
});
|
||||||
|
})(i);
|
||||||
|
}
|
||||||
|
this._binder.dispatchEvent('chart ready', points.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// need to update chart first time the container becomes visible
|
||||||
|
if (this._targetEl.parentNode.style.display !== 'block') {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (this._targetEl.parentNode.style.display === 'block') {
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
this._targetEl.__chartist__.update();
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(this._targetEl.parentNode, { attributes: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static onDomLoaded() {
|
||||||
|
uUtils.addScript('js/lib/chartist.min.js', 'chartist_js', () => {
|
||||||
|
uUtils.addScript('js/lib/chartist-plugin-axistitle.min.js', 'chartist_axistitle_js');
|
||||||
|
});
|
||||||
|
uUtils.addCss('css/chartist.min.css', 'chartist_css');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uEvent} event
|
||||||
|
* @param {*=} args
|
||||||
|
*/
|
||||||
|
handleEvent(event, args) {
|
||||||
|
if (event.type === uEvent.TRACK_READY) {
|
||||||
|
/** @type {uTrack} */
|
||||||
|
const track = args;
|
||||||
|
this._data = track.plotData;
|
||||||
|
this.render()
|
||||||
|
} else if (event.type === uEvent.UI_READY) {
|
||||||
|
/** @type {uUI} */
|
||||||
|
const ui = args;
|
||||||
|
this._targetEl = ui.chart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
275
js/config.js
Normal file
275
js/config.js
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import uEvent from './event.js';
|
||||||
|
|
||||||
|
export default class uConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {uBinder} binder
|
||||||
|
*/
|
||||||
|
set binder(binder) {
|
||||||
|
this._binder = binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch event
|
||||||
|
* @param {string} property
|
||||||
|
*/
|
||||||
|
notify(property) {
|
||||||
|
if (this._binder) {
|
||||||
|
this._binder.dispatchEvent(uEvent.CONFIG, property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get interval() {
|
||||||
|
return this._interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set interval(value) {
|
||||||
|
this._interval = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get units() {
|
||||||
|
return this._units;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
set units(value) {
|
||||||
|
this._units = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get mapapi() {
|
||||||
|
return this._mapapi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
set mapapi(value) {
|
||||||
|
this._mapapi = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {?string}
|
||||||
|
*/
|
||||||
|
get gkey() {
|
||||||
|
return this._gkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?string} value
|
||||||
|
*/
|
||||||
|
set gkey(value) {
|
||||||
|
this._gkey = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Object.<string, string>}
|
||||||
|
*/
|
||||||
|
get ol_layers() {
|
||||||
|
return this._ol_layers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object.<string, string>} value
|
||||||
|
*/
|
||||||
|
set ol_layers(value) {
|
||||||
|
this._ol_layers = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get init_latitude() {
|
||||||
|
return this._init_latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set init_latitude(value) {
|
||||||
|
this._init_latitude = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get init_longitude() {
|
||||||
|
return this._init_longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set init_longitude(value) {
|
||||||
|
this._init_longitude = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {RegExp}
|
||||||
|
*/
|
||||||
|
get pass_regex() {
|
||||||
|
return this._pass_regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RegExp} value
|
||||||
|
*/
|
||||||
|
set pass_regex(value) {
|
||||||
|
this._pass_regex = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get strokeWeight() {
|
||||||
|
return this._strokeWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set strokeWeight(value) {
|
||||||
|
this._strokeWeight = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get strokeColor() {
|
||||||
|
return this._strokeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
set strokeColor(value) {
|
||||||
|
this._strokeColor = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get strokeOpacity() {
|
||||||
|
return this._strokeOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set strokeOpacity(value) {
|
||||||
|
this._strokeOpacity = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get factor_kmh() {
|
||||||
|
return this._factor_kmh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set factor_kmh(value) {
|
||||||
|
this._factor_kmh = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get unit_kmh() {
|
||||||
|
return this._unit_kmh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
set unit_kmh(value) {
|
||||||
|
this._unit_kmh = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get factor_m() {
|
||||||
|
return this._factor_m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set factor_m(value) {
|
||||||
|
this._factor_m = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get unit_m() {
|
||||||
|
return this._unit_m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
set unit_m(value) {
|
||||||
|
this._unit_m = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get factor_km() {
|
||||||
|
return this._factor_km;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
set factor_km(value) {
|
||||||
|
this._factor_km = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get unit_km() {
|
||||||
|
return this._unit_km;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
set unit_km(value) {
|
||||||
|
this._unit_km = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get showLatest() {
|
||||||
|
return this._showLatest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} value
|
||||||
|
*/
|
||||||
|
set showLatest(value) {
|
||||||
|
this._showLatest = value;
|
||||||
|
this.notify('showLatest');
|
||||||
|
}
|
||||||
|
}
|
120
js/constants.js
Normal file
120
js/constants.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import uAuth from './auth.js';
|
||||||
|
import uConfig from './config.js';
|
||||||
|
import uUser from './user.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
class uConstants {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.auth = {};
|
||||||
|
this.config = {};
|
||||||
|
this.lang = {};
|
||||||
|
if (!this.loaded) {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {?XMLDocument}
|
||||||
|
*/
|
||||||
|
static fetch() {
|
||||||
|
let xml = null;
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
request.open('GET', 'utils/getconstants.php', false);
|
||||||
|
request.send(null);
|
||||||
|
if (request.status === 200) {
|
||||||
|
xml = request.responseXML;
|
||||||
|
}
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
const xml = uConstants.fetch();
|
||||||
|
if (xml) {
|
||||||
|
this.initAuth(xml);
|
||||||
|
this.initConfig(xml);
|
||||||
|
this.initLang(xml);
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {XMLDocument} xml
|
||||||
|
*/
|
||||||
|
initAuth(xml) {
|
||||||
|
this.auth = new uAuth();
|
||||||
|
const authNode = xml.getElementsByTagName('auth');
|
||||||
|
if (authNode.length) {
|
||||||
|
const isAuthenticated = uUtils.getNodeAsInt(authNode[0], 'isAuthenticated') === 1;
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const id = uUtils.getNodeAsInt(authNode[0], 'userId');
|
||||||
|
const login = uUtils.getNode(authNode[0], 'userLogin');
|
||||||
|
this.auth.user = new uUser(id, login);
|
||||||
|
this.auth.isAdmin = uUtils.getNodeAsInt(authNode[0], 'isAdmin') === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {XMLDocument} xml
|
||||||
|
*/
|
||||||
|
initLang(xml) {
|
||||||
|
const langNode = xml.getElementsByTagName('lang');
|
||||||
|
if (langNode.length) {
|
||||||
|
/** @type {Object<string, string>} */
|
||||||
|
this.lang.strings = uUtils.getNodesArray(langNode[0], 'strings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {XMLDocument} xml
|
||||||
|
*/
|
||||||
|
initConfig(xml) {
|
||||||
|
this.config = new uConfig();
|
||||||
|
const configNode = xml.getElementsByTagName('config');
|
||||||
|
if (configNode.length) {
|
||||||
|
this.config.interval = uUtils.getNodeAsInt(configNode[0], 'interval');
|
||||||
|
this.config.units = uUtils.getNode(configNode[0], 'units');
|
||||||
|
this.config.mapapi = uUtils.getNode(configNode[0], 'mapapi');
|
||||||
|
this.config.gkey = uUtils.getNode(configNode[0], 'gkey');
|
||||||
|
this.config.ol_layers = uUtils.getNodesArray(configNode[0], 'ol_layers');
|
||||||
|
this.config.init_latitude = uUtils.getNodeAsFloat(configNode[0], 'init_latitude');
|
||||||
|
this.config.init_longitude = uUtils.getNodeAsFloat(configNode[0], 'init_longitude');
|
||||||
|
const re = uUtils.getNode(configNode[0], 'pass_regex');
|
||||||
|
this.config.pass_regex = new RegExp(re.substr(1, re.length - 2));
|
||||||
|
this.config.strokeWeight = uUtils.getNodeAsInt(configNode[0], 'strokeWeight');
|
||||||
|
this.config.strokeColor = uUtils.getNode(configNode[0], 'strokeColor');
|
||||||
|
this.config.strokeOpacity = uUtils.getNodeAsInt(configNode[0], 'strokeOpacity');
|
||||||
|
this.config.factor_kmh = 1;
|
||||||
|
this.config.unit_kmh = 'km/h';
|
||||||
|
this.config.factor_m = 1;
|
||||||
|
this.config.unit_m = 'm';
|
||||||
|
this.config.factor_km = 1;
|
||||||
|
this.config.unit_km = 'km';
|
||||||
|
if (this.config.units === 'imperial') {
|
||||||
|
this.config.factor_kmh = 0.62; // to mph
|
||||||
|
this.config.unit_kmh = 'mph';
|
||||||
|
this.config.factor_m = 3.28; // to feet
|
||||||
|
this.config.unit_m = 'ft';
|
||||||
|
this.config.factor_km = 0.62; // to miles
|
||||||
|
this.config.unit_km = 'mi';
|
||||||
|
} else if (this.config.units === 'nautical') {
|
||||||
|
this.config.factor_kmh = 0.54; // to knots
|
||||||
|
this.config.unit_kmh = 'kt';
|
||||||
|
this.config.factor_m = 1; // meters
|
||||||
|
this.config.unit_m = 'm';
|
||||||
|
this.config.factor_km = 0.54; // to nautical miles
|
||||||
|
this.config.unit_km = 'nm';
|
||||||
|
}
|
||||||
|
this.config.showLatest = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const constants = new uConstants();
|
||||||
|
/** @type {uConfig} */
|
||||||
|
export const config = constants.config;
|
||||||
|
/** @type {{strings: Object<string, string>}} */
|
||||||
|
export const lang = constants.lang;
|
||||||
|
/** @type {uAuth} */
|
||||||
|
export const auth = constants.auth;
|
50
js/data.js
Normal file
50
js/data.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export default class uData {
|
||||||
|
/**
|
||||||
|
* @param {number} key
|
||||||
|
* @param {string} value
|
||||||
|
* @param {string} keyProperty
|
||||||
|
* @param {string} valueProperty
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line max-params
|
||||||
|
constructor(key, value, keyProperty, valueProperty) {
|
||||||
|
this[keyProperty] = key;
|
||||||
|
this[valueProperty] = value;
|
||||||
|
Object.defineProperty(this, 'key', {
|
||||||
|
get() {
|
||||||
|
return this[keyProperty];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, 'value', {
|
||||||
|
get() {
|
||||||
|
return this[valueProperty];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uBinder} binder
|
||||||
|
*/
|
||||||
|
set binder(binder) {
|
||||||
|
this._binder = binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {uBinder}
|
||||||
|
*/
|
||||||
|
get binder() {
|
||||||
|
return this._binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch event
|
||||||
|
* @param {string} type
|
||||||
|
* @param {*=} args Defaults to this
|
||||||
|
*/
|
||||||
|
emit(type, args) {
|
||||||
|
const data = args || this;
|
||||||
|
this.binder.dispatchEvent(type, data);
|
||||||
|
}
|
||||||
|
}
|
55
js/event.js
Normal file
55
js/event.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/* eslint-disable lines-between-class-members */
|
||||||
|
/**
|
||||||
|
* class uEvent
|
||||||
|
* property {string} type
|
||||||
|
* property {Set<Function>} listeners
|
||||||
|
*/
|
||||||
|
export default class uEvent {
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
|
constructor(type) {
|
||||||
|
/** type {string} */
|
||||||
|
this.type = type;
|
||||||
|
/** type {Set<Function>} */
|
||||||
|
this.listeners = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get ADD() { return 'µAdd'; }
|
||||||
|
static get API_CHANGE() { return 'µApiChange'; }
|
||||||
|
static get CHART_READY() { return 'µChartReady'; }
|
||||||
|
static get CONFIG() { return 'µConfig'; }
|
||||||
|
static get EDIT() { return 'µEdit'; }
|
||||||
|
static get EXPORT() { return 'µExport'; }
|
||||||
|
static get OPEN_URL() { return 'µOpen'; }
|
||||||
|
static get IMPORT() { return 'µImport'; }
|
||||||
|
static get PASSWORD() { return 'µPassword'; }
|
||||||
|
static get TRACK_READY() { return 'µTrackReady'; }
|
||||||
|
static get UI_READY() { return 'µUiReady'; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Function} listener
|
||||||
|
*/
|
||||||
|
addListener(listener) {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Function} listener
|
||||||
|
*/
|
||||||
|
removeListener(listener) {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {*=} args
|
||||||
|
*/
|
||||||
|
dispatch(args) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
(async () => {
|
||||||
|
console.log(`${this.type}: ${args.constructor.name} => ${listener.name}`);
|
||||||
|
await listener(this, args);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
js/lib/chartist-plugin-axistitle.min.js
vendored
Normal file
2
js/lib/chartist-plugin-axistitle.min.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
!function(a,b){"function"==typeof define&&define.amd?define(["chartist"],function(c){return a.returnExportsGlobal=b(c)}):"object"==typeof exports?module.exports=b(require("chartist")):a["Chartist.plugins.ctAxisTitle"]=b(Chartist)}(this,function(a){return function(a,b,c){"use strict";var d={axisTitle:"",axisClass:"ct-axis-title",offset:{x:0,y:0},textAnchor:"middle",flipTitle:!1},e={axisX:d,axisY:d},f=function(a){return a instanceof Function?a():a},g=function(a){return a instanceof Function?a():a};c.plugins=c.plugins||{},c.plugins.ctAxisTitle=function(a){return a=c.extend({},e,a),function(b){b.on("created",function(b){if(!a.axisX.axisTitle&&!a.axisY.axisTitle)throw new Error("ctAxisTitle plugin - You must provide at least one axis title");if(!b.axisX&&!b.axisY)throw new Error("ctAxisTitle plugin can only be used on charts that have at least one axis");var d,e,h,i=c.normalizePadding(b.options.chartPadding);if(a.axisX.axisTitle&&b.axisX&&(d=b.axisX.axisLength/2+b.options.axisY.offset+i.left,e=i.top,"end"===b.options.axisY.position&&(d-=b.options.axisY.offset),"end"===b.options.axisX.position&&(e+=b.axisY.axisLength),h=new c.Svg("text"),h.addClass(g(a.axisX.axisClass)),h.text(f(a.axisX.axisTitle)),h.attr({x:d+a.axisX.offset.x,y:e+a.axisX.offset.y,"text-anchor":a.axisX.textAnchor}),b.svg.append(h,!0)),a.axisY.axisTitle&&b.axisY){d=0,e=b.axisY.axisLength/2+i.top,"start"===b.options.axisX.position&&(e+=b.options.axisX.offset),"end"===b.options.axisY.position&&(d=b.axisX.axisLength);var j="rotate("+(a.axisY.flipTitle?-90:90)+", "+d+", "+e+")";h=new c.Svg("text"),h.addClass(g(a.axisY.axisClass)),h.text(f(a.axisY.axisTitle)),h.attr({x:d+a.axisY.offset.x,y:e+a.axisY.offset.y,transform:j,"text-anchor":a.axisY.textAnchor}),b.svg.append(h,!0)}})}}}(window,document,a),a.plugins.ctAxisTitle});
|
||||||
|
//# sourceMappingURL=chartist-plugin-axistitle.min.js.map
|
1
js/lib/chartist-plugin-axistitle.min.js.map
Normal file
1
js/lib/chartist-plugin-axistitle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
10
js/lib/chartist.min.js
vendored
Normal file
10
js/lib/chartist.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
js/lib/chartist.min.js.map
Normal file
1
js/lib/chartist.min.js.map
Normal file
File diff suppressed because one or more lines are too long
232
js/list.js
Normal file
232
js/list.js
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import uData from './data.js';
|
||||||
|
import uEvent from './event.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class uList
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
export default class uList {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} selector
|
||||||
|
* @param {uBinder} binder
|
||||||
|
* @param {Class<T>} type
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
constructor(selector, binder, type) {
|
||||||
|
/** @type {T[]} */
|
||||||
|
this.data = [];
|
||||||
|
/** @type {uBinder} */
|
||||||
|
this.binder = binder;
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.showAllOption = false;
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.hasHead = false;
|
||||||
|
this.headValue = '';
|
||||||
|
this.allValue = '';
|
||||||
|
/** @type {(T|uData)} */
|
||||||
|
this.T = type || uData;
|
||||||
|
/** @type {HTMLSelectElement} */
|
||||||
|
this.domElement = document.querySelector(selector);
|
||||||
|
if (this.domElement) {
|
||||||
|
this.domElement.addEventListener('change', this, false);
|
||||||
|
}
|
||||||
|
if (this.binder) {
|
||||||
|
this.binder.addEventListener(uEvent.ADD, this);
|
||||||
|
this.binder.addEventListener(uEvent.EDIT, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
this.selectedId = '';
|
||||||
|
this.fromDom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {(T|null)}
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
get current() {
|
||||||
|
const i = parseInt(this.selectedId);
|
||||||
|
if (!isNaN(i)) {
|
||||||
|
return this.data.find((item) => item.key === i);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isSelectedAllOption() {
|
||||||
|
return this.selectedId === 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @param {boolean=} skipUpdate
|
||||||
|
*/
|
||||||
|
select(id, skipUpdate) {
|
||||||
|
this.selectedId = id.toString();
|
||||||
|
this.render();
|
||||||
|
if (!skipUpdate) {
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.domElement.options.length = 0;
|
||||||
|
this.data.length = 0;
|
||||||
|
this.selectedId = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list from XML structure
|
||||||
|
* @param {Element|Document} xml
|
||||||
|
* @param {string} key Name of key node
|
||||||
|
* @param {string} value Name of value node
|
||||||
|
*/
|
||||||
|
fromXml(xml, key, value) {
|
||||||
|
if (!xml) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const item of xml) {
|
||||||
|
const row = new this.T(uUtils.getNodeAsInt(item, key), uUtils.getNode(item, value));
|
||||||
|
this.updateDataRow(row);
|
||||||
|
row.binder = this.binder;
|
||||||
|
this.data.push(row);
|
||||||
|
}
|
||||||
|
if (this.data.length) {
|
||||||
|
this.selectedId = this.data[0].key.toString();
|
||||||
|
}
|
||||||
|
/** @todo set defaults ?? */
|
||||||
|
// var defaultTrack = tid || getNodeAsInt(tracks[0], 'trackid');
|
||||||
|
// var defaultUser = uid || ns.userId;
|
||||||
|
this.render();
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize list from DOM select element options
|
||||||
|
*/
|
||||||
|
fromDom() {
|
||||||
|
if (!this.domElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const option of this.domElement) {
|
||||||
|
if (option.value === 'all') {
|
||||||
|
this.showAllOption = true;
|
||||||
|
} else if (!option.disabled) {
|
||||||
|
const row = new this.T(parseInt(option.value), option.innerText);
|
||||||
|
this.updateDataRow(row);
|
||||||
|
row.binder = this.binder;
|
||||||
|
this.data.push(row);
|
||||||
|
}
|
||||||
|
if (option.selected) {
|
||||||
|
this.selectedId = option.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(Event|uEvent)} event
|
||||||
|
* @param {*=} eventData
|
||||||
|
*/
|
||||||
|
handleEvent(event, eventData) {
|
||||||
|
if (event.type === 'change') {
|
||||||
|
this.selectedId = this.domElement.options[this.domElement.selectedIndex].value;
|
||||||
|
this.onChange();
|
||||||
|
} else if (event.type === uEvent.EDIT && this.domElement === eventData) {
|
||||||
|
this.onEdit();
|
||||||
|
} else if (event.type === uEvent.ADD && this.domElement === eventData) {
|
||||||
|
this.onAdd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @param {T[]} data
|
||||||
|
// */
|
||||||
|
// set list(data) {
|
||||||
|
// this.data = data;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add item
|
||||||
|
* @param {T} item
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
add(item) {
|
||||||
|
this.data.push(item);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
has(id) {
|
||||||
|
return this.data.findIndex((o) => o.key === id) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove item
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
remove(id) {
|
||||||
|
const currentId = this.current.key;
|
||||||
|
this.data.splice(this.data.findIndex((o) => o.key === id), 1);
|
||||||
|
if (id === currentId) {
|
||||||
|
if (this.data.length) {
|
||||||
|
this.selectedId = this.data[0].key.toString();
|
||||||
|
} else {
|
||||||
|
this.selectedId = '';
|
||||||
|
}
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.domElement.options.length = 0;
|
||||||
|
if (this.hasHead) {
|
||||||
|
const head = new Option(this.headValue, '0', true, this.selectedId === '0');
|
||||||
|
head.disabled = true;
|
||||||
|
this.domElement.options.add(head);
|
||||||
|
}
|
||||||
|
if (this.showAllOption) {
|
||||||
|
this.domElement.options.add(new Option(this.allValue, 'all'));
|
||||||
|
}
|
||||||
|
for (const item of this.data) {
|
||||||
|
this.domElement.options.add(new Option(item.value, item.key.toString(), false, item.key.toString() === this.selectedId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {T} row
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars,no-empty-function,class-methods-use-this
|
||||||
|
updateDataRow(row) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars,no-empty-function,class-methods-use-this
|
||||||
|
onChange() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars,no-empty-function,class-methods-use-this
|
||||||
|
onEdit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars,no-empty-function,class-methods-use-this
|
||||||
|
onAdd() {
|
||||||
|
}
|
||||||
|
}
|
1231
js/main.js
1231
js/main.js
File diff suppressed because it is too large
Load Diff
110
js/map.js
Normal file
110
js/map.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import * as gmApi from './mapapi/api_gmaps.js';
|
||||||
|
import * as olApi from './mapapi/api_openlayers.js';
|
||||||
|
import { config, lang } from './constants.js';
|
||||||
|
import uEvent from './event.js';
|
||||||
|
import { uLogger } from './ulogger.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class uMap
|
||||||
|
* @property {number} loadTime
|
||||||
|
* @property {?Array<number>} savedBounds
|
||||||
|
* @property {?(gmApi|olApi)} api
|
||||||
|
* @property {?HTMLElement} mapElement
|
||||||
|
*/
|
||||||
|
export default class uMap {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uBinder} binder
|
||||||
|
*/
|
||||||
|
constructor(binder) {
|
||||||
|
binder.addEventListener(uEvent.TRACK_READY, this);
|
||||||
|
binder.addEventListener(uEvent.UI_READY, this);
|
||||||
|
binder.addEventListener(uEvent.API_CHANGE, this);
|
||||||
|
this.loadTime = 0;
|
||||||
|
this.savedBounds = null;
|
||||||
|
this.api = null;
|
||||||
|
this.mapElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic change of map api
|
||||||
|
* @param {string=} apiName API name
|
||||||
|
*/
|
||||||
|
loadMapAPI(apiName) {
|
||||||
|
if (apiName) {
|
||||||
|
config.mapapi = apiName;
|
||||||
|
try {
|
||||||
|
this.savedBounds = this.api.getBounds();
|
||||||
|
} catch (e) {
|
||||||
|
this.savedBounds = null;
|
||||||
|
}
|
||||||
|
this.api.cleanup();
|
||||||
|
}
|
||||||
|
if (config.mapapi === 'gmaps') {
|
||||||
|
this.api = gmApi;
|
||||||
|
} else {
|
||||||
|
this.api = olApi;
|
||||||
|
}
|
||||||
|
this.waitAndInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to initialize map engine
|
||||||
|
*/
|
||||||
|
waitAndInit() {
|
||||||
|
// wait till main api loads
|
||||||
|
if (this.loadTime > 10000) {
|
||||||
|
this.loadTime = 0;
|
||||||
|
alert(uUtils.sprintf(lang.strings['apifailure'], config.mapapi));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.api.init(this.mapElement);
|
||||||
|
} catch (e) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadTime += 50;
|
||||||
|
this.waitAndInit();
|
||||||
|
}, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loadTime = 0;
|
||||||
|
let update = 1;
|
||||||
|
if (this.savedBounds) {
|
||||||
|
this.api.zoomToBounds(this.savedBounds);
|
||||||
|
update = 0;
|
||||||
|
}
|
||||||
|
// if (latest && isSelectedAllUsers()) {
|
||||||
|
// loadLastPositionAllUsers();
|
||||||
|
// } else {
|
||||||
|
// loadTrack(ns.userId, ns.trackId, update);
|
||||||
|
uLogger.trackList.onChange();
|
||||||
|
// }
|
||||||
|
// save current api as default
|
||||||
|
uUtils.setCookie('api', config.mapapi, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {uEvent} event
|
||||||
|
* @param {*=} args
|
||||||
|
*/
|
||||||
|
handleEvent(event, args) {
|
||||||
|
if (event.type === uEvent.TRACK_READY) {
|
||||||
|
const track = args;
|
||||||
|
this.api.clearMap();
|
||||||
|
/** @todo use update */
|
||||||
|
const update = 1;
|
||||||
|
this.api.displayTrack(track, update);
|
||||||
|
} else if (event.type === uEvent.UI_READY) {
|
||||||
|
/** @type {uUI} */
|
||||||
|
const ui = args;
|
||||||
|
this.mapElement = ui.map;
|
||||||
|
this.loadMapAPI();
|
||||||
|
} else if (event.type === uEvent.API_CHANGE) {
|
||||||
|
/** @type {string} */
|
||||||
|
const api = args;
|
||||||
|
this.loadMapAPI(api);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
302
js/mapapi/api_gmaps.js
Normal file
302
js/mapapi/api_gmaps.js
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
/* μ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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { config, lang } from '../constants.js';
|
||||||
|
import { uLogger } from '../ulogger.js';
|
||||||
|
import uUI from '../ui.js';
|
||||||
|
import uUtils from '../utils.js';
|
||||||
|
|
||||||
|
// google maps
|
||||||
|
|
||||||
|
/** @type {google.maps.Map} */
|
||||||
|
let map = null;
|
||||||
|
/** @type {google.maps.Polyline[]} */
|
||||||
|
const polies = [];
|
||||||
|
/** @type {google.maps.Marker[]} */
|
||||||
|
const markers = [];
|
||||||
|
/** @type {google.maps.InfoWindow[]} */
|
||||||
|
const popups = [];
|
||||||
|
/** @type {google.maps.InfoWindow} */
|
||||||
|
let popup = null;
|
||||||
|
/** @type {google.maps.PolylineOptions} */
|
||||||
|
let polyOptions = null;
|
||||||
|
/** @type {google.maps.MapOptions} */
|
||||||
|
let mapOptions = null;
|
||||||
|
/** @type {number} */
|
||||||
|
let timeoutHandle = 0;
|
||||||
|
const name = 'gmaps';
|
||||||
|
let isLoaded = false;
|
||||||
|
let authError = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize map
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
function init(el) {
|
||||||
|
const url = '//maps.googleapis.com/maps/api/js?' + ((config.gkey != null) ? ('key=' + config.gkey + '&') : '') + 'callback=gm_loaded';
|
||||||
|
uUtils.addScript(url, 'mapapi_gmaps');
|
||||||
|
if (!isLoaded) {
|
||||||
|
throw new Error('Google Maps API not ready');
|
||||||
|
}
|
||||||
|
start(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start map engine when loaded
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
function start(el) {
|
||||||
|
if (authError) {
|
||||||
|
window.gm_authFailure();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
google.maps.visualRefresh = true;
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
polyOptions = {
|
||||||
|
strokeColor: config.strokeColor,
|
||||||
|
strokeOpacity: config.strokeOpacity,
|
||||||
|
strokeWeight: config.strokeWeight
|
||||||
|
};
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
mapOptions = {
|
||||||
|
center: new google.maps.LatLng(config.init_latitude, config.init_longitude),
|
||||||
|
zoom: 8,
|
||||||
|
mapTypeId: google.maps.MapTypeId.ROADMAP,
|
||||||
|
scaleControl: true
|
||||||
|
};
|
||||||
|
map = new google.maps.Map(el, mapOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up API
|
||||||
|
*/
|
||||||
|
function cleanup() {
|
||||||
|
polies.length = 0;
|
||||||
|
markers.length = 0;
|
||||||
|
popups.length = 0;
|
||||||
|
map = null;
|
||||||
|
polyOptions = null;
|
||||||
|
mapOptions = null;
|
||||||
|
popup = null;
|
||||||
|
// ui.clearMapCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display track
|
||||||
|
* @param {boolean} update Should fit bounds if true
|
||||||
|
*/
|
||||||
|
function displayTrack(update) {
|
||||||
|
const track = uLogger.trackList.current;
|
||||||
|
if (!track) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// init polyline
|
||||||
|
const poly = new google.maps.Polyline(polyOptions);
|
||||||
|
poly.setMap(map);
|
||||||
|
const path = poly.getPath();
|
||||||
|
const latlngbounds = new google.maps.LatLngBounds();
|
||||||
|
let i = 0;
|
||||||
|
for (const position of track.positions) {
|
||||||
|
// set marker
|
||||||
|
setMarker(i++);
|
||||||
|
// update polyline
|
||||||
|
const coordinates = new google.maps.LatLng(position.latitude, position.longitude);
|
||||||
|
path.push(coordinates);
|
||||||
|
latlngbounds.extend(coordinates);
|
||||||
|
}
|
||||||
|
if (update) {
|
||||||
|
map.fitBounds(latlngbounds);
|
||||||
|
if (i === 1) {
|
||||||
|
// only one point, zoom out
|
||||||
|
const zListener =
|
||||||
|
google.maps.event.addListenerOnce(map, 'bounds_changed', function () {
|
||||||
|
if (this.getZoom()) {
|
||||||
|
this.setZoom(15);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTimeout(function () { google.maps.event.removeListener(zListener) }, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
polies.push(poly);
|
||||||
|
|
||||||
|
/** @todo handle summary and chart in track */
|
||||||
|
// ns.updateSummary(p.timestamp, totalDistance, totalSeconds);
|
||||||
|
// ns.updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear map
|
||||||
|
*/
|
||||||
|
function clearMap() {
|
||||||
|
if (polies) {
|
||||||
|
for (let i = 0; i < polies.length; i++) {
|
||||||
|
polies[i].setMap(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (markers) {
|
||||||
|
for (let 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.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set marker
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
function setMarker(id) {
|
||||||
|
// marker
|
||||||
|
const position = uLogger.trackList.current.positions[id];
|
||||||
|
const posLen = uLogger.trackList.current.length;
|
||||||
|
// 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: map
|
||||||
|
});
|
||||||
|
if (config.showLatest) {
|
||||||
|
marker.setIcon('images/marker-red.png');
|
||||||
|
} else if (id === 0) {
|
||||||
|
marker.setIcon('images/marker-green.png');
|
||||||
|
} else if (id === posLen - 1) {
|
||||||
|
marker.setIcon('images/marker-red.png');
|
||||||
|
} else {
|
||||||
|
marker.setIcon('images/marker-white.png');
|
||||||
|
}
|
||||||
|
// popup
|
||||||
|
const content = uUI.getPopupHtml(id);
|
||||||
|
popup = new google.maps.InfoWindow();
|
||||||
|
// noinspection JSUndefinedPropertyAssignment
|
||||||
|
popup.listener = google.maps.event.addListener(marker, 'click', (function (_marker, _content) {
|
||||||
|
return function () {
|
||||||
|
popup.setContent(_content);
|
||||||
|
popup.open(map, _marker);
|
||||||
|
/** @todo handle chart */
|
||||||
|
// ns.chartShowPosition(id);
|
||||||
|
}
|
||||||
|
})(marker, content));
|
||||||
|
markers.push(marker);
|
||||||
|
popups.push(popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add listener on chart to show position on map
|
||||||
|
* @param {google.visualization.LineChart} chart
|
||||||
|
* @param {google.visualization.DataTable} data
|
||||||
|
*/
|
||||||
|
function addChartEvent(chart, data) {
|
||||||
|
google.visualization.events.addListener(chart, 'select', function () {
|
||||||
|
if (popup) { popup.close(); clearTimeout(timeoutHandle); }
|
||||||
|
const selection = chart.getSelection()[0];
|
||||||
|
if (selection) {
|
||||||
|
const id = data.getValue(selection.row, 0) - 1;
|
||||||
|
const icon = markers[id].getIcon();
|
||||||
|
markers[id].setIcon('images/marker-gold.png');
|
||||||
|
timeoutHandle = setTimeout(function () { markers[id].setIcon(icon); }, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get map bounds
|
||||||
|
* eg. ((52.20105108685229, 20.789387865580238), (52.292069558807135, 21.172192736185707))
|
||||||
|
* @returns {number[]} Bounds
|
||||||
|
*/
|
||||||
|
function getBounds() {
|
||||||
|
const bounds = 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
|
||||||
|
*/
|
||||||
|
function zoomToExtent() {
|
||||||
|
const latlngbounds = new google.maps.LatLngBounds();
|
||||||
|
for (let i = 0; i < markers.length; i++) {
|
||||||
|
const coordinates = new google.maps.LatLng(markers[i].position.lat(), markers[i].position.lng());
|
||||||
|
latlngbounds.extend(coordinates);
|
||||||
|
}
|
||||||
|
map.fitBounds(latlngbounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom to bounds
|
||||||
|
* @param {number[]} bounds
|
||||||
|
*/
|
||||||
|
function 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);
|
||||||
|
map.fitBounds(latLngBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update size
|
||||||
|
*/
|
||||||
|
function updateSize() {
|
||||||
|
// ignore for google API
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAuthError() { authError = true; }
|
||||||
|
function setLoaded() { isLoaded = true; }
|
||||||
|
|
||||||
|
export {
|
||||||
|
name,
|
||||||
|
init,
|
||||||
|
cleanup,
|
||||||
|
displayTrack,
|
||||||
|
clearMap,
|
||||||
|
setMarker,
|
||||||
|
addChartEvent,
|
||||||
|
getBounds,
|
||||||
|
zoomToExtent,
|
||||||
|
zoomToBounds,
|
||||||
|
updateSize,
|
||||||
|
setAuthError,
|
||||||
|
setLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for Google Maps API
|
||||||
|
* It will be called when authentication fails
|
||||||
|
*/
|
||||||
|
window.gm_authFailure = function () {
|
||||||
|
setAuthError();
|
||||||
|
let message = uUtils.sprintf(lang.strings['apifailure'], 'Google Maps');
|
||||||
|
message += '<br><br>' + lang.strings['gmauthfailure'];
|
||||||
|
message += '<br><br>' + lang.strings['gmapilink'];
|
||||||
|
uUI.resolveModal(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for Google Maps API
|
||||||
|
* It will be called when API is loaded
|
||||||
|
*/
|
||||||
|
window.gm_loaded = function () {
|
||||||
|
setLoaded();
|
||||||
|
};
|
475
js/mapapi/api_openlayers.js
Normal file
475
js/mapapi/api_openlayers.js
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
/* μ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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { config } from '../constants.js';
|
||||||
|
import { uLogger } from '../ulogger.js';
|
||||||
|
import uUI from '../ui.js';
|
||||||
|
import uUtils from '../utils.js';
|
||||||
|
|
||||||
|
// openlayers 3+
|
||||||
|
|
||||||
|
/** @type {ol.Map} */
|
||||||
|
let map = null;
|
||||||
|
/** @type {ol.layer.Vector} */
|
||||||
|
let layerTrack = null;
|
||||||
|
/** @type {ol.layer.Vector} */
|
||||||
|
let layerMarkers = null;
|
||||||
|
/** @type {ol.layer.Base} */
|
||||||
|
let selectedLayer = null;
|
||||||
|
/** @type {ol.style.Style|{}} */
|
||||||
|
let olStyles = {};
|
||||||
|
const name = 'openlayers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize map
|
||||||
|
*/
|
||||||
|
function init(target) {
|
||||||
|
|
||||||
|
uUtils.addScript('//cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList', 'mapapi_openlayers_polyfill');
|
||||||
|
uUtils.addScript('js/lib/ol.js', 'mapapi_openlayers');
|
||||||
|
uUtils.addCss('css/ol.css', 'ol_css');
|
||||||
|
|
||||||
|
const controls = [
|
||||||
|
new ol.control.Zoom(),
|
||||||
|
new ol.control.Rotate(),
|
||||||
|
new ol.control.ScaleLine(),
|
||||||
|
new ol.control.ZoomToExtent({ label: getExtentImg() })
|
||||||
|
];
|
||||||
|
|
||||||
|
const view = new ol.View({
|
||||||
|
center: ol.proj.fromLonLat([ config.init_longitude, config.init_latitude ]),
|
||||||
|
zoom: 8
|
||||||
|
});
|
||||||
|
|
||||||
|
map = new ol.Map({
|
||||||
|
target: target,
|
||||||
|
controls: controls,
|
||||||
|
view: view
|
||||||
|
});
|
||||||
|
|
||||||
|
// default layer: OpenStreetMap
|
||||||
|
const osm = new ol.layer.Tile({
|
||||||
|
name: 'OpenStreetMap',
|
||||||
|
visible: true,
|
||||||
|
source: new ol.source.OSM()
|
||||||
|
});
|
||||||
|
map.addLayer(osm);
|
||||||
|
selectedLayer = osm;
|
||||||
|
|
||||||
|
// add extra layers
|
||||||
|
for (const layerName in config.ol_layers) {
|
||||||
|
if (config.ol_layers.hasOwnProperty(layerName)) {
|
||||||
|
const layerUrl = config.ol_layers[layerName];
|
||||||
|
const ol_layer = new ol.layer.Tile({
|
||||||
|
name: layerName,
|
||||||
|
visible: false,
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: layerUrl
|
||||||
|
})
|
||||||
|
});
|
||||||
|
map.addLayer(ol_layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init layers
|
||||||
|
const lineStyle = new ol.style.Style({
|
||||||
|
stroke: new ol.style.Stroke({
|
||||||
|
color: uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity),
|
||||||
|
width: config.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 = {};
|
||||||
|
const iconRed = new ol.style.Icon({
|
||||||
|
anchor: [ 0.5, 1 ],
|
||||||
|
src: 'images/marker-red.png'
|
||||||
|
});
|
||||||
|
const iconGreen = new ol.style.Icon({
|
||||||
|
anchor: [ 0.5, 1 ],
|
||||||
|
src: 'images/marker-green.png'
|
||||||
|
});
|
||||||
|
const iconWhite = new ol.style.Icon({
|
||||||
|
anchor: [ 0.5, 1 ],
|
||||||
|
opacity: 0.7,
|
||||||
|
src: 'images/marker-white.png'
|
||||||
|
});
|
||||||
|
const 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
|
||||||
|
const popupContainer = document.createElement('div');
|
||||||
|
popupContainer.id = 'popup';
|
||||||
|
popupContainer.className = 'ol-popup';
|
||||||
|
document.body.appendChild(popupContainer);
|
||||||
|
const popupCloser = document.createElement('a');
|
||||||
|
popupCloser.id = 'popup-closer';
|
||||||
|
popupCloser.className = 'ol-popup-closer';
|
||||||
|
popupCloser.href = '#';
|
||||||
|
popupContainer.appendChild(popupCloser);
|
||||||
|
const popupContent = document.createElement('div');
|
||||||
|
popupContent.id = 'popup-content';
|
||||||
|
popupContainer.appendChild(popupContent);
|
||||||
|
|
||||||
|
const popup = new ol.Overlay({
|
||||||
|
element: popupContainer,
|
||||||
|
autoPan: true,
|
||||||
|
autoPanAnimation: {
|
||||||
|
duration: 250
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
popupCloser.onclick = () => {
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
popup.setPosition(undefined);
|
||||||
|
popupCloser.blur();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// add click handler to map to show popup
|
||||||
|
map.on('click', (e) => {
|
||||||
|
const coordinate = e.coordinate;
|
||||||
|
const feature = map.forEachFeatureAtPixel(e.pixel,
|
||||||
|
(_feature, _layer) => {
|
||||||
|
if (_layer.get('name') === 'Markers') {
|
||||||
|
return _feature;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (feature) {
|
||||||
|
// popup show
|
||||||
|
popup.setPosition(coordinate);
|
||||||
|
popupContent.innerHTML = uUI.getPopupHtml(feature.getId());
|
||||||
|
map.addOverlay(popup);
|
||||||
|
// ns.chartShowPosition(id);
|
||||||
|
} else {
|
||||||
|
// popup destroy
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
popup.setPosition(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// change mouse cursor when over marker
|
||||||
|
map.on('pointermove', function({ pixel }) {
|
||||||
|
const hit = map.forEachFeatureAtPixel(pixel, (_feature, _layer) => _layer.get('name') === 'Markers');
|
||||||
|
if (hit) {
|
||||||
|
this.getTargetElement().style.cursor = 'pointer';
|
||||||
|
} else {
|
||||||
|
this.getTargetElement().style.cursor = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// layer switcher
|
||||||
|
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);
|
||||||
|
|
||||||
|
map.getLayers().forEach((_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 = switchLayer;
|
||||||
|
if (_layer.getVisible()) {
|
||||||
|
layerRadio.checked = true;
|
||||||
|
}
|
||||||
|
layerLabel.insertBefore(layerRadio, layerLabel.childNodes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchLayer() {
|
||||||
|
const targetName = this.value;
|
||||||
|
map.getLayers().forEach((_layer) => {
|
||||||
|
if (_layer.get('name') === targetName) {
|
||||||
|
if (_layer.get('type') === 'data') {
|
||||||
|
if (_layer.getVisible()) {
|
||||||
|
_layer.setVisible(false);
|
||||||
|
} else {
|
||||||
|
_layer.setVisible(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedLayer.setVisible(false);
|
||||||
|
selectedLayer = _layer;
|
||||||
|
_layer.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const switcherButton = document.createElement('button');
|
||||||
|
const layerImg = document.createElement('img');
|
||||||
|
layerImg.src = 'images/layers.svg';
|
||||||
|
layerImg.style.width = '60%';
|
||||||
|
switcherButton.appendChild(layerImg);
|
||||||
|
|
||||||
|
const switcherHandle = () => {
|
||||||
|
const 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);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
map.addControl(switcherControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up API
|
||||||
|
*/
|
||||||
|
function cleanup() {
|
||||||
|
map = null;
|
||||||
|
layerTrack = null;
|
||||||
|
layerMarkers = null;
|
||||||
|
selectedLayer = null;
|
||||||
|
olStyles = null;
|
||||||
|
uUI.removeElementById('popup');
|
||||||
|
uUI.removeElementById('switcher');
|
||||||
|
// ui.clearMapCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display track
|
||||||
|
* @param {boolean} update Should fit bounds if true
|
||||||
|
*/
|
||||||
|
function displayTrack(update) {
|
||||||
|
const track = uLogger.trackList.current;
|
||||||
|
if (!track) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const points = [];
|
||||||
|
let i = 0;
|
||||||
|
for (const position of track.positions) {
|
||||||
|
// set marker
|
||||||
|
setMarker(i++);
|
||||||
|
// update polyline
|
||||||
|
const point = ol.proj.fromLonLat([ position.longitude, position.latitude ]);
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
const lineString = new ol.geom.LineString(points);
|
||||||
|
|
||||||
|
const lineFeature = new ol.Feature({
|
||||||
|
geometry: lineString
|
||||||
|
});
|
||||||
|
|
||||||
|
layerTrack.getSource().addFeature(lineFeature);
|
||||||
|
|
||||||
|
let extent = layerTrack.getSource().getExtent();
|
||||||
|
|
||||||
|
map.getControls().forEach((el) => {
|
||||||
|
if (el instanceof ol.control.ZoomToExtent) {
|
||||||
|
map.removeControl(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
map.getView().fit(extent);
|
||||||
|
const zoom = map.getView().getZoom();
|
||||||
|
if (zoom > 20) {
|
||||||
|
map.getView().setZoom(20);
|
||||||
|
extent = map.getView().calculateExtent(map.getSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomToExtentControl = new ol.control.ZoomToExtent({
|
||||||
|
extent,
|
||||||
|
label: getExtentImg()
|
||||||
|
});
|
||||||
|
map.addControl(zoomToExtentControl);
|
||||||
|
|
||||||
|
/** @todo handle summary and chart in track */
|
||||||
|
|
||||||
|
// ns.updateSummary(p.timestamp, totalDistance, totalSeconds);
|
||||||
|
// ns.updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear map
|
||||||
|
*/
|
||||||
|
function clearMap() {
|
||||||
|
if (layerTrack) {
|
||||||
|
layerTrack.getSource().clear();
|
||||||
|
}
|
||||||
|
if (layerMarkers) {
|
||||||
|
layerMarkers.getSource().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set marker
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
function setMarker(id) {
|
||||||
|
// marker
|
||||||
|
const position = uLogger.trackList.current.positions[id];
|
||||||
|
const posLen = uLogger.trackList.current.positions.length;
|
||||||
|
const marker = new ol.Feature({
|
||||||
|
geometry: new ol.geom.Point(ol.proj.fromLonLat([ position.longitude, position.latitude ]))
|
||||||
|
});
|
||||||
|
|
||||||
|
let iconStyle;
|
||||||
|
if (config.showLatest) {
|
||||||
|
iconStyle = olStyles['red'];
|
||||||
|
} else if (id === 0) {
|
||||||
|
iconStyle = olStyles['green'];
|
||||||
|
} else if (id === posLen - 1) {
|
||||||
|
iconStyle = olStyles['red'];
|
||||||
|
} else {
|
||||||
|
iconStyle = olStyles['white'];
|
||||||
|
}
|
||||||
|
marker.setStyle(iconStyle);
|
||||||
|
marker.setId(id);
|
||||||
|
/** @todo why set position in marker? ID should be enough */
|
||||||
|
marker.set('p', position);
|
||||||
|
marker.set('posLen', posLen);
|
||||||
|
layerMarkers.getSource().addFeature(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add listener on chart to show position on map
|
||||||
|
* @param {google.visualization.LineChart} chart
|
||||||
|
* @param {google.visualization.DataTable} data
|
||||||
|
*/
|
||||||
|
function addChartEvent(chart, data) {
|
||||||
|
google.visualization.events.addListener(chart, 'select', () => {
|
||||||
|
const selection = chart.getSelection()[0];
|
||||||
|
if (selection) {
|
||||||
|
const id = data.getValue(selection.row, 0) - 1;
|
||||||
|
const marker = layerMarkers.getSource().getFeatureById(id);
|
||||||
|
const initStyle = marker.getStyle();
|
||||||
|
const iconStyle = olStyles['gold'];
|
||||||
|
marker.setStyle(iconStyle);
|
||||||
|
setTimeout(() => {
|
||||||
|
marker.setStyle(initStyle);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get map bounds
|
||||||
|
* eg. (20.597985430276808, 52.15547181298076, 21.363595171488573, 52.33750879522563)
|
||||||
|
* @returns {number[]} Bounds
|
||||||
|
*/
|
||||||
|
function getBounds() {
|
||||||
|
const extent = map.getView().calculateExtent(map.getSize());
|
||||||
|
const bounds = ol.proj.transformExtent(extent, 'EPSG:900913', 'EPSG:4326');
|
||||||
|
const lon_sw = bounds[0];
|
||||||
|
const lat_sw = bounds[1];
|
||||||
|
const lon_ne = bounds[2];
|
||||||
|
const lat_ne = bounds[3];
|
||||||
|
return [ lon_sw, lat_sw, lon_ne, lat_ne ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom to track extent
|
||||||
|
*/
|
||||||
|
function zoomToExtent() {
|
||||||
|
map.getView().fit(layerMarkers.getSource().getExtent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom to bounds
|
||||||
|
* @param {number[]} bounds
|
||||||
|
*/
|
||||||
|
function zoomToBounds(bounds) {
|
||||||
|
const extent = ol.proj.transformExtent(bounds, 'EPSG:4326', 'EPSG:900913');
|
||||||
|
map.getView().fit(extent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update size
|
||||||
|
*/
|
||||||
|
function updateSize() {
|
||||||
|
map.updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get extent image
|
||||||
|
* @returns {HTMLImageElement}
|
||||||
|
*/
|
||||||
|
function getExtentImg() {
|
||||||
|
const extentImg = document.createElement('img');
|
||||||
|
extentImg.src = 'images/extent.svg';
|
||||||
|
extentImg.style.width = '60%';
|
||||||
|
return extentImg;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
name,
|
||||||
|
init,
|
||||||
|
cleanup,
|
||||||
|
displayTrack,
|
||||||
|
clearMap,
|
||||||
|
setMarker,
|
||||||
|
addChartEvent,
|
||||||
|
getBounds,
|
||||||
|
zoomToExtent,
|
||||||
|
zoomToBounds,
|
||||||
|
updateSize
|
||||||
|
}
|
126
js/modal.js
Normal file
126
js/modal.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { lang } from './constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ModalResult
|
||||||
|
* @property {boolean} cancelled Was dialog cancelled
|
||||||
|
* @property {string} [action] Click action name
|
||||||
|
* @property {Object} [data] Additional data
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback ModalCallback
|
||||||
|
* @param {ModalResult} result
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class uModal {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds modal dialog
|
||||||
|
* Positive click handlers bound to elements with class 'button-resolve'.
|
||||||
|
* Negative click handlers bound to elements with class 'button-reject'.
|
||||||
|
* Optional attribute 'data-action' value is returned in {@link ModalResult.action}
|
||||||
|
* @param {(string|Node|NodeList|Array.<Node>)} content
|
||||||
|
*/
|
||||||
|
constructor(content) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.setAttribute('id', 'modal');
|
||||||
|
const modalHeader = document.createElement('div');
|
||||||
|
modalHeader.setAttribute('id', 'modal-header');
|
||||||
|
const buttonClose = document.createElement('button');
|
||||||
|
buttonClose.setAttribute('id', 'modal-close');
|
||||||
|
buttonClose.setAttribute('type', 'button');
|
||||||
|
buttonClose.setAttribute('class', 'button-reject');
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.setAttribute('src', 'images/close.svg');
|
||||||
|
img.setAttribute('alt', lang.strings['close']);
|
||||||
|
buttonClose.append(img);
|
||||||
|
modalHeader.append(buttonClose);
|
||||||
|
modal.append(modalHeader);
|
||||||
|
const modalBody = document.createElement('div');
|
||||||
|
modalBody.setAttribute('id', 'modal-body');
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
modalBody.innerHTML = content;
|
||||||
|
} else if (content instanceof NodeList || content instanceof Array) {
|
||||||
|
for (const node of content) {
|
||||||
|
modalBody.append(node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
modalBody.append(content);
|
||||||
|
}
|
||||||
|
modal.append(modalBody);
|
||||||
|
this._modal = modal;
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
get modal() {
|
||||||
|
return this._modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show modal dialog
|
||||||
|
* @returns {Promise<ModalResult>}
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.addListeners(resolve);
|
||||||
|
if (!this.visible) {
|
||||||
|
document.body.append(this._modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add listeners
|
||||||
|
* @param {ModalCallback} resolve callback
|
||||||
|
*/
|
||||||
|
addListeners(resolve) {
|
||||||
|
this._modal.querySelectorAll('.button-resolve').forEach((el) => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
uModal.onClick(el, resolve, { cancelled: false, action: el.getAttribute('data-action') });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._modal.querySelectorAll('.button-reject').forEach((el) => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
uModal.onClick(el, resolve, { cancelled: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On click action
|
||||||
|
* Handles optional confirmation dialog
|
||||||
|
* @param {Element} el Clicked element
|
||||||
|
* @param {ModalCallback} resolve callback
|
||||||
|
* @param {ModalResult} result
|
||||||
|
*/
|
||||||
|
static onClick(el, resolve, result) {
|
||||||
|
const confirm = el.getAttribute('data-confirm');
|
||||||
|
let proceed = true;
|
||||||
|
if (confirm) {
|
||||||
|
proceed = this.isConfirmed(confirm);
|
||||||
|
}
|
||||||
|
if (proceed) {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show confirmation dialog and return user decision
|
||||||
|
* @param {string} message
|
||||||
|
* @return {boolean} True if confirmed, false otherwise
|
||||||
|
*/
|
||||||
|
static isConfirmed(message) {
|
||||||
|
return confirm(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove modal dialog
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
document.body.removeChild(this._modal);
|
||||||
|
this.visible = false
|
||||||
|
}
|
||||||
|
}
|
77
js/pass.js
77
js/pass.js
@ -1,77 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @namespace */
|
|
||||||
var uLogger = window.uLogger || {};
|
|
||||||
(function (ns) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show change password dialog
|
|
||||||
*/
|
|
||||||
function changePass() {
|
|
||||||
var form = '<form id="passForm" method="post" onsubmit="uLogger.submitPass(); return false">';
|
|
||||||
form += '<label><b>' + ns.lang.strings['oldpassword'] + '</b></label><input type="password" placeholder="' + ns.lang.strings['passwordenter'] + '" name="oldpass" required>';
|
|
||||||
form += '<label><b>' + ns.lang.strings['newpassword'] + '</b></label><input type="password" placeholder="' + ns.lang.strings['passwordenter'] + '" name="pass" required>';
|
|
||||||
form += '<label><b>' + ns.lang.strings['newpasswordrepeat'] + '</b></label><input type="password" placeholder="' + ns.lang.strings['passwordenter'] + '" name="pass2" required>';
|
|
||||||
form += '<button type="button" onclick="uLogger.ui.removeModal()">' + ns.lang.strings['cancel'] + '</button><button type="submit">' + ns.lang.strings['submit'] + '</button>';
|
|
||||||
form += '</form>';
|
|
||||||
ns.ui.showModal(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit password 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(ns.lang.strings['allrequired']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pass !== pass2) {
|
|
||||||
alert(ns.lang.strings['passnotmatch']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ns.config.pass_regex.test(pass)) {
|
|
||||||
alert(ns.lang.strings['passlenmin'] + '\n' + ns.lang.strings['passrules']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ns.post('utils/changepass.php',
|
|
||||||
{
|
|
||||||
oldpass: oldpass,
|
|
||||||
pass: pass
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function () {
|
|
||||||
ns.ui.removeModal();
|
|
||||||
alert(ns.lang.strings['actionsuccess']);
|
|
||||||
},
|
|
||||||
fail: function (message) {
|
|
||||||
alert(ns.lang.strings['actionfailure'] + '\n' + message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// exports
|
|
||||||
ns.changePass = changePass;
|
|
||||||
ns.submitPass = submitPass;
|
|
||||||
|
|
||||||
})(uLogger);
|
|
50
js/position.js
Normal file
50
js/position.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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} username
|
||||||
|
* @property {string} trackname
|
||||||
|
* @property {number} trackid
|
||||||
|
* @property {number} timestamp
|
||||||
|
* @property {number} distance
|
||||||
|
* @property {number} seconds
|
||||||
|
* @property {number} totalDistance
|
||||||
|
* @property {number} totalSeconds
|
||||||
|
*/
|
||||||
|
export default class uPosition {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element|Document} xml
|
||||||
|
* @returns {uPosition}
|
||||||
|
*/
|
||||||
|
static fromXml(xml) {
|
||||||
|
const position = new uPosition();
|
||||||
|
position.id = uUtils.getAttributeAsInt(xml, 'id');
|
||||||
|
position.latitude = uUtils.getNodeAsFloat(xml, 'latitude');
|
||||||
|
position.longitude = uUtils.getNodeAsFloat(xml, 'longitude');
|
||||||
|
position.altitude = uUtils.getNodeAsInt(xml, 'altitude'); // may be null
|
||||||
|
position.speed = uUtils.getNodeAsInt(xml, 'speed'); // may be null
|
||||||
|
position.bearing = uUtils.getNodeAsInt(xml, 'bearing'); // may be null
|
||||||
|
position.accuracy = uUtils.getNodeAsInt(xml, 'accuracy'); // may be null
|
||||||
|
position.provider = uUtils.getNode(xml, 'provider'); // may be null
|
||||||
|
position.comments = uUtils.getNode(xml, 'comments'); // may be null
|
||||||
|
position.username = uUtils.getNode(xml, 'username');
|
||||||
|
position.trackname = uUtils.getNode(xml, 'trackname');
|
||||||
|
position.trackid = uUtils.getNodeAsInt(xml, 'trackid');
|
||||||
|
position.timestamp = uUtils.getNodeAsInt(xml, 'timestamp');
|
||||||
|
position.distance = uUtils.getNodeAsInt(xml, 'distance');
|
||||||
|
position.seconds = uUtils.getNodeAsInt(xml, 'seconds');
|
||||||
|
position.totalDistance = 0;
|
||||||
|
position.totalSeconds = 0;
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
261
js/track.js
261
js/track.js
@ -1,109 +1,176 @@
|
|||||||
/* μlogger
|
import { config } from './constants.js';
|
||||||
*
|
import uAjax from './ajax.js';
|
||||||
* Copyright(C) 2017 Bartek Fabiszewski (www.fabiszewski.net)
|
import uData from './data.js';
|
||||||
*
|
import uEvent from './event.js';
|
||||||
* This is free software; you can redistribute it and/or modify it under
|
import uPosition from './position.js';
|
||||||
* 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.
|
* @class uTrack
|
||||||
*
|
* @extends {uData}
|
||||||
* This program is distributed in the hope that it will be useful, but
|
* @property {number} id
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
* @property {string} name
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
* @property {uUser} user
|
||||||
* General Public License for more details.
|
* @property {?uPosition[]} positions
|
||||||
*
|
* @property {?Array<{x: number, y: number}>} plotData
|
||||||
* 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 uTrack extends uData {
|
||||||
/** @namespace */
|
|
||||||
var uLogger = window.uLogger || {};
|
|
||||||
(function (ns) {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show edit track dialog
|
* @param {number} id
|
||||||
*/
|
|
||||||
function editTrack() {
|
|
||||||
var userForm = ns.ui.userSelect;
|
|
||||||
var trackUser = (userForm) ? userForm.options[userForm.selectedIndex].text : ns.config.auth;
|
|
||||||
if (trackUser !== ns.config.auth && !ns.config.admin) {
|
|
||||||
alert(ns.lang.strings['owntrackswarn']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var trackForm = ns.ui.trackSelect;
|
|
||||||
if (trackForm.selectedIndex < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var trackId = trackForm.options[trackForm.selectedIndex].value;
|
|
||||||
var trackName = trackForm.options[trackForm.selectedIndex].text;
|
|
||||||
var message = '<div style="float:left">' + ns.sprintf(ns.lang.strings['editingtrack'], '<b>' + ns.htmlEncode(trackName) + '</b>') + '</div>';
|
|
||||||
message += '<div class="red-button"><b><a href="javascript:void(0);" onclick="uLogger.submitTrack(\'delete\'); return false">' + ns.lang.strings['deltrack'] + '</a></b></div>';
|
|
||||||
message += '<div style="clear: both; padding-bottom: 1em;"></div>';
|
|
||||||
|
|
||||||
var form = '<form id="trackForm" method="post" onsubmit="uLogger.submitTrack(\'update\'); return false">';
|
|
||||||
form += '<input type="hidden" name="trackid" value="' + trackId + '">';
|
|
||||||
form += '<label><b>' + ns.lang.strings['trackname'] + '</b></label><input type="text" placeholder="' + ns.lang.strings['trackname'] + '" name="trackname" value="' + ns.htmlEncode(trackName) + '" required>';
|
|
||||||
form += '<div class="buttons"><button type="button" onclick="uLogger.ui.removeModal()">' + ns.lang.strings['cancel'] + '</button><button type="submit">' + ns.lang.strings['submit'] + '</button></div>';
|
|
||||||
form += '</form>';
|
|
||||||
ns.ui.showModal(message + form);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show confirmation dialog
|
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @returns {boolean} True if confirmed
|
* @param {uUser} user
|
||||||
*/
|
*/
|
||||||
function confirmedDelete(name) {
|
constructor(id, name, user) {
|
||||||
return confirm(ns.sprintf(ns.lang.strings['trackdelwarn'], '"' + name + '"'));
|
super(id, name, 'id', 'name');
|
||||||
|
this._user = user;
|
||||||
|
this._positions = null;
|
||||||
|
this._plotData = null;
|
||||||
|
this._maxId = 0;
|
||||||
|
this._onlyLatest = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit form dialog
|
* @return {?uPosition[]}
|
||||||
* @param action
|
|
||||||
*/
|
*/
|
||||||
function submitTrack(action) {
|
get positions() {
|
||||||
var form = document.getElementById('trackForm');
|
return this._positions;
|
||||||
var trackId = parseInt(form.elements['trackid'].value);
|
|
||||||
var trackName = form.elements['trackname'].value.trim();
|
|
||||||
if (isNaN(trackId)) {
|
|
||||||
alert(ns.lang.strings['allrequired']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action !== 'delete') {
|
|
||||||
if (!trackName) {
|
|
||||||
alert(ns.lang.strings['allrequired']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (!confirmedDelete(trackName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ns.post('utils/handletrack.php',
|
|
||||||
{
|
|
||||||
action: action,
|
|
||||||
trackid: trackId,
|
|
||||||
trackname: trackName
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function () {
|
|
||||||
ns.ui.removeModal();
|
|
||||||
alert(ns.lang.strings['actionsuccess']);
|
|
||||||
var el = ns.ui.trackSelect;
|
|
||||||
if (action === 'delete') {
|
|
||||||
el.remove(el.selectedIndex);
|
|
||||||
ns.map.clearMap();
|
|
||||||
ns.selectTrack();
|
|
||||||
} else {
|
|
||||||
el.options[el.selectedIndex].innerHTML = ns.htmlEncode(trackName);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: function (message) {
|
|
||||||
alert(ns.lang.strings['actionfailure'] + '\n' + message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ns.editTrack = editTrack;
|
/**
|
||||||
ns.submitTrack = submitTrack;
|
* @param {uUser} user
|
||||||
|
*/
|
||||||
|
set user(user) {
|
||||||
|
this._user = user;
|
||||||
|
}
|
||||||
|
|
||||||
})(uLogger);
|
/**
|
||||||
|
* @return {uUser}
|
||||||
|
*/
|
||||||
|
get user() {
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} value
|
||||||
|
*/
|
||||||
|
set onlyLatest(value) {
|
||||||
|
this._onlyLatest = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._positions = null;
|
||||||
|
this._plotData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get track data from xml
|
||||||
|
* @param {XMLDocument} xml
|
||||||
|
* @param {boolean} isUpdate
|
||||||
|
*/
|
||||||
|
fromXml(xml, isUpdate) {
|
||||||
|
let positions = [];
|
||||||
|
let plotData = [];
|
||||||
|
let totalDistance = 0;
|
||||||
|
let totalSeconds = 0;
|
||||||
|
if (isUpdate && this._positions) {
|
||||||
|
positions = this._positions;
|
||||||
|
plotData = this._plotData;
|
||||||
|
totalDistance = positions[positions.length - 1].totalDistance;
|
||||||
|
totalSeconds = positions[positions.length - 1].totalSeconds;
|
||||||
|
}
|
||||||
|
const xmlPos = xml.getElementsByTagName('position');
|
||||||
|
for (xml of xmlPos) {
|
||||||
|
const position = uPosition.fromXml(xml);
|
||||||
|
totalDistance += position.distance;
|
||||||
|
totalSeconds += position.seconds;
|
||||||
|
position.totalDistance = totalDistance;
|
||||||
|
position.totalSeconds = totalSeconds;
|
||||||
|
positions.push(position);
|
||||||
|
if (position.altitude != null) {
|
||||||
|
plotData.push({ x: position.totalDistance, y: position.altitude * config.factor_m });
|
||||||
|
}
|
||||||
|
if (position.id > this._maxId) {
|
||||||
|
this._maxId = position.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._positions = positions;
|
||||||
|
this._plotData = plotData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {?Array<{x: number, y: number}>}
|
||||||
|
*/
|
||||||
|
get plotData() {
|
||||||
|
return this._plotData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get length() {
|
||||||
|
return this._positions ? this._positions.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get hasPositions() {
|
||||||
|
return this._positions !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
fetch() {
|
||||||
|
const data = {
|
||||||
|
userid: this._user.id
|
||||||
|
};
|
||||||
|
let isUpdate = this.hasPositions;
|
||||||
|
if (config.showLatest) {
|
||||||
|
data.last = 1;
|
||||||
|
isUpdate = false;
|
||||||
|
} else {
|
||||||
|
data.trackid = this.id;
|
||||||
|
}
|
||||||
|
if (this._onlyLatest !== config.showLatest) {
|
||||||
|
this._onlyLatest = config.showLatest;
|
||||||
|
isUpdate = false;
|
||||||
|
} else {
|
||||||
|
data.afterid = this._maxId;
|
||||||
|
}
|
||||||
|
return uAjax.get('utils/getpositions.php', data, {
|
||||||
|
// loader: ui.trackTitle
|
||||||
|
}).then((xml) => {
|
||||||
|
this.fromXml(xml, isUpdate);
|
||||||
|
return this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} action
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
update(action) {
|
||||||
|
return uAjax.post('utils/handletrack.php',
|
||||||
|
{
|
||||||
|
action: action,
|
||||||
|
trackid: this.id,
|
||||||
|
trackname: this.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.emit(uEvent.TRACK_READY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export to file
|
||||||
|
* @param {string} type File type
|
||||||
|
*/
|
||||||
|
export(type) {
|
||||||
|
const url = `utils/export.php?type=${type}&userid=${this._user.id}&trackid=${this.id}`;
|
||||||
|
this.emit(uEvent.OPEN_URL, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
108
js/trackdialog.js
Normal file
108
js/trackdialog.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/* μ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 } from './constants.js';
|
||||||
|
import uModal from './modal.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
export default class TrackDialog {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uTrack} track
|
||||||
|
*/
|
||||||
|
constructor(track) {
|
||||||
|
this.track = track;
|
||||||
|
const html = `<div style="float:left">${uUtils.sprintf(lang.strings['editingtrack'], `<b>${uUtils.htmlEncode(this.track.name)}</b>`)}</div>
|
||||||
|
<div class="red-button button-resolve" data-action="delete" data-confirm="${uUtils.sprintf(lang.strings['trackdelwarn'], uUtils.htmlEncode(this.track.name))}"><b><a>${lang.strings['deltrack']}</a></b></div>
|
||||||
|
<div style="clear: both; padding-bottom: 1em;"></div>
|
||||||
|
<form id="trackForm">
|
||||||
|
<label><b>${lang.strings['trackname']}</b></label>
|
||||||
|
<input type="text" placeholder="${lang.strings['trackname']}" name="trackname" value="${uUtils.htmlEncode(this.track.name)}" required>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button-reject" type="button">${lang.strings['cancel']}</button>
|
||||||
|
<button class="button-resolve" type="submit" data-action="update">${lang.strings['submit']}</button>
|
||||||
|
</div>
|
||||||
|
</form>`;
|
||||||
|
this.dialog = new uModal(html);
|
||||||
|
this.form = this.dialog.modal.querySelector('#trackForm');
|
||||||
|
this.form.onsubmit = () => false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show edit track dialog
|
||||||
|
* @see {uModal}
|
||||||
|
* @returns {Promise<ModalResult>}
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.resolveModal(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ModalCallback} resolve
|
||||||
|
*/
|
||||||
|
resolveModal(resolve) {
|
||||||
|
this.dialog.show().then((result) => {
|
||||||
|
if (result.cancelled) {
|
||||||
|
return this.hide();
|
||||||
|
}
|
||||||
|
if (result.action === 'update') {
|
||||||
|
if (!this.validate()) {
|
||||||
|
return this.resolveModal(resolve);
|
||||||
|
}
|
||||||
|
result.data = this.getData();
|
||||||
|
}
|
||||||
|
return resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide dialog
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.dialog.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data from track form
|
||||||
|
* @return {{name: string}}
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
const trackName = this.form.elements['trackname'].value.trim();
|
||||||
|
return { name: trackName };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate form
|
||||||
|
* @return {boolean} True if valid
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
const trackName = this.form.elements['trackname'].value.trim();
|
||||||
|
if (trackName === this.track.name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!trackName) {
|
||||||
|
alert(lang.strings['allrequired']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
188
js/tracklist.js
Normal file
188
js/tracklist.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { auth, config, lang } from './constants.js';
|
||||||
|
import TrackDialog from './trackdialog.js';
|
||||||
|
import uAjax from './ajax.js';
|
||||||
|
import uEvent from './event.js';
|
||||||
|
import uList from './list.js';
|
||||||
|
import { uLogger } from './ulogger.js';
|
||||||
|
import uPosition from './position.js';
|
||||||
|
import uTrack from './track.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class TrackList
|
||||||
|
* @extends {uList<uTrack>}
|
||||||
|
*/
|
||||||
|
export default class TrackList extends uList {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} selector
|
||||||
|
* @param {uBinder} binder
|
||||||
|
*/
|
||||||
|
constructor(selector, binder) {
|
||||||
|
super(selector, binder, uTrack);
|
||||||
|
if (binder) {
|
||||||
|
this.binder.addEventListener(uEvent.CONFIG, this);
|
||||||
|
this.binder.addEventListener(uEvent.EXPORT, this);
|
||||||
|
this.binder.addEventListener(uEvent.IMPORT, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {uTrack} row
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
updateDataRow(row) {
|
||||||
|
row.user = uLogger.userList.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {(Event|uEvent)} event
|
||||||
|
* @param {*=} data
|
||||||
|
*/
|
||||||
|
handleEvent(event, data) {
|
||||||
|
if (event.type === 'change') {
|
||||||
|
config.showLatest = false;
|
||||||
|
}
|
||||||
|
super.handleEvent(event, data);
|
||||||
|
if (event.type === uEvent.EXPORT) {
|
||||||
|
this.current.export(data);
|
||||||
|
} else if (event.type === uEvent.IMPORT) {
|
||||||
|
this.import(data).catch((msg) => alert(`${lang.strings['actionfailure']}\n${msg}`));
|
||||||
|
} else if (event.type === uEvent.CONFIG && data === 'showLatest') {
|
||||||
|
if (config.showLatest) {
|
||||||
|
this.fetchLatest().catch((msg) => alert(`${lang.strings['actionfailure']}\n${msg}`));
|
||||||
|
} else {
|
||||||
|
this.fetchTrack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLFormElement} form
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
import(form) {
|
||||||
|
return uAjax.post('utils/import.php', form,
|
||||||
|
{
|
||||||
|
// loader: ui.importTitle
|
||||||
|
})
|
||||||
|
.then((xml) => {
|
||||||
|
const root = xml.getElementsByTagName('root');
|
||||||
|
const trackCnt = uUtils.getNodeAsInt(root[0], 'trackcnt');
|
||||||
|
if (trackCnt > 1) {
|
||||||
|
alert(uUtils.sprintf(lang.strings['imultiple'], trackCnt));
|
||||||
|
}
|
||||||
|
const trackId = uUtils.getNodeAsInt(root[0], 'trackid');
|
||||||
|
return this.fetch().then(() => this.select(trackId));
|
||||||
|
}).catch((msg) => alert(`${lang.strings['actionfailure']}\n${msg}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch tracks for current user
|
||||||
|
* @return {Promise<Document, string>}
|
||||||
|
*/
|
||||||
|
fetch() {
|
||||||
|
return uAjax.get('utils/gettracks.php',
|
||||||
|
{
|
||||||
|
userid: uLogger.userList.current.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// loader: ui.trackTitle
|
||||||
|
}).then((xml) => {
|
||||||
|
this.clear();
|
||||||
|
return this.fromXml(xml.getElementsByTagName('track'), 'trackid', 'trackname');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch track with latest position for current user
|
||||||
|
* @throws
|
||||||
|
* @return {Promise<Document, string>}
|
||||||
|
*/
|
||||||
|
fetchLatest() {
|
||||||
|
return uAjax.get('utils/getpositions.php', {
|
||||||
|
userid: uLogger.userList.current.id,
|
||||||
|
last: 1
|
||||||
|
}, {
|
||||||
|
// loader: ui.trackTitle
|
||||||
|
}).then((xml) => {
|
||||||
|
const xmlPos = xml.getElementsByTagName('position');
|
||||||
|
if (xmlPos.length === 1) {
|
||||||
|
const position = uPosition.fromXml(xmlPos[0]);
|
||||||
|
if (this.has(position.trackid)) {
|
||||||
|
this.select(position.trackid, true);
|
||||||
|
this.current.fromXml(xml, false);
|
||||||
|
this.current.onlyLatest = true;
|
||||||
|
return this.current.render();
|
||||||
|
}
|
||||||
|
// tracklist needs update
|
||||||
|
return this.fetch().fetchLatest();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
onChange() {
|
||||||
|
this.fetchTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchTrack() {
|
||||||
|
if (this.current) {
|
||||||
|
this.current.fetch()
|
||||||
|
.catch((msg) => alert(`${lang.strings['actionfailure']}\n${msg}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
onEdit() {
|
||||||
|
if (this.current) {
|
||||||
|
if (this.current.user.login !== auth.user.login && !auth.isAdmin) {
|
||||||
|
alert(lang.strings['owntrackswarn']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.editTrack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {TrackDialog=} modal
|
||||||
|
*/
|
||||||
|
editTrack(modal) {
|
||||||
|
const dialog = modal || new TrackDialog(this.current);
|
||||||
|
dialog.show()
|
||||||
|
.then((result) => {
|
||||||
|
switch (result.action) {
|
||||||
|
case 'update':
|
||||||
|
this.current.name = result.data.name;
|
||||||
|
return this.current.update('update').then(() => this.render());
|
||||||
|
case 'delete':
|
||||||
|
return this.current.update('delete').then(() => this.remove(this.current.id));
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw new Error();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
alert(lang.strings['actionsuccess']);
|
||||||
|
dialog.hide();
|
||||||
|
})
|
||||||
|
.catch((msg) => {
|
||||||
|
alert(`${lang.strings['actionfailure']}\n${msg}`);
|
||||||
|
this.editTrack(dialog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-empty-function,class-methods-use-this
|
||||||
|
onAdd() {
|
||||||
|
}
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
/* μ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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef uLogger.config
|
|
||||||
* @memberOf uLogger
|
|
||||||
* @type {Object}
|
|
||||||
* @property {number} interval
|
|
||||||
* @property {string} units
|
|
||||||
* @property {string} mapapi
|
|
||||||
* @property {?string} gkey
|
|
||||||
* @property {Object.<string, string>} ol_layers
|
|
||||||
* @property {number} init_latitude
|
|
||||||
* @property {number} init_longitude
|
|
||||||
* @property {boolean} admin
|
|
||||||
* @property {?string} auth
|
|
||||||
* @property {RegExp} pass_regex
|
|
||||||
* @property {number} strokeWeight
|
|
||||||
* @property {string} strokeColor
|
|
||||||
* @property {number} strokeOpacity
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef uLogger.lang
|
|
||||||
* @memberOf uLogger
|
|
||||||
* @type {Object}
|
|
||||||
* @property {Object.<string, string>} strings
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} uLogger.mapAPI.api
|
|
||||||
* @memberOf uLogger
|
|
||||||
* @type {Object}
|
|
||||||
* @property {string} name
|
|
||||||
* @property {function} init
|
|
||||||
* @property {function} cleanup
|
|
||||||
* @property {function(HTMLCollection, boolean)} displayTrack
|
|
||||||
* @property {function} clearMap
|
|
||||||
* @property {function(uLogger.Position, number, number)} setMarker
|
|
||||||
* @property {function} addChartEvent
|
|
||||||
* @property {function} getBounds
|
|
||||||
* @property {function} zoomToExtent
|
|
||||||
* @property {function} zoomToBounds
|
|
||||||
* @property {function} updateSize
|
|
||||||
*/
|
|
449
js/ui.js
Normal file
449
js/ui.js
Normal file
@ -0,0 +1,449 @@
|
|||||||
|
import { config, lang } from './constants.js';
|
||||||
|
import uEvent from './event.js';
|
||||||
|
import { uLogger } from './ulogger.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
export default class uUI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {uBinder} binder
|
||||||
|
*/
|
||||||
|
constructor(binder) {
|
||||||
|
this._binder = binder;
|
||||||
|
binder.addEventListener(uEvent.CONFIG, this);
|
||||||
|
binder.addEventListener(uEvent.CHART_READY, this);
|
||||||
|
binder.addEventListener(uEvent.OPEN_URL, this);
|
||||||
|
document.addEventListener('DOMContentLoaded', () => { this.initUI(); });
|
||||||
|
this.isLiveOn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize uUI elements
|
||||||
|
*/
|
||||||
|
initUI() {
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.menu = document.getElementById('menu');
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
this.menuHead = document.getElementById('menu_head');
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
this.userDropdown = document.getElementById('user_dropdown');
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
this.menuPass = document.getElementById('menu_pass');
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
/** @type {?HTMLSelectElement} */
|
||||||
|
this.userSelect = function () {
|
||||||
|
const list = document.getElementsByName('user');
|
||||||
|
if (list.length) { return list[0]; }
|
||||||
|
return null;
|
||||||
|
}();
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
/** @type {HTMLSelectElement} */
|
||||||
|
this.trackSelect = document.getElementsByName('track')[0];
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
/** @type {HTMLSelectElement} */
|
||||||
|
this.api = document.getElementsByName('api')[0];
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
/** @type {HTMLSelectElement} */
|
||||||
|
this.lang = document.getElementsByName('lang')[0];
|
||||||
|
// noinspection JSValidateTypes
|
||||||
|
/** @type {HTMLSelectElement} */
|
||||||
|
this.units = document.getElementsByName('units')[0];
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.chart = document.getElementById('chart');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.chartClose = document.getElementById('chart_close');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.bottom = document.getElementById('bottom');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.chartLink = document.getElementById('altitudes');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.main = document.getElementById('main');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.menuClose = document.getElementById('menu-close');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.track = document.getElementById('track');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.trackTitle = this.track ? this.track.getElementsByClassName('menutitle')[0] : null;
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.import = document.getElementById('import');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.importTitle = this.import ? this.import.getElementsByClassName('menutitle')[0] : null;
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.summary = document.getElementById('summary');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.latest = document.getElementById('latest');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.autoReload = document.getElementById('auto_reload');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.forceReload = document.getElementById('force_reload');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.auto = document.getElementById('auto');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.setTime = document.getElementById('set_time');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.exportKml = document.getElementById('export_kml');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.exportGpx = document.getElementById('export_gpx');
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
this.inputFile = document.getElementById('inputFile');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.importGpx = document.getElementById('import_gpx');
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
this.addUser = document.getElementById('adduser');
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
this.editUser = document.getElementById('edituser');
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
this.editTrack = document.getElementById('edittrack');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.map = document.getElementById('map-canvas');
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.head = document.getElementsByTagName('head')[0];
|
||||||
|
|
||||||
|
if (this.menuHead) {
|
||||||
|
this.menuHead.onclick = () => this.showUserMenu();
|
||||||
|
}
|
||||||
|
if (this.menuPass) {
|
||||||
|
this.menuPass.onclick = () => {
|
||||||
|
this.emit(uEvent.PASSWORD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.hideUserMenu = this.hideUserMenu.bind(this);
|
||||||
|
this.latest.onchange = () => uUI.toggleLatest();
|
||||||
|
this.autoReload.onchange = () => this.toggleAutoReload();
|
||||||
|
this.setTime.onclick = () => this.setAutoReloadTime();
|
||||||
|
this.forceReload.onclick = () => this.trackReload();
|
||||||
|
this.chartLink.onclick = () => this.toggleChart();
|
||||||
|
this.api.onchange = () => {
|
||||||
|
const api = this.api.options[this.api.selectedIndex].value;
|
||||||
|
this.emit(uEvent.API_CHANGE, api);
|
||||||
|
};
|
||||||
|
this.lang.onchange = () => {
|
||||||
|
uUI.setLang(this.lang.options[this.lang.selectedIndex].value);
|
||||||
|
};
|
||||||
|
this.units.onchange = () => {
|
||||||
|
uUI.setUnits(this.units.options[this.units.selectedIndex].value);
|
||||||
|
};
|
||||||
|
this.exportKml.onclick = () => {
|
||||||
|
this.emit(uEvent.EXPORT, 'kml');
|
||||||
|
};
|
||||||
|
this.exportGpx.onclick = () => {
|
||||||
|
this.emit(uEvent.EXPORT, 'gpx');
|
||||||
|
};
|
||||||
|
if (this.inputFile) {
|
||||||
|
this.inputFile.onchange = () => {
|
||||||
|
const form = this.inputFile.parentElement;
|
||||||
|
const sizeMax = form.elements['MAX_FILE_SIZE'].value;
|
||||||
|
if (this.inputFile.files && this.inputFile.files.length === 1 && this.inputFile.files[0].size > sizeMax) {
|
||||||
|
alert(uUtils.sprintf(lang.strings['isizefailure'], sizeMax));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.emit(uEvent.IMPORT, form);
|
||||||
|
};
|
||||||
|
this.importGpx.onclick = () => {
|
||||||
|
this.inputFile.click();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (this.addUser) {
|
||||||
|
this.addUser.onclick = () => {
|
||||||
|
this.emit(uEvent.ADD, this.userSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.editUser) {
|
||||||
|
this.editUser.onclick = () => {
|
||||||
|
this.emit(uEvent.EDIT, this.userSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.editTrack) {
|
||||||
|
this.editTrack.onclick = () => {
|
||||||
|
this.emit(uEvent.EDIT, this.trackSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.menuClose.onclick = () => this.toggleSideMenu();
|
||||||
|
this.chartClose.onclick = () => this.hideChart();
|
||||||
|
this.emit(uEvent.UI_READY);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackReload() {
|
||||||
|
uUI.emitDom(this.trackSelect, 'change');
|
||||||
|
}
|
||||||
|
|
||||||
|
userReload() {
|
||||||
|
uUI.emitDom(this.userSelect, 'change');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle auto-reload
|
||||||
|
*/
|
||||||
|
toggleAutoReload() {
|
||||||
|
if (this.isLiveOn) {
|
||||||
|
this.stopAutoReload();
|
||||||
|
} else {
|
||||||
|
this.startAutoReload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startAutoReload() {
|
||||||
|
this.isLiveOn = true;
|
||||||
|
this.liveInterval = setInterval(() => {
|
||||||
|
this.trackReload();
|
||||||
|
}, config.interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAutoReload() {
|
||||||
|
this.isLiveOn = false;
|
||||||
|
clearInterval(this.liveInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set new interval from user dialog
|
||||||
|
*/
|
||||||
|
setAutoReloadTime() {
|
||||||
|
const i = parseInt(prompt(lang.strings['newinterval']));
|
||||||
|
if (!isNaN(i) && i !== config.interval) {
|
||||||
|
config.interval = i;
|
||||||
|
this.auto.innerHTML = config.interval.toString();
|
||||||
|
// if live tracking on, reload with new interval
|
||||||
|
if (this.isLiveOn) {
|
||||||
|
this.stopAutoReload();
|
||||||
|
this.startAutoReload();
|
||||||
|
}
|
||||||
|
// save current state as default
|
||||||
|
uUtils.setCookie('interval', config.interval, 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle side menu
|
||||||
|
*/
|
||||||
|
toggleSideMenu() {
|
||||||
|
if (this.menuClose.innerHTML === '»') {
|
||||||
|
this.menu.style.width = '0';
|
||||||
|
this.main.style.marginRight = '0';
|
||||||
|
this.menuClose.style.right = '0';
|
||||||
|
this.menuClose.innerHTML = '«';
|
||||||
|
} else {
|
||||||
|
this.menu.style.width = '165px';
|
||||||
|
this.main.style.marginRight = '165px';
|
||||||
|
this.menuClose.style.right = '165px';
|
||||||
|
this.menuClose.innerHTML = '»';
|
||||||
|
}
|
||||||
|
uUI.emitDom(window, 'resize');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch event at specified target
|
||||||
|
* @param {(Element|Document|Window)} el Target element
|
||||||
|
* @param {string} event Event name
|
||||||
|
*/
|
||||||
|
static emitDom(el, event) {
|
||||||
|
el.dispatchEvent(new Event(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch event
|
||||||
|
* @param {string} type
|
||||||
|
* @param {*=} args Defaults to this
|
||||||
|
*/
|
||||||
|
emit(type, args) {
|
||||||
|
const data = args || this;
|
||||||
|
this._binder.dispatchEvent(type, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is chart visible
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isChartVisible() {
|
||||||
|
return this.bottom.style.display === 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show chart
|
||||||
|
*/
|
||||||
|
showChart() {
|
||||||
|
this.bottom.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide chart
|
||||||
|
*/
|
||||||
|
hideChart() {
|
||||||
|
this.bottom.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle chart visibility
|
||||||
|
*/
|
||||||
|
toggleChart() {
|
||||||
|
if (this.isChartVisible()) {
|
||||||
|
this.hideChart();
|
||||||
|
} else {
|
||||||
|
this.showChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animate element text
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
static setLoader(el) {
|
||||||
|
const str = el.textContent;
|
||||||
|
el.innerHTML = '';
|
||||||
|
for (const c of str) {
|
||||||
|
el.innerHTML += `<span class="loader">${c}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop animation
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
static removeLoader(el) {
|
||||||
|
el.innerHTML = el.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get popup html
|
||||||
|
* @param {number} id Position ID
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static getPopupHtml(id) {
|
||||||
|
const pos = uLogger.trackList.current.positions[id];
|
||||||
|
const count = uLogger.trackList.current.positions.length;
|
||||||
|
let date = '–––';
|
||||||
|
let time = '–––';
|
||||||
|
if (pos.timestamp > 0) {
|
||||||
|
const d = new Date(pos.timestamp * 1000);
|
||||||
|
date = `${d.getFullYear()}-${(`0${d.getMonth() + 1}`).slice(-2)}-${(`0${d.getDate()}`).slice(-2)}`;
|
||||||
|
time = d.toTimeString();
|
||||||
|
let offset;
|
||||||
|
if ((offset = time.indexOf(' ')) >= 0) {
|
||||||
|
time = `${time.substr(0, offset)} <span class="smaller">${time.substr(offset + 1)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let provider = '';
|
||||||
|
if (pos.provider === 'gps') {
|
||||||
|
provider = ` (<img class="icon" alt="${lang.strings['gps']}" title="${lang.strings['gps']}" src="images/gps_dark.svg">)`;
|
||||||
|
} else if (pos.provider === 'network') {
|
||||||
|
provider = ` (<img class="icon" alt="${lang.strings['network']}" title="${lang.strings['network']}" src="images/network_dark.svg">)`;
|
||||||
|
}
|
||||||
|
let stats = '';
|
||||||
|
if (!config.showLatest) {
|
||||||
|
stats =
|
||||||
|
`<div id="pright">
|
||||||
|
<img class="icon" alt="${lang.strings['track']}" src="images/stats_blue.svg" style="padding-left: 3em;"><br>
|
||||||
|
<img class="icon" alt="${lang.strings['ttime']}" title="${lang.strings['ttime']}" src="images/time_blue.svg"> ${pos.totalSeconds.toHMS()}<br>
|
||||||
|
<img class="icon" alt="${lang.strings['aspeed']}" title="${lang.strings['aspeed']}" src="images/speed_blue.svg"> ${(pos.totalSeconds > 0) ? ((pos.totalDistance / pos.totalSeconds).toKmH() * config.factor_kmh).toFixed() : 0} ${config.unit_kmh}<br>
|
||||||
|
<img class="icon" alt="${lang.strings['tdistance']}" title="${lang.strings['tdistance']}" src="images/distance_blue.svg"> ${(pos.totalDistance.toKm() * config.factor_km).toFixed(2)} ${config.unit_km}<br>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
return `<div id="popup">
|
||||||
|
<div id="pheader">
|
||||||
|
<div><img alt="${lang.strings['user']}" title="${lang.strings['user']}" src="images/user_dark.svg"> ${uUtils.htmlEncode(pos.username)}</div>
|
||||||
|
<div><img alt="${lang.strings['track']}" title="${lang.strings['track']}" src="images/route_dark.svg"> ${uUtils.htmlEncode(pos.trackname)}</div>
|
||||||
|
</div>
|
||||||
|
<div id="pbody">
|
||||||
|
${(pos.comment != null) ? `<div id="pcomments">${uUtils.htmlEncode(pos.comment)}</div>` : ''}
|
||||||
|
<div id="pleft">
|
||||||
|
<img class="icon" alt="${lang.strings['time']}" title="${lang.strings['time']}" src="images/calendar_dark.svg"> ${date}<br>
|
||||||
|
<img class="icon" alt="${lang.strings['time']}" title="${lang.strings['time']}" src="images/clock_dark.svg"> ${time}<br>
|
||||||
|
${(pos.speed != null) ? `<img class="icon" alt="${lang.strings['speed']}" title="${lang.strings['speed']}" src="images/speed_dark.svg">${pos.speed.toKmH() * config.factor_kmh} ${config.unit_kmh}<br>` : ''}
|
||||||
|
${(pos.altitude != null) ? `<img class="icon" alt="${lang.strings['altitude']}" title="${lang.strings['altitude']}" src="images/altitude_dark.svg">${(pos.altitude * config.factor_m).toFixed()} ${config.unit_m}<br>` : ''}
|
||||||
|
${(pos.accuracy != null) ? `<img class="icon" alt="${lang.strings['accuracy']}" title="${lang.strings['accuracy']}" src="images/accuracy_dark.svg">${(pos.accuracy * config.factor_m).toFixed()} ${config.unit_m}${provider}<br>` : ''}
|
||||||
|
</div>${stats}</div>
|
||||||
|
<div id="pfooter">${uUtils.sprintf(lang.strings['pointof'], id + 1, count)}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear map canvas
|
||||||
|
*/
|
||||||
|
clearMapCanvas() {
|
||||||
|
this.map.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle user menu visibility
|
||||||
|
*/
|
||||||
|
showUserMenu() {
|
||||||
|
if (this.userDropdown.classList.contains('show')) {
|
||||||
|
this.userDropdown.classList.remove('show');
|
||||||
|
} else {
|
||||||
|
this.userDropdown.classList.add('show');
|
||||||
|
window.addEventListener('click', this.hideUserMenu, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click listener callback to hide user menu
|
||||||
|
* @param {MouseEvent} e
|
||||||
|
*/
|
||||||
|
hideUserMenu(e) {
|
||||||
|
const parent = e.target.parentElement;
|
||||||
|
this.userDropdown.classList.remove('show');
|
||||||
|
window.removeEventListener('click', this.hideUserMenu, true);
|
||||||
|
if (!parent.classList.contains('dropdown')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {(Event|uEvent)} event
|
||||||
|
* @param {*=} args
|
||||||
|
*/
|
||||||
|
handleEvent(event, args) {
|
||||||
|
if (event.type === uEvent.CHART_READY) {
|
||||||
|
// toggle chart link
|
||||||
|
const hasPoints = args > 0;
|
||||||
|
if (hasPoints) {
|
||||||
|
this.chartLink.style.visibility = 'visible';
|
||||||
|
} else {
|
||||||
|
this.chartLink.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
} else if (event.type === uEvent.OPEN_URL) {
|
||||||
|
window.location.assign(args);
|
||||||
|
} else if (event.type === uEvent.CONFIG) {
|
||||||
|
if (args === 'showLatest') {
|
||||||
|
this.latest.checked = config.showLatest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set language
|
||||||
|
* @param {string} languageCode Language code
|
||||||
|
*/
|
||||||
|
static setLang(languageCode) {
|
||||||
|
uUtils.setCookie('lang', languageCode, 30);
|
||||||
|
uUI.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set units
|
||||||
|
* @param {string} unitCode New units
|
||||||
|
*/
|
||||||
|
static setUnits(unitCode) {
|
||||||
|
uUtils.setCookie('units', unitCode, 30);
|
||||||
|
uUI.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
static reload() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
static toggleLatest() {
|
||||||
|
config.showLatest = !config.showLatest;
|
||||||
|
}
|
||||||
|
}
|
29
js/ulogger.js
Normal file
29
js/ulogger.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { auth, config } from './constants.js';
|
||||||
|
import TrackList from './tracklist.js';
|
||||||
|
import UserList from './userlist.js';
|
||||||
|
import uBinder from './binder.js';
|
||||||
|
import uChart from './chart.js';
|
||||||
|
import uEvent from './event.js';
|
||||||
|
import uMap from './map.js';
|
||||||
|
import uUI from './ui.js';
|
||||||
|
|
||||||
|
|
||||||
|
export const uLogger = {
|
||||||
|
/** @type {?UserList} */
|
||||||
|
userList: null,
|
||||||
|
/** @type {?TrackList} */
|
||||||
|
trackList: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const binder = new uBinder();
|
||||||
|
binder.addEventListener(uEvent.PASSWORD, auth);
|
||||||
|
config.binder = binder;
|
||||||
|
|
||||||
|
new uMap(binder);
|
||||||
|
new uChart(binder);
|
||||||
|
new uUI(binder);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
uLogger.userList = new UserList('#user', binder);
|
||||||
|
uLogger.trackList = new TrackList('#track', binder);
|
||||||
|
});
|
56
js/user.js
Normal file
56
js/user.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import uAjax from './ajax.js';
|
||||||
|
import uData from './data.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class uUser
|
||||||
|
* @extends {uData}
|
||||||
|
* @property {number} id
|
||||||
|
* @property {string} login
|
||||||
|
* @property {string} [password]
|
||||||
|
*/
|
||||||
|
export default class uUser extends uData {
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @param {string} login
|
||||||
|
*/
|
||||||
|
constructor(id, login) {
|
||||||
|
super(id, login, 'id', 'login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} action
|
||||||
|
* @return {Promise<uUser>}
|
||||||
|
*/
|
||||||
|
update(action) {
|
||||||
|
const pass = this.password;
|
||||||
|
// don't store password in class property
|
||||||
|
delete this.password;
|
||||||
|
return uAjax.post('utils/handleuser.php',
|
||||||
|
{
|
||||||
|
action: action,
|
||||||
|
login: this.login,
|
||||||
|
pass: pass
|
||||||
|
}).then((xml) => {
|
||||||
|
if (action === 'add') {
|
||||||
|
this.id = uUtils.getNodeAsInt(xml, 'userid');
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} password
|
||||||
|
* @param {string} oldPassword
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
changePass(password, oldPassword) {
|
||||||
|
return uAjax.post('utils/changepass.php',
|
||||||
|
{
|
||||||
|
login: this.login,
|
||||||
|
pass: password,
|
||||||
|
oldpass: oldPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
178
js/userdialog.js
Normal file
178
js/userdialog.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/* μ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 { config, lang } from './constants.js';
|
||||||
|
import uModal from './modal.js';
|
||||||
|
import uUtils from './utils.js';
|
||||||
|
|
||||||
|
export default class UserDialog {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type: edit, add, pass
|
||||||
|
* @param {uUser=} user Update existing user if supplied
|
||||||
|
*/
|
||||||
|
constructor(type, user) {
|
||||||
|
this.type = type;
|
||||||
|
this.user = user;
|
||||||
|
this.dialog = new uModal(this.getHtml());
|
||||||
|
this.form = this.dialog.modal.querySelector('#userForm');
|
||||||
|
this.form.onsubmit = () => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getHtml() {
|
||||||
|
let deleteButton = '';
|
||||||
|
let header = '';
|
||||||
|
let action;
|
||||||
|
let fields;
|
||||||
|
switch (this.type) {
|
||||||
|
case 'add':
|
||||||
|
action = 'add';
|
||||||
|
header = `<label><b>${lang.strings['username']}</b></label>
|
||||||
|
<input type="text" placeholder="${lang.strings['usernameenter']}" name="login" required>`;
|
||||||
|
fields = `<label><b>${lang.strings['password']}</b></label>
|
||||||
|
<input type="password" placeholder="${lang.strings['passwordenter']}" name="pass" required>
|
||||||
|
<label><b>${lang.strings['passwordrepeat']}</b></label>
|
||||||
|
<input type="password" placeholder="${lang.strings['passwordenter']}" name="pass2" required>`;
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
action = 'update';
|
||||||
|
deleteButton = `<div style="float:left">${uUtils.sprintf(lang.strings['editinguser'], `<b>${uUtils.htmlEncode(this.user.login)}</b>`)}</div>
|
||||||
|
<div class="red-button button-resolve" data-action="delete" data-confirm="${uUtils.sprintf(lang.strings['userdelwarn'], uUtils.htmlEncode(this.user.login))}"><b><a>${lang.strings['deluser']}</a></b></div>
|
||||||
|
<div style="clear: both; padding-bottom: 1em;"></div>`;
|
||||||
|
fields = `<label><b>${lang.strings['password']}</b></label>
|
||||||
|
<input type="password" placeholder="${lang.strings['passwordenter']}" name="pass" required>
|
||||||
|
<label><b>${lang.strings['passwordrepeat']}</b></label>
|
||||||
|
<input type="password" placeholder="${lang.strings['passwordenter']}" name="pass2" required>`;
|
||||||
|
break;
|
||||||
|
case 'pass':
|
||||||
|
action = 'update';
|
||||||
|
fields = `<label><b>${lang.strings['oldpassword']}</b></label>
|
||||||
|
<input type="password" placeholder="${lang.strings['passwordenter']}" name="oldpass" required>
|
||||||
|
<label><b>${lang.strings['newpassword']}</b></label>
|
||||||
|
<input type="password" placeholder="${lang.strings['passwordenter']}" name="pass" required>
|
||||||
|
<label><b>${lang.strings['newpasswordrepeat']}</b></label>
|
||||||
|
<input type="password" placeholder="${lang.strings['passwordenter']}" name="pass2" 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">${lang.strings['cancel']}</button>
|
||||||
|
<button class="button-resolve" type="submit" data-action="${action}">${lang.strings['submit']}</button>
|
||||||
|
</div>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show edit user dialog
|
||||||
|
* @see {uModal}
|
||||||
|
* @returns {Promise<ModalResult>}
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.resolveModal(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ModalCallback} resolve
|
||||||
|
*/
|
||||||
|
resolveModal(resolve) {
|
||||||
|
this.dialog.show().then((result) => {
|
||||||
|
if (result.cancelled) {
|
||||||
|
return this.hide();
|
||||||
|
}
|
||||||
|
if (result.action === 'update' || result.action === 'add') {
|
||||||
|
if (!this.validate()) {
|
||||||
|
return this.resolveModal(resolve);
|
||||||
|
}
|
||||||
|
result.data = this.getData();
|
||||||
|
}
|
||||||
|
return resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide dialog
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.dialog.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data from track form
|
||||||
|
* @return {boolean|{login: string, password: string, oldPassword: ?string}}
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
let login;
|
||||||
|
if (this.type === 'add') {
|
||||||
|
login = this.form.elements['login'].value.trim();
|
||||||
|
} else {
|
||||||
|
login = this.user.login;
|
||||||
|
}
|
||||||
|
let oldPass = null;
|
||||||
|
if (this.type === 'pass') {
|
||||||
|
oldPass = this.form.elements['oldpass'].value.trim();
|
||||||
|
}
|
||||||
|
const pass = this.form.elements['pass'].value.trim();
|
||||||
|
return { login: login, password: pass, oldPassword: oldPass };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate form
|
||||||
|
* @return {boolean} True if valid
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
if (this.type === 'add') {
|
||||||
|
const login = this.form.elements['login'].value.trim();
|
||||||
|
if (!login) {
|
||||||
|
alert(lang.strings['allrequired']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (this.type === 'pass') {
|
||||||
|
const oldPass = this.form.elements['oldpass'].value.trim();
|
||||||
|
if (!oldPass) {
|
||||||
|
alert(lang.strings['allrequired']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pass = this.form.elements['pass'].value.trim();
|
||||||
|
const pass2 = this.form.elements['pass2'].value.trim();
|
||||||
|
if (!pass || !pass2) {
|
||||||
|
alert(lang.strings['allrequired']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pass !== pass2) {
|
||||||
|
alert(lang.strings['passnotmatch']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!config.pass_regex.test(pass)) {
|
||||||
|
alert(lang.strings['passlenmin'] + '\n' + lang.strings['passrules']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
114
js/userlist.js
Normal file
114
js/userlist.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { auth, config, lang } from './constants.js';
|
||||||
|
import UserDialog from './userdialog.js';
|
||||||
|
import uList from './list.js';
|
||||||
|
import { uLogger } from './ulogger.js';
|
||||||
|
import uUser from './user.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class UserList
|
||||||
|
* @extends {uList<uUser>}
|
||||||
|
*/
|
||||||
|
export default class UserList extends uList {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} selector
|
||||||
|
* @param {uBinder} binder
|
||||||
|
*/
|
||||||
|
constructor(selector, binder) {
|
||||||
|
super(selector, binder, uUser);
|
||||||
|
super.hasHead = true;
|
||||||
|
super.allValue = `- ${lang.strings['allusers']} -`;
|
||||||
|
super.headValue = lang.strings['suser'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
onChange() {
|
||||||
|
if (this.isSelectedAllOption) {
|
||||||
|
// clearOptions(ui.trackSelect);
|
||||||
|
// loadLastPositionAllUsers();
|
||||||
|
} else if (config.showLatest) {
|
||||||
|
uLogger.trackList.fetchLatest()
|
||||||
|
.catch((msg) => alert(`${lang.strings['actionfailure']}\n${msg}`));
|
||||||
|
} else {
|
||||||
|
uLogger.trackList.fetch()
|
||||||
|
.catch((msg) => alert(`${lang.strings['actionfailure']}\n${msg}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
onEdit() {
|
||||||
|
if (this.isSelectedAllOption) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.current) {
|
||||||
|
if (this.current.login === auth.user.login) {
|
||||||
|
alert(lang.strings['selfeditwarn']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.editUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UserDialog=} modal
|
||||||
|
*/
|
||||||
|
editUser(modal) {
|
||||||
|
const dialog = modal || new UserDialog('edit', this.current);
|
||||||
|
dialog.show()
|
||||||
|
.then((result) => {
|
||||||
|
switch (result.action) {
|
||||||
|
case 'update':
|
||||||
|
// currently only password
|
||||||
|
this.current.password = result.data.password;
|
||||||
|
return this.current.update('update');
|
||||||
|
case 'delete':
|
||||||
|
return this.current.update('delete').then(() => this.remove(this.current.id));
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw new Error();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
alert(lang.strings['actionsuccess']);
|
||||||
|
dialog.hide();
|
||||||
|
})
|
||||||
|
.catch((msg) => {
|
||||||
|
alert(`${lang.strings['actionfailure']}\n${msg}`);
|
||||||
|
this.editUser(dialog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
onAdd() {
|
||||||
|
this.addUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UserDialog=} modal
|
||||||
|
*/
|
||||||
|
addUser(modal) {
|
||||||
|
const dialog = modal || new UserDialog('add');
|
||||||
|
dialog.show()
|
||||||
|
.then((result) => {
|
||||||
|
const newUser = new uUser(0, result.data.login);
|
||||||
|
newUser.password = result.data.password;
|
||||||
|
return newUser.update('add')
|
||||||
|
})
|
||||||
|
.then((user) => {
|
||||||
|
alert(lang.strings['actionsuccess']);
|
||||||
|
this.add(user);
|
||||||
|
dialog.hide();
|
||||||
|
})
|
||||||
|
.catch((msg) => {
|
||||||
|
alert(`${lang.strings['actionfailure']}\n${msg}`);
|
||||||
|
this.addUser(dialog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
244
js/utils.js
Normal file
244
js/utils.js
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
|
||||||
|
export default class uUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cookie
|
||||||
|
* @param {string} name
|
||||||
|
* @param {(string|number)} value
|
||||||
|
* @param {number=} days
|
||||||
|
*/
|
||||||
|
static setCookie(name, value, days) {
|
||||||
|
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) { // eslint-disable-line no-unused-vars
|
||||||
|
const args = Array.prototype.slice.call(arguments);
|
||||||
|
const format = args.shift();
|
||||||
|
let i = 0;
|
||||||
|
return format.replace(/%%|%s|%d/g, (match) => {
|
||||||
|
if (match === '%%') {
|
||||||
|
return '%';
|
||||||
|
}
|
||||||
|
return (typeof args[i] != 'undefined') ? args[i++] : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add script tag
|
||||||
|
* @param {string} url attribute
|
||||||
|
* @param {string} id attribute
|
||||||
|
* @param {Function=} onload
|
||||||
|
*/
|
||||||
|
static addScript(url, id, onload) {
|
||||||
|
if (id && document.getElementById(id)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode string for HTML
|
||||||
|
* @param {string} s
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static htmlEncode(s) {
|
||||||
|
return s.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hex string and opacity to an rgba string
|
||||||
|
* @param {string} hex
|
||||||
|
* @param {number} opacity
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static hexToRGBA(hex, opacity) {
|
||||||
|
return 'rgba(' + (hex = hex.replace('#', ''))
|
||||||
|
.match(new RegExp('(.{' + hex.length / 3 + '})', 'g'))
|
||||||
|
.map((l) => parseInt(hex.length % 2 ? l + l : l, 16))
|
||||||
|
.concat(opacity || 1).join(',') + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add link tag with type css
|
||||||
|
* @param {string} url attribute
|
||||||
|
* @param {string} id attribute
|
||||||
|
*/
|
||||||
|
static addCss(url, id) {
|
||||||
|
if (id && document.getElementById(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = document.createElement('link');
|
||||||
|
tag.type = 'text/css';
|
||||||
|
tag.rel = 'stylesheet';
|
||||||
|
tag.href = url;
|
||||||
|
if (id) {
|
||||||
|
tag.id = id;
|
||||||
|
}
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of first XML child node with given name
|
||||||
|
* @param {(Element|XMLDocument)} node
|
||||||
|
* @param {string} name Node name
|
||||||
|
* @returns {?string} Node value or null if not found
|
||||||
|
*/
|
||||||
|
static getNode(node, name) {
|
||||||
|
const el = node.getElementsByTagName(name);
|
||||||
|
if (el.length) {
|
||||||
|
const children = el[0].childNodes;
|
||||||
|
if (children.length) {
|
||||||
|
return children[0].nodeValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of first XML child node with given name
|
||||||
|
* @param {(Element|XMLDocument)} node
|
||||||
|
* @param {string} name Node name
|
||||||
|
* @returns {?number} Node value or null if not found
|
||||||
|
*/
|
||||||
|
static getNodeAsFloat(node, name) {
|
||||||
|
const str = uUtils.getNode(node, name);
|
||||||
|
if (str != null) {
|
||||||
|
return parseFloat(str);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of first XML child node with given name
|
||||||
|
* @param {(Element|XMLDocument)} node
|
||||||
|
* @param {string} name Node name
|
||||||
|
* @returns {?number} Node value or null if not found
|
||||||
|
*/
|
||||||
|
static getNodeAsInt(node, name) {
|
||||||
|
const str = uUtils.getNode(node, name);
|
||||||
|
if (str != null) {
|
||||||
|
return parseInt(str);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of first XML child node with given name
|
||||||
|
* @param {(Element|XMLDocument)} node
|
||||||
|
* @param {string} name Node name
|
||||||
|
* @returns {Object<string, string>} Node value or null if not found
|
||||||
|
*/
|
||||||
|
static getNodesArray(node, name) {
|
||||||
|
const el = node.getElementsByTagName(name);
|
||||||
|
if (el.length) {
|
||||||
|
const obj = {};
|
||||||
|
const children = el[0].childNodes;
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
obj[child.nodeName] = child.firstChild ? child.firstChild.nodeValue : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of first XML child node with given name
|
||||||
|
* @param {(Element|XMLDocument)} node
|
||||||
|
* @param {string} name Node name
|
||||||
|
* @returns {?number} Node value or null if not found
|
||||||
|
*/
|
||||||
|
static getAttributeAsInt(node, name) {
|
||||||
|
const str = node.getAttribute(name);
|
||||||
|
if (str != null) {
|
||||||
|
return parseInt(str);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// seconds to (d) H:M:S
|
||||||
|
Number.prototype.toHMS = function () {
|
||||||
|
let s = this;
|
||||||
|
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 + ' 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;
|
||||||
|
};
|
26
login.php
26
login.php
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="<?= uConfig::$lang ?>">
|
||||||
<head>
|
<head>
|
||||||
<title><?= $lang["title"] ?></title>
|
<title><?= $lang["title"] ?></title>
|
||||||
<?php include("meta.php"); ?>
|
<?php include("meta.php"); ?>
|
||||||
@ -42,18 +42,20 @@
|
|||||||
<div id="title"><?= $lang["title"] ?></div>
|
<div id="title"><?= $lang["title"] ?></div>
|
||||||
<div id="subtitle"><?= $lang["private"] ?></div>
|
<div id="subtitle"><?= $lang["private"] ?></div>
|
||||||
<form action="<?= BASE_URL ?>" method="post">
|
<form action="<?= BASE_URL ?>" method="post">
|
||||||
<?= $lang["username"] ?>:<br>
|
<label for="login-user"><?= $lang["username"] ?></label><br>
|
||||||
<input type="text" name="user"><br>
|
<input id="login-user" type="text" name="user" required><br>
|
||||||
<?= $lang["password"] ?>:<br>
|
<label for="login-pass"><?= $lang["password"] ?></label><br>
|
||||||
<input type="password" name="pass"><br>
|
<input id="login-pass" type="password" name="pass" required><br>
|
||||||
<br>
|
<br>
|
||||||
<input type="submit" value="<?= $lang["login"] ?>">
|
<input type="submit" value="<?= $lang["login"] ?>">
|
||||||
<input type="hidden" name="action" value="auth">
|
<input type="hidden" name="action" value="auth">
|
||||||
<?php if (!uConfig::$require_authentication): ?>
|
<?php if (!uConfig::$require_authentication): ?>
|
||||||
<div id="cancel"><a href="<?= BASE_URL ?>"><?= $lang["cancel"] ?></a></div>
|
<div id="cancel"><a href="<?= BASE_URL ?>"><?= $lang["cancel"] ?></a></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
<div id="error"><?= (($auth_error) ? $lang["authfail"] : "") ?></div>
|
<?php if ($auth_error): ?>
|
||||||
|
<div id="error"><?= $lang["authfail"] ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
7252
package-lock.json
generated
Normal file
7252
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -33,20 +33,23 @@
|
|||||||
if (empty($pass)) {
|
if (empty($pass)) {
|
||||||
uUtils::exitWithError("Empty password");
|
uUtils::exitWithError("Empty password");
|
||||||
}
|
}
|
||||||
if ($auth->isAdmin() && !empty($login)) {
|
if (empty($login)) {
|
||||||
// different user, only admin
|
uUtils::exitWithError("Empty login");
|
||||||
$passUser = new uUser($login);
|
}
|
||||||
if (!$passUser->isValid) {
|
if ($auth->user->login === $login) {
|
||||||
uUtils::exitWithError("User unknown");
|
|
||||||
}
|
|
||||||
} else if (!empty($login)) {
|
|
||||||
uUtils::exitWithError("Unauthorized");
|
|
||||||
} else {
|
|
||||||
// current user
|
// current user
|
||||||
$passUser = $auth->user;
|
$passUser = $auth->user;
|
||||||
if (!$passUser->validPassword($oldpass)) {
|
if (!$passUser->validPassword($oldpass)) {
|
||||||
uUtils::exitWithError("Wrong old password");
|
uUtils::exitWithError("Wrong old password");
|
||||||
}
|
}
|
||||||
|
} else if ($auth->isAdmin()) {
|
||||||
|
// different user, only admin
|
||||||
|
$passUser = new uUser($login);
|
||||||
|
if (!$passUser->isValid) {
|
||||||
|
uUtils::exitWithError("User unknown");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uUtils::exitWithError("Unauthorized");
|
||||||
}
|
}
|
||||||
if ($passUser->setPass($pass) === false) {
|
if ($passUser->setPass($pass) === false) {
|
||||||
uUtils::exitWithError("Server error");
|
uUtils::exitWithError("Server error");
|
||||||
|
75
utils/getconstants.php
Normal file
75
utils/getconstants.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
/* μ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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once(dirname(__DIR__) . "/helpers/auth.php");
|
||||||
|
require_once(ROOT_DIR . "/helpers/config.php");
|
||||||
|
require_once(ROOT_DIR . "/helpers/lang.php");
|
||||||
|
|
||||||
|
$auth = new uAuth();
|
||||||
|
$langStrings = (new uLang(uConfig::$lang))->getStrings();
|
||||||
|
|
||||||
|
header("Content-type: text/xml");
|
||||||
|
$xml = new XMLWriter();
|
||||||
|
$xml->openURI("php://output");
|
||||||
|
$xml->startDocument("1.0");
|
||||||
|
$xml->setIndent(true);
|
||||||
|
$xml->startElement('root');
|
||||||
|
|
||||||
|
|
||||||
|
$xml->startElement("auth");
|
||||||
|
$xml->writeElement("isAdmin", $auth->isAdmin());
|
||||||
|
$xml->writeElement("isAuthenticated", $auth->isAuthenticated());
|
||||||
|
if ($auth->isAuthenticated()) {
|
||||||
|
$xml->writeElement("userId", $auth->user->id);
|
||||||
|
$xml->writeElement("userLogin", $auth->user->login);
|
||||||
|
}
|
||||||
|
$xml->endElement();
|
||||||
|
|
||||||
|
$xml->startElement("config");
|
||||||
|
$xml->writeElement("interval", uConfig::$interval);
|
||||||
|
$xml->writeElement("units", uConfig::$units);
|
||||||
|
$xml->writeElement("mapapi", uConfig::$mapapi);
|
||||||
|
$xml->writeElement("gkey", uConfig::$gkey);
|
||||||
|
$xml->startElement("ol_layers");
|
||||||
|
foreach (uConfig::$ol_layers as $key => $val) {
|
||||||
|
$xml->writeElement($key, $val);
|
||||||
|
}
|
||||||
|
$xml->endElement();
|
||||||
|
$xml->writeElement("init_latitude", uConfig::$init_latitude);
|
||||||
|
$xml->writeElement("init_longitude", uConfig::$init_longitude);
|
||||||
|
$xml->writeElement("pass_regex", uConfig::passRegex());
|
||||||
|
$xml->writeElement("strokeWeight", uConfig::$strokeWeight);
|
||||||
|
$xml->writeElement("strokeColor", uConfig::$strokeColor);
|
||||||
|
$xml->writeElement("strokeOpacity", uConfig::$strokeOpacity);
|
||||||
|
$xml->endElement();
|
||||||
|
|
||||||
|
$xml->startElement("lang");
|
||||||
|
$xml->startElement("strings");
|
||||||
|
foreach ($langStrings as $key => $val) {
|
||||||
|
$xml->writeElement($key, $val);
|
||||||
|
}
|
||||||
|
$xml->endElement();
|
||||||
|
$xml->endElement();
|
||||||
|
|
||||||
|
|
||||||
|
$xml->endElement();
|
||||||
|
$xml->endDocument();
|
||||||
|
$xml->flush();
|
||||||
|
|
||||||
|
?>
|
@ -25,6 +25,7 @@ $auth = new uAuth();
|
|||||||
|
|
||||||
$userId = uUtils::getInt('userid');
|
$userId = uUtils::getInt('userid');
|
||||||
$trackId = uUtils::getInt('trackid');
|
$trackId = uUtils::getInt('trackid');
|
||||||
|
$afterId = uUtils::getInt('afterid');
|
||||||
$last = uUtils::getInt('last');
|
$last = uUtils::getInt('last');
|
||||||
|
|
||||||
$positionsArr = [];
|
$positionsArr = [];
|
||||||
@ -33,7 +34,7 @@ if ($userId) {
|
|||||||
($auth->isAuthenticated() && ($auth->isAdmin() || $auth->user->id === $userId))) {
|
($auth->isAuthenticated() && ($auth->isAdmin() || $auth->user->id === $userId))) {
|
||||||
if ($trackId) {
|
if ($trackId) {
|
||||||
// get all track data
|
// get all track data
|
||||||
$positionsArr = uPosition::getAll($userId, $trackId);
|
$positionsArr = uPosition::getAll($userId, $trackId, $afterId);
|
||||||
} else if ($last) {
|
} else if ($last) {
|
||||||
// get data only for latest point
|
// get data only for latest point
|
||||||
$position = uPosition::getLast($userId);
|
$position = uPosition::getLast($userId);
|
||||||
|
@ -35,14 +35,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$aUser = new uUser($login);
|
$aUser = new uUser($login);
|
||||||
|
$data = NULL;
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'add':
|
case 'add':
|
||||||
if ($aUser->isValid) {
|
if ($aUser->isValid) {
|
||||||
uUtils::exitWithError($lang["userexists"]);
|
uUtils::exitWithError($lang["userexists"]);
|
||||||
}
|
}
|
||||||
if (empty($pass) || uUser::add($login, $pass) === false) {
|
if (empty($pass) || ($userId = uUser::add($login, $pass)) === false) {
|
||||||
uUtils::exitWithError($lang["servererror"]);
|
uUtils::exitWithError($lang["servererror"]);
|
||||||
|
} else {
|
||||||
|
$data = [ 'userid' => $userId ];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -64,6 +67,6 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uUtils::exitWithSuccess();
|
uUtils::exitWithSuccess($data);
|
||||||
|
|
||||||
?>
|
?>
|
Loading…
x
Reference in New Issue
Block a user