Remove obsolete files

This commit is contained in:
Bartek Fabiszewski 2019-12-29 22:50:51 +01:00
parent 8e2356aca0
commit 83a1546e28
24 changed files with 0 additions and 3610 deletions

View File

@ -1,109 +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/>.
*/
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'
* @return {Promise<Document, string>}
*/
static ajax(url, data, options) {
const params = [];
data = data || {};
options = options || {};
let method = options.method || 'GET';
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);
}
};
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);
});
}
}

View File

@ -1,96 +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/>.
*/
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);
});
}
}

View File

@ -1,77 +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/>.
*/
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);
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,172 +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/>.
*/
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.MARKER_OVER, this);
binder.addEventListener(uEvent.MARKER_SELECT, this);
binder.addEventListener(uEvent.TRACK_READY, this);
binder.addEventListener(uEvent.UI_READY, this);
this._binder = binder;
this._targetEl = null;
this._points = 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', () => {
this._points = document.querySelectorAll('.ct-chart-line .ct-point');
const len = this._points.length;
for (let i = 0; i < len; i++) {
((id) => {
this._points[id].addEventListener('click', () => {
this._binder.dispatchEvent(uEvent.CHART_CLICKED, id);
});
})(i);
}
this._binder.dispatchEvent(uEvent.CHART_READY, len);
});
// need to update chart first time the container becomes visible
if (!this.isVisible()) {
const observer = new MutationObserver(() => {
if (this.isVisible()) {
// eslint-disable-next-line no-underscore-dangle
this._targetEl.__chartist__.update();
observer.disconnect();
}
});
observer.observe(this._targetEl.parentNode, { attributes: true });
}
}
isVisible() {
return this._targetEl && this._targetEl.parentNode && this._targetEl.parentNode.style.display === 'block';
}
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;
} else if (event.type === uEvent.MARKER_OVER) {
/** @type {number} */
const pointId = args;
if (pointId) {
this.pointOver(pointId);
} else {
this.pointOut();
}
} else if (event.type === uEvent.MARKER_SELECT) {
/** @type {number} */
const pointId = args;
if (pointId) {
this.pointSelect(pointId);
} else {
this.pointUnselect();
}
}
}
pointOver(pointId) {
if (this.isVisible()) {
const point = this._points[pointId];
point.classList.add('ct-point-hilight');
}
}
pointOut() {
this._targetEl.querySelectorAll('.ct-point-hilight').forEach((el) => el.classList.remove('ct-point-hilight'));
}
pointSelect(pointId) {
if (this.isVisible()) {
const point = this._points[pointId];
point.classList.add('ct-point-selected');
}
}
pointUnselect() {
this._targetEl.querySelectorAll('.ct-point-selected').forEach((el) => el.classList.remove('ct-point-selected'));
}
}

View File

@ -1,371 +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/>.
*/
import uEvent from './event.js';
export default class uConfig {
constructor() {
this.inititialize();
}
inititialize() {
this.interval = 10;
this.units = 'metric';
this.mapapi = 'openlayers';
this.gkey = null;
this.ol_layers = {};
this.init_latitude = 52.23;
this.init_longitude = 21.01;
this.pass_regex = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{12,})');
this.strokeWeight = 2;
this.strokeColor = '#ff0000';
this.strokeOpacity = 1;
this.showLatest = false;
// marker colors
this.colorNormal = '#fff';
this.colorStart = '#55b500';
this.colorStop = '#ff6a00';
this.colorExtra = '#ccc';
this.colorHilite = '#feff6a';
}
/**
*
* @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;
if (this._units === 'imperial') {
this._factor_kmh = 0.62; // to mph
this._unit_kmh = 'mph';
this._factor_m = 3.28; // to feet
this._unit_m = 'ft';
this._factor_km = 0.62; // to miles
this._unit_km = 'mi';
} else if (this._units === 'nautical') {
this._factor_kmh = 0.54; // to knots
this._unit_kmh = 'kt';
this._factor_m = 1; // meters
this._unit_m = 'm';
this._factor_km = 0.54; // to nautical miles
this._unit_km = 'nm';
} else {
this._factor_kmh = 1;
this._unit_kmh = 'km/h';
this._factor_m = 1;
this._unit_m = 'm';
this._factor_km = 1;
this._unit_km = 'km';
}
}
/**
* @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;
}
/**
* @return {string}
*/
get unit_kmh() {
return this._unit_kmh;
}
/**
* @return {number}
*/
get factor_m() {
return this._factor_m;
}
/**
* @return {string}
*/
get unit_m() {
return this._unit_m;
}
/**
* @return {number}
*/
get factor_km() {
return this._factor_km;
}
/**
* @return {string}
*/
get unit_km() {
return this._unit_km;
}
/**
* @return {boolean}
*/
get showLatest() {
return this._showLatest;
}
/**
* @param {boolean} value
*/
set showLatest(value) {
if (this._showLatest !== value) {
this._showLatest = value;
this.notify('showLatest');
}
}
/**
* @return {string}
*/
get colorNormal() {
return this._colorNormal;
}
/**
* @param {string} value
*/
set colorNormal(value) {
this._colorNormal = value;
}
/**
* @return {string}
*/
get colorStart() {
return this._colorStart;
}
/**
* @param {string} value
*/
set colorStart(value) {
this._colorStart = value;
}
/**
* @return {string}
*/
get colorStop() {
return this._colorStop;
}
/**
* @param {string} value
*/
set colorStop(value) {
this._colorStop = value;
}
/**
* @return {string}
*/
get colorExtra() {
return this._colorExtra;
}
/**
* @param {string} value
*/
set colorExtra(value) {
this._colorExtra = value;
}
/**
* @return {string}
*/
get colorHilite() {
return this._colorHilite;
}
/**
* @param {string} value
*/
set colorHilite(value) {
this._colorHilite = value;
}
}

View File

@ -1,117 +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/>.
*/
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');
}
}
}
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;

View File

@ -1,69 +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/>.
*/
/**
* @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);
}
}

View File

@ -1,78 +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/>.
*/
/* 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_CLICKED() { return 'µChartClicked'; }
static get CONFIG() { return 'µConfig'; }
static get CHANGE() { return 'µChange'; }
static get CHART_READY() { return 'µChartReady'; }
static get EDIT() { return 'µEdit'; }
static get EXPORT() { return 'µExport'; }
static get IMPORT() { return 'µImport'; }
static get LOADER() { return 'µLoader'; }
static get MARKER_OVER() { return 'µMarkerOver'; }
static get MARKER_SELECT() { return 'µMarkerSelect'; }
static get OPEN_URL() { return 'µOpen'; }
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 ? args.constructor.name : ''}`);
await listener(this, args);
})();
}
}
}

View File

@ -1,283 +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/>.
*/
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.binder) {
this.binder.addEventListener(uEvent.ADD, this);
this.binder.addEventListener(uEvent.CHANGE, this);
this.binder.addEventListener(uEvent.CONFIG, 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';
}
get showAllOption() {
return this._showAllOption;
}
set showAllOption(value) {
if (this._showAllOption !== value) {
this._showAllOption = value;
if (value === false) {
this.selectDefault();
}
this.render();
this.onChange();
}
}
/**
* @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();
}
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 {uEvent} event
* @param {*=} eventData
*/
handleEvent(event, eventData) {
if (event.type === uEvent.CHANGE && eventData.el === this.domElement) {
this.selectedId = eventData.id;
this.onChange();
} else if (event.type === uEvent.EDIT && eventData === this.domElement) {
this.onEdit();
} else if (event.type === uEvent.ADD && eventData === this.domElement) {
this.onAdd();
} else if (event.type === uEvent.CONFIG) {
this.onConfigChange(eventData);
}
}
// /**
// * @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) {
this.selectDefault();
this.onChange();
}
this.render();
}
selectDefault() {
if (this.data.length) {
this.selectedId = this.data[0].key.toString();
} else {
this.selectedId = '';
}
}
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
onReload() {
}
/**
* @abstract
*/
// eslint-disable-next-line no-unused-vars,no-empty-function,class-methods-use-this
onAdd() {
}
/**
* @abstract
* @param {string} property
*/
// eslint-disable-next-line no-unused-vars,no-empty-function,class-methods-use-this
onConfigChange(property) {
}
}

147
js/map.js
View File

@ -1,147 +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/>.
*/
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';
/**
* @typedef {Object} uMap.api
* @memberOf uMap
* @type {Object}
* @property {string} name
* @property {function(uBinder, HTMLElement)} init
* @property {function} cleanup
* @property {function(uTrack, boolean)} displayTrack
* @property {function} clearMap
* @property {function(number)} animateMarker
* @property {function} getBounds
* @property {function} zoomToExtent
* @property {function} zoomToBounds
* @property {function} updateSize
*/
/**
* @class 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.API_CHANGE, this);
binder.addEventListener(uEvent.CHART_CLICKED, this);
binder.addEventListener(uEvent.TRACK_READY, this);
binder.addEventListener(uEvent.UI_READY, this);
this.loadTime = 0;
this.savedBounds = null;
this.api = null;
this.mapElement = null;
this.lastTrackId = null;
this._binder = binder;
this.track = 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._binder, this.mapElement);
} catch (e) {
setTimeout(() => {
this.loadTime += 50;
this.waitAndInit();
}, 50);
return;
}
this.loadTime = 0;
if (this.savedBounds) {
this.api.zoomToBounds(this.savedBounds);
}
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) {
/** @type {uTrack} */
const track = args;
this.api.clearMap();
const onlyReload = track.id !== this.lastTrackId;
this.api.displayTrack(track, onlyReload);
this.lastTrackId = track.id;
} 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);
} else if (event.type === uEvent.CHART_CLICKED) {
/** @type {number} */
const id = args;
this.api.animateMarker(id);
}
}
}

View File

@ -1,145 +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/>.
*/
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
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,85 +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/>.
*/
import uUtils from './utils.js';
/**
* @class uPosition
* @property {number} id
* @property {number} latitude
* @property {number} longitude
* @property {?number} altitude
* @property {?number} speed
* @property {?number} bearing
* @property {?number} accuracy
* @property {?string} provider
* @property {?string} comment
* @property {?string} image
* @property {string} username
* @property {string} trackname
* @property {number} trackid
* @property {number} timestamp
* @property {number} 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.comment = uUtils.getNode(xml, 'comment'); // may be null
position.image = uUtils.getNode(xml, 'image'); // 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;
}
/**
* @return {boolean}
*/
hasComment() {
return (this.comment != null && this.comment.length);
}
/**
* @return {boolean}
*/
hasImage() {
return (this.image != null && this.image.length);
}
}

View File

@ -1,213 +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/>.
*/
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 {boolean} continuous
* @property {?uPosition[]} positions
* @property {?Array<{x: number, y: number}>} plotData
*/
export default class uTrack extends uData {
/**
* @param {number} id
* @param {string} name
* @param {uUser} user
*/
constructor(id, name, user) {
super(id, name, 'id', 'name');
this._user = user;
this._positions = null;
this._plotData = null;
this._maxId = 0;
this._onlyLatest = false;
this._continuous = true;
}
/**
* @return {?uPosition[]}
*/
get positions() {
return this._positions;
}
/**
* @param {uUser} user
*/
set user(user) {
this._user = user;
}
/**
* @return {uUser}
*/
get user() {
return this._user;
}
/**
* @param {boolean} value
*/
set continuous(value) {
this._continuous = value;
}
/**
* @return {boolean}
*/
get continuous() {
return this._continuous;
}
/**
* @param {boolean} value
*/
set onlyLatest(value) {
this._onlyLatest = value;
}
clear() {
this._positions = null;
this._plotData = null;
}
/**
* Get track data from xml
* @param {XMLDocument} xml XML with positions data
* @param {boolean} isUpdate If true append to old data
*/
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).then((xml) => {
this.fromXml(xml, isUpdate);
this.render();
return xml;
});
}
/**
* Save track data
* @param {string} action
* @return {Promise<void>}
*/
update(action) {
return uAjax.post('utils/handletrack.php',
{
action: action,
trackid: this.id,
trackname: this.name
});
}
/**
* Render track
*/
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);
}
}

View File

@ -1,109 +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/>.
*/
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;
}
}

View File

@ -1,242 +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/>.
*/
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.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 === uEvent.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}`));
}
}
/**
* @param {HTMLFormElement} form
* @return {Promise<void>}
*/
import(form) {
this.emit(true, 'import');
return uAjax.post('utils/import.php', form)
.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');
this.emit(false, 'import');
return this.fetch().then(() => this.select(trackId));
}).catch((msg) => {
this.emit(false, 'import');
alert(`${lang.strings['actionfailure']}\n${msg}`);
});
}
emit(on, action) {
this.binder.dispatchEvent(uEvent.LOADER, { on: on, action: action });
}
/**
* Fetch tracks for current user
* @throws
* @return {Promise<Document, string>}
*/
fetch() {
this.emit(true, 'track');
return uAjax.get('utils/gettracks.php',
{
userid: uLogger.userList.current.id
})
.then((xml) => {
this.clear();
this.fromXml(xml.getElementsByTagName('track'), 'trackid', 'trackname');
this.emit(false, 'track');
return xml;
}).catch((msg) => {
this.emit(false, 'track');
alert(`${lang.strings['actionfailure']}\n${msg}`);
});
}
/**
* Fetch track with latest position for current user
* @throws
* @return {Promise<Document, string>}
*/
fetchLatest() {
this.emit(true, 'track');
const data = {
last: 1
};
const allUsers = uLogger.userList.isSelectedAllOption;
if (!allUsers) {
data.userid = uLogger.userList.current.id;
}
return uAjax.get('utils/getpositions.php', data).then((xml) => {
if (!allUsers) {
const xmlPos = xml.getElementsByTagName('position');
// single user
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;
this.current.render();
} else {
// tracklist needs update
return this.fetch().then(() => this.fetchLatest());
}
}
} else {
// all users
this.clear();
const track = new uTrack(0, '', null);
track.binder = this.binder;
track.continuous = false;
track.fromXml(xml, false);
this.add(track);
this.select(0, true);
this.current.render();
}
this.emit(false, 'track');
return xml;
}).catch((msg) => {
this.emit(false, 'track');
alert(`${lang.strings['actionfailure']}\n${msg}`);
});
}
/**
* @override
*/
onChange() {
if (!config.showLatest) {
this.fetchTrack();
}
}
/**
* Fetch and render track
*/
fetchTrack() {
if (this.current) {
this.emit(true, 'track');
this.current.fetch()
.then(() => this.emit(false, 'track'))
.catch((msg) => {
this.emit(false, 'track');
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() {
}
}

559
js/ui.js
View File

@ -1,559 +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/>.
*/
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);
binder.addEventListener(uEvent.LOADER, this);
binder.addEventListener(uEvent.TRACK_READY, this);
document.addEventListener('DOMContentLoaded', () => { this.initUI(); });
this.isLiveOn = false;
}
/**
* Initialize uUI elements
*/
initUI() {
/** @type {HTMLElement} */
this.menu = document.getElementById('menu');
/** @type {?HTMLElement} */
this.userMenu = document.getElementById('user-menu');
/** @type {?HTMLElement} */
this.userDropdown = document.getElementById('user-dropdown');
/** @type {?HTMLElement} */
this.userPass = document.getElementById('user-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.apiSelect = document.getElementsByName('api')[0];
// noinspection JSValidateTypes
/** @type {HTMLSelectElement} */
this.langSelect = document.getElementsByName('lang')[0];
// noinspection JSValidateTypes
/** @type {HTMLSelectElement} */
this.unitsSelect = 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 = document.querySelector('label[for="track"]');
/** @type {HTMLElement} */
this.importTitle = document.getElementById('import') || 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.interval = document.getElementById('interval');
/** @type {HTMLElement} */
this.setInterval = document.getElementById('set-interval');
/** @type {HTMLElement} */
this.exportKml = document.getElementById('export-kml');
/** @type {HTMLElement} */
this.exportGpx = document.getElementById('export-gpx');
/** @type {?HTMLElement} */
this.inputFile = document.getElementById('input-file');
/** @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.userMenu) {
this.userMenu.onclick = () => this.showUserMenu();
}
if (this.userPass) {
this.userPass.onclick = () => {
this.emit(uEvent.PASSWORD);
}
}
this.hideUserMenu = this.hideUserMenu.bind(this);
this.latest.onchange = () => uUI.toggleLatest();
this.autoReload.onchange = () => this.toggleAutoReload();
this.setInterval.onclick = () => this.setAutoReloadTime();
this.forceReload.onclick = () => this.trackReload();
this.chartLink.onclick = () => this.toggleChart();
this.trackSelect.onchange = () => {
const trackId = this.trackSelect.options[this.trackSelect.selectedIndex].value;
this.emit(uEvent.CHANGE, { el: this.trackSelect, id: trackId });
};
this.userSelect.onchange = () => {
const userId = this.userSelect.options[this.userSelect.selectedIndex].value;
this.emit(uEvent.CHANGE, { el: this.userSelect, id: userId });
};
this.apiSelect.onchange = () => {
const api = this.apiSelect.options[this.apiSelect.selectedIndex].value;
this.emit(uEvent.API_CHANGE, api);
};
this.langSelect.onchange = () => {
uUI.setLang(this.langSelect.options[this.langSelect.selectedIndex].value);
};
this.unitsSelect.onchange = () => {
uUI.setUnits(this.unitsSelect.options[this.unitsSelect.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.interval.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 parts = uUtils.getTimeString(new Date(pos.timestamp * 1000));
date = parts.date;
time = `${parts.time}<span class="smaller">${parts.zone}</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.hasComment()) ? `<div id="pcomments">${uUtils.htmlEncode(pos.comment)}</div>` : ''}
${(pos.hasImage()) ? `<div id="pimage"><img src="uploads/${pos.image}" alt="image"></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>`;
}
/**
* Update track summary
* @param {number} timestamp
* @param {number=} totalDistance Total distance (m)
* @param {number=} totalTime Total time (s)
*/
updateSummary(timestamp, totalDistance, totalTime) {
if (config.showLatest) {
const today = new Date();
const date = new Date(timestamp * 1000);
let dateString = '';
if (date.toDateString() !== today.toDateString()) {
dateString = `${date.getFullYear()}-${(`0${date.getMonth() + 1}`).slice(-2)}-${(`0${date.getDate()}`).slice(-2)}<br>`;
}
let timeString = date.toTimeString();
let offset;
if ((offset = timeString.indexOf(' ')) >= 0) {
timeString = `${timeString.substr(0, offset)} <span style="font-weight:normal">${timeString.substr(offset + 1)}</span>`;
}
this.summary.innerHTML = `
<div class="menu-title">${lang.strings['latest']}:</div>
${dateString}
${timeString}`;
} else {
this.summary.innerHTML = `
<div class="menu-title">${lang.strings['summary']}</div>
<div><img class="icon" alt="${lang.strings['tdistance']}" title="${lang.strings['tdistance']}" src="images/distance.svg"> ${(totalDistance.toKm() * config.factor_km).toFixed(2)} ${config.unit_km}</div>
<div><img class="icon" alt="${lang.strings['ttime']}" title="${lang.strings['ttime']}" src="images/time.svg"> ${totalTime.toHMS()}</div>`;
}
}
/**
* Clear map canvas
*/
clearMapCanvas() {
this.map.innerHTML = '';
}
/**
* Toggle user menu visibility
*/
showUserMenu() {
if (this.userDropdown.style.display === 'block') {
this.userDropdown.style.display = 'none';
} else {
this.userDropdown.style.display = 'block';
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.style.display = 'none';
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.TRACK_READY) {
/** @type {uTrack} */
const track = args;
if (track.hasPositions) {
const position = track.positions[track.positions.length - 1];
this.updateSummary(position.timestamp, position.totalDistance, position.totalSeconds);
}
} 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;
}
} else if (event.type === uEvent.LOADER) {
const el = args.action === 'track' ? this.trackTitle : this.importTitle;
if (args.on) {
uUI.setLoader(el);
} else {
uUI.removeLoader(el);
}
}
}
/**
* 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;
}
/**
* Get SVG marker path
* @param {boolean} isLarge Large marker with hole if true
* @return {string}
*/
static getMarkerPath(isLarge) {
const markerHole = 'M15,34.911c0,0,0.359-3.922,1.807-8.588c0.414-1.337,1.011-2.587,2.495-4.159' +
'c1.152-1.223,3.073-2.393,3.909-4.447c1.681-6.306-3.676-9.258-8.211-9.258c-4.536,0-9.893,2.952-8.211,9.258' +
'c0.836,2.055,2.756,3.225,3.91,4.447c1.484,1.572,2.08,2.822,2.495,4.159C14.64,30.989,15,34.911,15,34.911z M18,15.922' +
'c0,1.705-1.342,3.087-2.999,3.087c-1.657,0-3-1.382-3-3.087c0-1.704,1.343-3.086,3-3.086C16.658,12.836,18,14.218,18,15.922z';
const marker = 'M14.999,34.911c0,0,0.232-1.275,1.162-4.848c0.268-1.023,0.652-1.98,1.605-3.184' +
'c0.742-0.937,1.975-1.832,2.514-3.404c1.082-4.828-2.363-7.088-5.281-7.088c-2.915,0-6.361,2.26-5.278,7.088' +
'c0.538,1.572,1.771,2.468,2.514,3.404c0.953,1.203,1.337,2.16,1.604,3.184C14.77,33.635,14.999,34.911,14.999,34.911z';
return isLarge ? markerHole : marker;
}
/**
* Get marker extra mark
* @param {boolean} isLarge
* @return {string}
*/
static getMarkerExtra(isLarge) {
const offset1 = isLarge ? 'M26.074,13.517' : 'M23.328,20.715';
const offset2 = isLarge ? 'M28.232,10.942' : 'M25.486,18.141';
return `<path fill="none" stroke="red" stroke-width="2" d="${offset1}c0-3.961-3.243-7.167-7.251-7.167"/>
<path fill="none" stroke="red" stroke-width="2" d="${offset2}c-0.5-4.028-3.642-7.083-7.724-7.542"/>`;
}
/**
* Get inline SVG source
* @param {string} fill
* @param {boolean=} isLarge
* @param {boolean=} isExtra
* @return {string}
*/
static getSvgSrc(fill, isLarge, isExtra) {
const svg = `<svg viewBox="0 0 30 35" width="30px" height="35px" xmlns="http://www.w3.org/2000/svg">
<g><path stroke="black" fill="${fill}" d="${uUI.getMarkerPath(isLarge)}"/>${isExtra ? uUI.getMarkerExtra(isLarge) : ''}</g></svg>`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
}

View File

@ -1,48 +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/>.
*/
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);
});

View File

@ -1,75 +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/>.
*/
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
});
}
}

View File

@ -1,179 +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/>.
*/
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;
}
}

View File

@ -1,146 +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/>.
*/
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 (config.showLatest) {
if (this.isSelectedAllOption) {
uLogger.trackList.fetchLatest();
} else {
uLogger.trackList.fetch()
.then(() => uLogger.trackList.fetchLatest());
}
} else {
uLogger.trackList.fetch();
}
}
/**
* @override
*/
onConfigChange(property) {
if (property === 'showLatest') {
if (config.showLatest && this.data.length > 1) {
this.showAllOption = true;
} else if (!config.showLatest && this.showAllOption) {
this.showAllOption = false;
}
}
}
/**
* @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);
});
}
}

View File

@ -1,286 +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/>.
*/
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;
}
/**
* Format date to date, time and time zone strings
* Simplify zone name, eg.
* date: 2017-06-14, time: 11:42:19, zone: GMT+2 CEST
* @param {Date} date
* @return {{date: string, time: string, zone: string}}
*/
static getTimeString(date) {
let timeZone = '';
const dateStr = `${date.getFullYear()}-${(`0${date.getMonth() + 1}`).slice(-2)}-${(`0${date.getDate()}`).slice(-2)}`;
const timeStr = date.toTimeString().replace(/^\s*([^ ]+)([^(]*)(\([^)]*\))*/,
// eslint-disable-next-line max-params
(_, hours, zone, dst) => {
if (zone) {
timeZone = zone.replace(/(0(?=[1-9]00))|(00\b)/g, '');
if (dst && (/[A-Z]/).test(dst)) {
timeZone += dst.match(/\b[A-Z]+/g).join('');
}
}
return hours;
});
return { date: dateStr, time: timeStr, zone: timeZone };
}
}
// 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;
};