es6 initial changes

This commit is contained in:
Bartek Fabiszewski 2019-05-15 11:32:36 +02:00
parent 7f1170187c
commit 1339682b66
46 changed files with 11089 additions and 2489 deletions

View File

@ -426,7 +426,11 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [ "userid" => $this->testUserId ],
"form_params" => [
"login" => $this->testUser,
"pass" => $this->testPass,
"oldpass" => $this->testPass
],
];
$response = $this->http->post("/utils/changepass.php", $options);
$this->assertEquals(401, $response->getStatusCode(), "Unexpected status code");
@ -453,7 +457,7 @@ class InternalAPITest extends UloggerAPITestCase {
$this->assertEquals((string) $xml->message, "Empty password", "Wrong error message");
}
public function testChangePassNoUser() {
public function testChangePassUserUnknown() {
$this->assertTrue($this->authenticate(), "Authentication failed");
$options = [
@ -472,12 +476,31 @@ class InternalAPITest extends UloggerAPITestCase {
$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() {
$this->assertTrue($this->authenticate(), "Authentication failed");
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testAdminUser,
"oldpass" => "badpass",
"pass" => "newpass",
],
@ -497,6 +520,7 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testAdminUser,
"pass" => "newpass",
],
];
@ -517,6 +541,7 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testAdminUser,
"oldpass" => $this->testAdminPass,
"pass" => $newPass,
],
@ -539,6 +564,7 @@ class InternalAPITest extends UloggerAPITestCase {
$options = [
"http_errors" => false,
"form_params" => [
"login" => $this->testUser,
"oldpass" => $this->testPass,
"pass" => $newPass,
],

1
css/chartist.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -290,6 +290,10 @@ button {
margin-right: 5px;
}
button > * {
pointer-events: none;
}
#cancel {
margin-top: 0.5em;
}

View File

@ -230,14 +230,15 @@
return $positionsArr;
}
/**
* Get array of all positions
*
* @param int $userId Optional limit to given user id
* @param int $trackId Optional limit to given track id
* @return array|bool Array of uPosition positions, false on error
*/
public static function getAll($userId = NULL, $trackId = NULL) {
/**
* Get array of all positions
*
* @param int $userId Optional limit to given user id
* @param int $trackId Optional limit to given track id
* @param int $afterId Optional limit to positions with id greater then given id
* @return array|bool Array of uPosition positions, false on error
*/
public static function getAll($userId = NULL, $trackId = NULL, $afterId = NULL) {
$rules = [];
if (!empty($userId)) {
$rules[] = "p.user_id = " . self::db()->quote($userId);
@ -245,6 +246,9 @@
if (!empty($trackId)) {
$rules[] = "p.track_id = " . self::db()->quote($trackId);
}
if (!empty($trackId)) {
$rules[] = "p.id > " . self::db()->quote($afterId);
}
if (!empty($rules)) {
$where = "WHERE " . implode(" AND ", $rules);
} else {

View File

@ -44,7 +44,6 @@
$auth->exitWithRedirect("login.php");
}
$displayUserId = NULL;
$usersArr = [];
if ($auth->isAdmin() || uConfig::$public_tracks) {
@ -77,64 +76,8 @@
<head>
<title><?= $lang["title"] ?></title>
<?php include("meta.php"); ?>
<script>
/** @namespace uLogger */
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>
<script type="module" src="js/ulogger.js"></script>
<!-- <script src="dist/ulogger.js"></script>-->
</head>
<body>

View File

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

View File

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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
})();
}
}
}

View 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

File diff suppressed because one or more lines are too long

10
js/lib/chartist.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

232
js/list.js Normal file
View 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

File diff suppressed because it is too large Load Diff

110
js/map.js Normal file
View 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
View 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
View 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
View 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
}
}

View File

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

View File

@ -1,109 +1,176 @@
/* μ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 uAjax from './ajax.js';
import uData from './data.js';
import uEvent from './event.js';
import uPosition from './position.js';
/**
* @class uTrack
* @extends {uData}
* @property {number} id
* @property {string} name
* @property {uUser} user
* @property {?uPosition[]} positions
* @property {?Array<{x: number, y: number}>} plotData
*/
/** @namespace */
var uLogger = window.uLogger || {};
(function (ns) {
export default class uTrack extends uData {
/**
* Show edit track dialog
*/
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 {number} id
* @param {string} name
* @returns {boolean} True if confirmed
* @param {uUser} user
*/
function confirmedDelete(name) {
return confirm(ns.sprintf(ns.lang.strings['trackdelwarn'], '"' + name + '"'));
constructor(id, name, user) {
super(id, name, 'id', 'name');
this._user = user;
this._positions = null;
this._plotData = null;
this._maxId = 0;
this._onlyLatest = false;
}
/**
* Submit form dialog
* @param action
* @return {?uPosition[]}
*/
function submitTrack(action) {
var form = document.getElementById('trackForm');
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);
}
});
get positions() {
return this._positions;
}
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
View 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
View 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() {
}
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/**
* Convert hex string and opacity to an rgba string
* @param {string} hex
* @param {number} opacity
* @returns {string}
*/
static hexToRGBA(hex, opacity) {
return 'rgba(' + (hex = hex.replace('#', ''))
.match(new RegExp('(.{' + hex.length / 3 + '})', 'g'))
.map((l) => parseInt(hex.length % 2 ? l + l : l, 16))
.concat(opacity || 1).join(',') + ')';
}
/**
* Add link tag with type css
* @param {string} url attribute
* @param {string} id attribute
*/
static addCss(url, id) {
if (id && document.getElementById(id)) {
return;
}
const tag = document.createElement('link');
tag.type = 'text/css';
tag.rel = 'stylesheet';
tag.href = url;
if (id) {
tag.id = id;
}
document.getElementsByTagName('head')[0].appendChild(tag);
}
/**
* @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;
};

View File

@ -27,7 +27,7 @@
?>
<!DOCTYPE html>
<html>
<html lang="<?= uConfig::$lang ?>">
<head>
<title><?= $lang["title"] ?></title>
<?php include("meta.php"); ?>
@ -42,18 +42,20 @@
<div id="title"><?= $lang["title"] ?></div>
<div id="subtitle"><?= $lang["private"] ?></div>
<form action="<?= BASE_URL ?>" method="post">
<?= $lang["username"] ?>:<br>
<input type="text" name="user"><br>
<?= $lang["password"] ?>:<br>
<input type="password" name="pass"><br>
<br>
<input type="submit" value="<?= $lang["login"] ?>">
<input type="hidden" name="action" value="auth">
<?php if (!uConfig::$require_authentication): ?>
<div id="cancel"><a href="<?= BASE_URL ?>"><?= $lang["cancel"] ?></a></div>
<?php endif; ?>
<label for="login-user"><?= $lang["username"] ?></label><br>
<input id="login-user" type="text" name="user" required><br>
<label for="login-pass"><?= $lang["password"] ?></label><br>
<input id="login-pass" type="password" name="pass" required><br>
<br>
<input type="submit" value="<?= $lang["login"] ?>">
<input type="hidden" name="action" value="auth">
<?php if (!uConfig::$require_authentication): ?>
<div id="cancel"><a href="<?= BASE_URL ?>"><?= $lang["cancel"] ?></a></div>
<?php endif; ?>
</form>
<div id="error"><?= (($auth_error) ? $lang["authfail"] : "") ?></div>
<?php if ($auth_error): ?>
<div id="error"><?= $lang["authfail"] ?></div>
<?php endif; ?>
</div>
</body>
</html>

7252
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -33,20 +33,23 @@
if (empty($pass)) {
uUtils::exitWithError("Empty password");
}
if ($auth->isAdmin() && !empty($login)) {
// different user, only admin
$passUser = new uUser($login);
if (!$passUser->isValid) {
uUtils::exitWithError("User unknown");
}
} else if (!empty($login)) {
uUtils::exitWithError("Unauthorized");
} else {
if (empty($login)) {
uUtils::exitWithError("Empty login");
}
if ($auth->user->login === $login) {
// current user
$passUser = $auth->user;
if (!$passUser->validPassword($oldpass)) {
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) {
uUtils::exitWithError("Server error");

75
utils/getconstants.php Normal file
View 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();
?>

View File

@ -25,6 +25,7 @@ $auth = new uAuth();
$userId = uUtils::getInt('userid');
$trackId = uUtils::getInt('trackid');
$afterId = uUtils::getInt('afterid');
$last = uUtils::getInt('last');
$positionsArr = [];
@ -33,7 +34,7 @@ if ($userId) {
($auth->isAuthenticated() && ($auth->isAdmin() || $auth->user->id === $userId))) {
if ($trackId) {
// get all track data
$positionsArr = uPosition::getAll($userId, $trackId);
$positionsArr = uPosition::getAll($userId, $trackId, $afterId);
} else if ($last) {
// get data only for latest point
$position = uPosition::getLast($userId);

View File

@ -35,14 +35,17 @@
}
$aUser = new uUser($login);
$data = NULL;
switch ($action) {
case 'add':
if ($aUser->isValid) {
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"]);
} else {
$data = [ 'userid' => $userId ];
}
break;
@ -64,6 +67,6 @@
break;
}
uUtils::exitWithSuccess();
uUtils::exitWithSuccess($data);
?>