diff --git a/helpers/utils.php b/helpers/utils.php index ed8e955..4df8d9c 100644 --- a/helpers/utils.php +++ b/helpers/utils.php @@ -32,7 +32,7 @@ $upload_max_filesize = self::iniGetBytes('upload_max_filesize'); $post_max_size = self::iniGetBytes('post_max_size'); // post_max_size = 0 means unlimited size - if ($post_max_size == 0) { $post_max_size = $upload_max_filesize; } + if ($post_max_size === 0) { $post_max_size = $upload_max_filesize; } $memory_limit = self::iniGetBytes('memory_limit'); // memory_limit = -1 means no limit if ($memory_limit < 0) { $memory_limit = $post_max_size; } @@ -45,10 +45,11 @@ * * @param string $iniParam Ini parameter name * @return int Bytes + * @noinspection PhpMissingBreakStatementInspection */ private static function iniGetBytes($iniParam) { $iniStr = ini_get($iniParam); - $val = floatval($iniStr); + $val = (float) $iniStr; $suffix = substr(trim($iniStr), -1); if (ctype_alpha($suffix)) { switch (strtolower($suffix)) { @@ -89,22 +90,15 @@ * @param array|null $extra Optional array of extra parameters */ private static function exitWithStatus($isError, $extra = NULL) { - header("Content-type: text/xml"); - $xml = new XMLWriter(); - $xml->openURI("php://output"); - $xml->startDocument("1.0"); - $xml->setIndent(true); - $xml->startElement("root"); - $xml->writeElement("error", (int) $isError); + $output = []; + $output["error"] = $isError; if (!empty($extra)) { foreach ($extra as $key => $value) { - $xml->writeElement($key, $value); + $output[$key] = $value; } } - - $xml->endElement(); - $xml->endDocument(); - $xml->flush(); + header("Content-type: application/json"); + echo json_encode($output); exit; } @@ -115,9 +109,9 @@ * @return string URL */ public static function getBaseUrl() { - $proto = (!isset($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] == "" || $_SERVER["HTTPS"] == "off") ? "http://" : "https://"; + $proto = (!isset($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] === "" || $_SERVER["HTTPS"] === "off") ? "http://" : "https://"; // Check if we are behind an https proxy - if (isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] == "https") { + if (isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] === "https") { $proto = "https://"; } $host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : ""; @@ -168,7 +162,7 @@ public static function requestFile($name, $default = NULL) { if (isset($_FILES[$name])) { $files = $_FILES[$name]; - if (isset($files["name"]) && isset($files["type"]) && isset($files["size"]) && isset($files["tmp_name"])) { + if (isset($files["name"], $files["type"], $files["size"], $files["tmp_name"])) { return $_FILES[$name]; } } @@ -189,26 +183,23 @@ private static function requestString($name, $default, $type) { if (is_string(($val = self::requestValue($name, $default, $type)))) { return trim($val); - } else { - return $val; } + return $val; } private static function requestInt($name, $default, $type) { if (is_float(($val = self::requestValue($name, $default, $type, FILTER_VALIDATE_FLOAT)))) { return (int) round($val); - } else { - return self::requestValue($name, $default, $type, FILTER_VALIDATE_INT); } + return self::requestValue($name, $default, $type, FILTER_VALIDATE_INT); } private static function requestValue($name, $default, $type, $filters = FILTER_DEFAULT, $flags = NULL) { $input = filter_input($type, $name, $filters, $flags); - if ($input !== false && !is_null($input)) { + if ($input !== false && $input !== null) { return $input; - } else { - return $default; } + return $default; } } diff --git a/js/src/listitem.js b/js/src/listitem.js index 2f09c8d..1676c1e 100644 --- a/js/src/listitem.js +++ b/js/src/listitem.js @@ -17,6 +17,8 @@ * along with this program; if not, see . */ +import uSelect from './select.js'; + /** * @class uListItem * @property {string} listValue @@ -27,7 +29,12 @@ export default class uListItem { * @param {string|number} id * @param {string|number} value */ - constructor(id, value) { + constructor() { + this.listValue = uSelect.allValue; + this.listText = '-'; + } + + listItem(id, value) { this.listValue = String(id); this.listText = String(value); } diff --git a/js/src/mapapi/api_gmaps.js b/js/src/mapapi/api_gmaps.js index 5414e98..04c575d 100644 --- a/js/src/mapapi/api_gmaps.js +++ b/js/src/mapapi/api_gmaps.js @@ -19,6 +19,7 @@ import { config, lang } from '../initializer.js'; import MapViewModel from '../mapviewmodel.js'; +import uTrack from '../track.js'; import uUtils from '../utils.js'; // google maps @@ -52,7 +53,7 @@ export default class GoogleMapsApi { * @return {Promise} */ init() { - const params = `?${(config.gkey != null) ? `key=${config.gkey}&` : ''}callback=gm_loaded`; + const params = `?${(config.gkey) ? `key=${config.gkey}&` : ''}callback=gm_loaded`; const gmReady = Promise.all([ GoogleMapsApi.onScriptLoaded(), uUtils.loadScript(`https://maps.googleapis.com/maps/api/js${params}`, 'mapapi_gmaps', GoogleMapsApi.loadTimeoutMs) @@ -125,7 +126,7 @@ export default class GoogleMapsApi { /** * Display track - * @param {uTrack} track + * @param {uPositionSet} track * @param {boolean} update Should fit bounds if true */ displayTrack(track, update) { @@ -159,7 +160,7 @@ export default class GoogleMapsApi { // update polyline const position = track.positions[i]; const coordinates = new google.maps.LatLng(position.latitude, position.longitude); - if (track.continuous) { + if (track instanceof uTrack) { path.push(coordinates); } latlngbounds.extend(coordinates); @@ -175,7 +176,7 @@ export default class GoogleMapsApi { } }); setTimeout(function () { - google.maps.event.removeListener(zListener) + google.maps.event.removeListener(zListener); }, 2000); } } @@ -219,13 +220,12 @@ export default class GoogleMapsApi { /** * Set marker - * @param {uTrack} track + * @param {uPositionSet} track * @param {number} id */ setMarker(id, track) { // marker const position = track.positions[id]; - const posLen = track.length; // noinspection JSCheckFunctionSignatures const marker = new google.maps.Marker({ position: new google.maps.LatLng(position.latitude, position.longitude), @@ -234,9 +234,9 @@ export default class GoogleMapsApi { }); const isExtra = position.hasComment() || position.hasImage(); let icon; - if (id === posLen - 1) { + if (track.isLastPosition(id)) { icon = GoogleMapsApi.getMarkerIcon(config.colorStop, true, isExtra); - } else if (id === 0) { + } else if (track.isFirstPosition(id)) { icon = GoogleMapsApi.getMarkerIcon(config.colorStart, true, isExtra); } else { icon = GoogleMapsApi.getMarkerIcon(isExtra ? config.colorExtra : config.colorNormal, false, isExtra); @@ -295,7 +295,6 @@ export default class GoogleMapsApi { /** * Get map bounds - * eg. ((52.20105108685229, 20.789387865580238), (52.292069558807135, 21.172192736185707)) * @returns {number[]} Bounds [ lon_sw, lat_sw, lon_ne, lat_ne ] */ getBounds() { diff --git a/js/src/mapapi/api_openlayers.js b/js/src/mapapi/api_openlayers.js index 05f0444..904e44d 100644 --- a/js/src/mapapi/api_openlayers.js +++ b/js/src/mapapi/api_openlayers.js @@ -19,6 +19,7 @@ import MapViewModel from '../mapviewmodel.js'; import { config } from '../initializer.js'; +import uTrack from '../track.js'; import uUtils from '../utils.js'; /** @@ -89,7 +90,7 @@ export default class OpenLayersApi { */ init() { uUtils.addCss('css/ol.css', 'ol_css'); - const olReady = ol ? Promise.resolve() : import(/* webpackChunkName : "ol" */'../lib/ol.js').then((m) => { ol = m }); + const olReady = ol ? Promise.resolve() : import(/* webpackChunkName : "ol" */'../lib/ol.js').then((m) => { ol = m; }); return olReady.then(() => { this.initMap(); this.initLayers(); @@ -412,7 +413,7 @@ export default class OpenLayersApi { /** * Display track - * @param {uTrack} track Track + * @param {uPositionSet} track Track * @param {boolean} update Should fit bounds if true */ displayTrack(track, update) { @@ -423,7 +424,7 @@ export default class OpenLayersApi { for (let i = start; i < track.length; i++) { this.setMarker(i, track); } - if (track.continuous) { + if (track instanceof uTrack) { let lineString; if (this.layerTrack && this.layerTrack.getSource().getFeatures().length) { lineString = this.layerTrack.getSource().getFeatures()[0].getGeometry(); @@ -492,23 +493,23 @@ export default class OpenLayersApi { /** * Get marker style * @param {number} id - * @param {uTrack} track + * @param {uPositionSet} track * @return {Style} */ getMarkerStyle(id, track) { const position = track.positions[id]; let iconStyle = this.markerStyles.normal; if (position.hasComment() || position.hasImage()) { - if (id === track.length - 1) { + if (track.isLastPosition(id)) { iconStyle = this.markerStyles.stopExtra; - } else if (id === 0) { + } else if (track.isFirstPosition(id)) { iconStyle = this.markerStyles.startExtra; } else { iconStyle = this.markerStyles.extra; } - } else if (id === track.length - 1) { + } else if (track.isLastPosition(id)) { iconStyle = this.markerStyles.stop; - } else if (id === 0) { + } else if (track.isFirstPosition(id)) { iconStyle = this.markerStyles.start; } return iconStyle; @@ -517,7 +518,7 @@ export default class OpenLayersApi { /** * Set marker * @param {number} id - * @param {uTrack} track + * @param {uPositionSet} track */ setMarker(id, track) { // marker diff --git a/js/src/track.js b/js/src/track.js new file mode 100644 index 0000000..f4fc91b --- /dev/null +++ b/js/src/track.js @@ -0,0 +1,220 @@ +/* + * μ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 . + */ + +import { auth } from './initializer.js'; +import uAjax from './ajax.js'; +import uPosition from './position.js'; +import uPositionSet from './positionset.js'; +import uUser from './user.js'; +import uUtils from './utils.js'; + +/** + * Set of positions representing user's track + * @class uTrack + * @property {number} id + * @property {string} name + * @property {uUser} user + * @property {uPosition[]} positions + * @property {PlotData} plotData + */ +export default class uTrack extends uPositionSet { + + /** + * @param {number} id + * @param {string} name + * @param {uUser} user + */ + constructor(id, name, user) { + super(); + if (!Number.isSafeInteger(id) || id <= 0 || !name || !(user instanceof uUser)) { + throw new Error('Invalid argument for track constructor'); + } + this.id = id; + this.name = name; + this.user = user; + this.plotData = []; + this.maxId = 0; + this.listItem(id, name); + } + + clear() { + super.clear(); + this.maxId = 0; + this.plotData.length = 0; + } + + /** + * @param {uTrack} track + * @return {boolean} + */ + isEqualTo(track) { + return !!track && track.id === this.id; + } + + /** + * @return {boolean} + */ + get hasPlotData() { + return this.plotData.length > 0; + } + + /** + * Get track data from json + * @param {Object[]} posArr Positions data + * @param {boolean=} isUpdate If true append to old data + */ + fromJson(posArr, isUpdate = false) { + let totalMeters = 0; + let totalSeconds = 0; + let positions = []; + if (isUpdate && this.hasPositions) { + positions = this.positions; + const last = positions[this.length - 1]; + totalMeters = last.totalMeters; + totalSeconds = last.totalSeconds; + } else { + this.clear(); + } + for (const pos of posArr) { + const position = uPosition.fromJson(pos); + totalMeters += position.meters; + totalSeconds += position.seconds; + position.totalMeters = totalMeters; + position.totalSeconds = totalSeconds; + positions.push(position); + if (position.altitude != null) { + this.plotData.push({ x: position.totalMeters, y: position.altitude }); + } + if (position.id > this.maxId) { + this.maxId = position.id; + } + } + // update at the end to avoid observers update invidual points + this.positions = positions; + } + + /** + * @param {number} id + * @return {boolean} + */ + isLastPosition(id) { + return this.length > 0 && id === this.length - 1; + } + + /** + * @param {number} id + * @return {boolean} + */ + isFirstPosition(id) { + return this.length > 0 && id === 0; + } + + /** + * Fetch track positions + * @return {Promise} + */ + fetchPositions() { + const params = { + userid: this.user.id, + trackid: this.id + }; + if (this.maxId) { + params.afterid = this.maxId; + } + return uPositionSet.fetch(params).then((_positions) => { + this.fromJson(_positions, params.afterid > 0); + }); + } + + /** + * Fetch track with latest position of a user. + * @param {uUser} user + * @return {Promise} + */ + static fetchLatest(user) { + return this.fetch({ + last: true, + userid: user.id + }).then((_positions) => { + if (_positions.length) { + const track = new uTrack(_positions[0].trackid, _positions[0].trackname, user); + track.fromJson(_positions); + return track; + } + return null; + }); + } + + /** + * Fetch tracks for given user + * @throws + * @param {uUser} user + * @return {Promise} + */ + static fetchList(user) { + return uAjax.get('utils/gettracks.php', { userid: user.id }).then( + /** + * @param {Array.<{id: number, name: string}>} _tracks + * @return {uTrack[]} + */ + (_tracks) => { + const tracks = []; + for (const track of _tracks) { + tracks.push(new uTrack(track.id, track.name, user)); + } + return tracks; + }); + } + + /** + * Export to file + * @param {string} type File type + */ + export(type) { + if (this.hasPositions) { + const url = `utils/export.php?type=${type}&userid=${this.user.id}&trackid=${this.id}`; + uUtils.openUrl(url); + } + } + + /** + * Imports tracks submited with HTML form and returns last imported track id + * @param {HTMLFormElement} form + * @return {Promise} + */ + static import(form) { + if (!auth.isAuthenticated) { + throw new Error('User not authenticated'); + } + return uAjax.post('utils/import.php', form) + .then( + /** + * @param {Array.<{id: number, name: string}>} _tracks + * @return {uTrack[]} + */ + (_tracks) => { + const tracks = []; + for (const track of _tracks) { + tracks.push(new uTrack(track.id, track.name, auth.user)); + } + return tracks; + }); + } + +} diff --git a/js/src/user.js b/js/src/user.js index 19a75b0..d4cef04 100644 --- a/js/src/user.js +++ b/js/src/user.js @@ -33,9 +33,13 @@ export default class uUser extends uListItem { * @param {string} login */ constructor(id, login) { - super(id, login); + super(); + if (!Number.isSafeInteger(id) || id <= 0) { + throw new Error('Invalid argument for user constructor'); + } this.id = id; this.login = login; + this.listItem(id, login); } /** diff --git a/js/src/utils.js b/js/src/utils.js new file mode 100644 index 0000000..e79c8ef --- /dev/null +++ b/js/src/utils.js @@ -0,0 +1,317 @@ +/* + * μ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 . + */ + +export default class uUtils { + + /** + * Set cookie + * @param {string} name + * @param {(string|number)} value + * @param {?number=} days Default validity is 30 days, null = never expire + */ + static setCookie(name, value, days = 30) { + let expires = ''; + if (days) { + const date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = `; expires=${date.toUTCString()}`; + } + document.cookie = `ulogger_${name}=${value}${expires}; path=/`; + } + + /** + * sprintf, naive approach, only %s, %d supported + * @param {string} fmt String + * @param {...(string|number)=} params Optional parameters + * @returns {string} + */ + static sprintf(fmt, params) { // 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 + * @param {Function=} onerror + */ + // eslint-disable-next-line max-params + static addScript(url, id, onload, onerror) { + if (id && document.getElementById(id)) { + if (onload instanceof Function) { + onload(); + } + return; + } + const tag = document.createElement('script'); + tag.type = 'text/javascript'; + tag.src = url; + if (id) { + tag.id = id; + } + tag.async = true; + if (onload instanceof Function) { + tag.onload = onload; + } + if (onerror instanceof Function) { + tag.onerror = () => onerror(new Error(`error loading ${id} script`)); + } + + document.getElementsByTagName('head')[0].appendChild(tag); + } + + /** + * Load script with timeout + * @param {string} url URL + * @param {string} id Element id + * @param {number=} ms Timeout in ms + * @return {Promise} + */ + static loadScript(url, id, ms = 10000) { + const scriptLoaded = new Promise( + (resolve, reject) => uUtils.addScript(url, id, resolve, reject)); + const timeout = this.timeoutPromise(ms); + return Promise.race([ scriptLoaded, timeout ]); + } + + static timeoutPromise(ms) { + return new Promise((resolve, reject) => { + const tid = setTimeout(() => { + clearTimeout(tid); + reject(new Error(`timeout (${ms} ms).`)); + }, ms); + }); + } + + /** + * Encode string for HTML + * @param {string} s + * @returns {string} + */ + static htmlEncode(s) { + return s.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + /** + * Convert hex string and opacity to an rgba string + * @param {string} hex + * @param {number} opacity + * @returns {string} + */ + static hexToRGBA(hex, opacity) { + return 'rgba(' + (hex = hex.replace('#', '')) + .match(new RegExp('(.{' + hex.length / 3 + '})', 'g')) + .map((l) => parseInt(hex.length % 2 ? l + l : l, 16)) + .concat(opacity || 1).join(',') + ')'; + } + + /** + * Add link tag with type css + * @param {string} url attribute + * @param {string} id attribute + */ + static addCss(url, id) { + if (id && document.getElementById(id)) { + return; + } + const tag = document.createElement('link'); + tag.type = 'text/css'; + tag.rel = 'stylesheet'; + tag.href = url; + if (id) { + tag.id = id; + } + document.getElementsByTagName('head')[0].appendChild(tag); + } + + /** + * Remove HTML element + * @param {string} id Element ID + */ + static removeElementById(id) { + const tag = document.getElementById(id); + if (tag && tag.parentNode) { + tag.parentNode.removeChild(tag); + } + } + + /** + * @param {string} html HTML representing a single element + * @return {Node} + */ + static nodeFromHtml(html) { + const template = document.createElement('template'); + template.innerHTML = html; + return template.content.firstChild; + } + + /** + * @param {string} html HTML representing a single element + * @return {NodeList} + */ + static nodesFromHtml(html) { + const template = document.createElement('template'); + template.innerHTML = html; + return template.content.childNodes; + } + + /** + * + * @param {NodeList} nodeList + * @param {string} selector + * @return {?Element} + */ + static querySelectorInList(nodeList, selector) { + for (const node of nodeList) { + if (node instanceof HTMLElement) { + const el = node.querySelector(selector); + if (el) { + return el; + } + } + } + return null; + } + + /** + * @throws On invalid input + * @param {*} input + * @param {boolean=} isNullable + * @return {(null|number)} + */ + static getFloat(input, isNullable = false) { + return uUtils.getParsed(input, isNullable, 'float'); + } + + /** + * @throws On invalid input + * @param {*} input + * @param {boolean=} isNullable + * @return {(null|number)} + */ + static getInteger(input, isNullable = false) { + return uUtils.getParsed(input, isNullable, 'int'); + } + + /** + * @throws On invalid input + * @param {*} input + * @param {boolean=} isNullable + * @return {(null|string)} + */ + static getString(input, isNullable = false) { + return uUtils.getParsed(input, isNullable, 'string'); + } + + /** + * @throws On invalid input + * @param {*} input + * @param {boolean} isNullable + * @param {string} type + * @return {(null|number|string)} + */ + static getParsed(input, isNullable, type) { + if (isNullable && input === null) { + return null; + } + let output; + switch (type) { + case 'float': + output = parseFloat(input); + break; + case 'int': + output = parseInt(input); + break; + case 'string': + output = String(input); + break; + default: + throw new Error('Unknown type'); + } + if (typeof input === 'undefined' || input === null || + (type !== 'string' && isNaN(output))) { + throw new Error('Invalid value'); + } + return output; + } + + /** + * Format date to date, time and time zone strings + * Simplify zone name, eg. + * date: 2017-06-14, time: 11:42:19, zone: GMT+2 CEST + * @param {Date} date + * @return {{date: string, time: string, zone: string}} + */ + static getTimeString(date) { + let timeZone = ''; + const dateStr = `${date.getFullYear()}-${(`0${date.getMonth() + 1}`).slice(-2)}-${(`0${date.getDate()}`).slice(-2)}`; + const timeStr = date.toTimeString().replace(/^\s*([^ ]+)([^(]*)(\([^)]*\))*/, + // eslint-disable-next-line max-params + (_, hours, zone, dst) => { + if (zone) { + timeZone = zone.replace(/(0(?=[1-9]00))|(00\b)/g, ''); + if (dst && (/[A-Z]/).test(dst)) { + timeZone += dst.match(/\b[A-Z]+/g).join(''); + } + } + return hours; + }); + return { date: dateStr, time: timeStr, zone: timeZone }; + } + + /** + * @param {string} url + */ + static openUrl(url) { + window.location.assign(url); + } +} + +// 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; +}; diff --git a/js/test/api_gmaps.test.js b/js/test/api_gmaps.test.js index a833042..09fa8d8 100644 --- a/js/test/api_gmaps.test.js +++ b/js/test/api_gmaps.test.js @@ -21,6 +21,7 @@ import * as gmStub from './googlemaps.stub.js'; import { config, lang } from '../src/initializer.js' import GoogleMapsApi from '../src/mapapi/api_gmaps.js'; import uPosition from '../src/position.js'; +import uPositionSet from '../src/positionset.js'; import uTrack from '../src/track.js'; import uUser from '../src/user.js'; import uUtils from '../src/utils.js'; @@ -230,10 +231,9 @@ describe('Google Maps map API tests', () => { it('should construct non-continuous track markers without polyline', () => { // given - const track = getTrack(); + const track = getPositionSet(); spyOn(api, 'setMarker'); // when - track.continuous = false; api.displayTrack(track, false); // then expect(api.polies.length).toBe(1); @@ -473,15 +473,32 @@ describe('Google Maps map API tests', () => { expect(GoogleMapsApi.loadTimeoutMs).toEqual(jasmine.any(Number)); }); - function getTrack(length = 2) { - const track = new uTrack(1, 'test track', new uUser(1, 'testUser')); + function getSet(length = 2, type) { + let track; + if (type === uTrack) { + track = new uTrack(1, 'test track', new uUser(1, 'testUser')); + } else { + track = new uPositionSet(); + } track.positions = []; + let lat = 21.01; + let lon = 52.23; for (let i = 0; i < length; i++) { - track.positions.push(getPosition()); + track.positions.push(getPosition(lat, lon)); + lat += 0.5; + lon += 0.5; } return track; } + function getTrack(length = 2) { + return getSet(length, uTrack); + } + + function getPositionSet(length = 2) { + return getSet(length, uPositionSet); + } + function getPosition(latitude = 52.23, longitude = 21.01) { const position = new uPosition(); position.latitude = latitude; diff --git a/js/test/api_openlayers.test.js b/js/test/api_openlayers.test.js index 08bab25..2830526 100644 --- a/js/test/api_openlayers.test.js +++ b/js/test/api_openlayers.test.js @@ -22,6 +22,7 @@ import OpenlayersApi from '../src/mapapi/api_openlayers.js'; import { config } from '../src/initializer.js' import uPosition from '../src/position.js'; +import uPositionSet from '../src/positionset.js'; import uTrack from '../src/track.js'; import uUser from '../src/user.js'; import uUtils from '../src/utils.js'; @@ -276,8 +277,7 @@ describe('Openlayers map API tests', () => { api.map.addControl(new ol.control.ZoomToExtent()); api.layerTrack = new ol.layer.VectorLayer({ source: new ol.source.Vector() }); api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() }); - const track = getTrack(); - track.continuous = false; + const track = getPositionSet(); spyOn(api, 'setMarker'); spyOn(api, 'fitToExtent'); // when @@ -549,8 +549,13 @@ describe('Openlayers map API tests', () => { expect(mockViewModel.model.markerSelect).toBe(null); }); - function getTrack(length = 2) { - const track = new uTrack(1, 'test track', new uUser(1, 'testUser')); + function getSet(length = 2, type) { + let track; + if (type === uTrack) { + track = new uTrack(1, 'test track', new uUser(1, 'testUser')); + } else { + track = new uPositionSet(); + } track.positions = []; let lat = 21.01; let lon = 52.23; @@ -562,6 +567,14 @@ describe('Openlayers map API tests', () => { return track; } + function getTrack(length = 2) { + return getSet(length, uTrack); + } + + function getPositionSet(length = 2) { + return getSet(length, uPositionSet); + } + function getPosition(latitude = 52.23, longitude = 21.01) { const position = new uPosition(); position.latitude = latitude; diff --git a/js/test/track.test.js b/js/test/track.test.js new file mode 100644 index 0000000..8d2aea8 --- /dev/null +++ b/js/test/track.test.js @@ -0,0 +1,424 @@ +/* + * μ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 . + */ + +import { auth } from '../src/initializer.js'; +import uPosition from '../src/position.js'; +import uTrack from '../src/track.js'; +import uUser from '../src/user.js'; +import uUtils from '../src/utils.js'; + + +describe('Track tests', () => { + + let track; + + let posId; + let latitude; + let longitude; + let altitude; + let speed; + let bearing; + let timestamp; + let accuracy; + let provider; + let comment; + let image; + let username; + let trackid; + let trackname; + let meters; + let seconds; + + let jsonPosition; + beforeEach(() => { + const id = 1; + const name = 'test'; + const user = new uUser(1, 'user'); + track = new uTrack(id, name, user); + + posId = 110286; + latitude = 11.221871666666999; + longitude = 22.018848333333001; + altitude = -39; + speed = 0; + bearing = null; + timestamp = 1564250017; + accuracy = 9; + provider = 'gps'; + comment = null; + image = '134_5d3c8fa92ebac.jpg'; + username = 'test'; + trackid = 134; + trackname = 'Test name'; + meters = 0; + seconds = 0; + + jsonPosition = { + 'id': posId, + 'latitude': latitude, + 'longitude': longitude, + 'altitude': altitude, + 'speed': speed, + 'bearing': bearing, + 'timestamp': timestamp, + 'accuracy': accuracy, + 'provider': provider, + 'comment': comment, + 'image': image, + 'username': username, + 'trackid': trackid, + 'trackname': trackname, + 'meters': meters, + 'seconds': seconds + }; + }); + + describe('simple tests', () => { + + it('should throw error when creating uTrack instance without user parameter', () => { + // given + const id = 1; + const name = 'test'; + // when + // then + expect(() => new uTrack(id, name)).toThrow(new Error('Invalid argument for track constructor')); + }); + + it('should create uTrack instance with user parameter', () => { + // given + const id = 1; + const name = 'test'; + const user = new uUser(1, 'user'); + // when + track = new uTrack(id, name, user); + // then + expect(track.id).toBe(id); + expect(track.name).toBe(name); + expect(track.user).toBe(user); + expect(track.positions).toEqual([]); + expect(track.plotData).toEqual([]); + expect(track.maxId).toBe(0); + expect(track.listValue).toBe(id.toString()); + expect(track.listText).toBe(name); + }); + + it('should clear positions data', () => { + // given + track.positions.push(new uPosition()); + track.plotData.push({ x: 1, y: 2 }); + track.maxId = 1; + // when + track.clear(); + // then + expect(track.positions).toEqual([]); + expect(track.plotData).toEqual([]); + expect(track.maxId).toBe(0); + }); + + it('should return positions length', () => { + // given + track.positions.push(new uPosition()); + // when + const length = track.length; + // then + expect(length).toBe(1); + }); + + it('should return true when has positions', () => { + // given + track.positions.push(new uPosition()); + // when + const result = track.hasPositions; + // then + expect(result).toBe(true); + }); + + it('should return false when does not have positions', () => { + // given + track.positions.length = 0; + // when + const result = track.hasPositions; + // then + expect(result).toBe(false); + }); + + it('should return true when has plot data', () => { + // given + track.plotData.push({ x: 1, y: 2 }); + // when + const result = track.hasPlotData; + // then + expect(result).toBe(true); + }); + + it('should return false when does not have plot data', () => { + // given + track.plotData.length = 0; + // when + const result = track.hasPlotData; + // then + expect(result).toBe(false); + }); + + it('should be equal to other track with same id', () => { + // given + track.id = 1; + const otherTrack = new uTrack(1, 'other', new uUser(2, 'user2')); + // when + const result = track.isEqualTo(otherTrack); + // then + expect(result).toBe(true); + }); + + it('should not be equal to other track with other id', () => { + // given + track.id = 1; + const otherTrack = new uTrack(2, 'other', new uUser(2, 'user2')); + // when + const result = track.isEqualTo(otherTrack); + // then + expect(result).toBe(false); + }); + + it('should not be equal to null track', () => { + // given + track.id = 1; + const otherTrack = null; + // when + const result = track.isEqualTo(otherTrack); + // then + expect(result).toBe(false); + }); + + it('should parse json object to track positions', () => { + // when + track.fromJson([ jsonPosition ]); + // then + expect(track.length).toBe(1); + expect(track.plotData.length).toBe(1); + expect(track.maxId).toBe(posId); + const position = track.positions[0]; + + expect(position.id).toBe(posId); + expect(position.latitude).toBe(latitude); + expect(position.longitude).toBe(longitude); + expect(position.speed).toBe(speed); + expect(position.bearing).toBe(bearing); + expect(position.timestamp).toBe(timestamp); + expect(position.accuracy).toBe(accuracy); + expect(position.provider).toBe(provider); + expect(position.comment).toBe(comment); + expect(position.image).toBe(image); + expect(position.username).toBe(username); + expect(position.trackid).toBe(trackid); + expect(position.trackname).toBe(trackname); + expect(position.meters).toBe(meters); + expect(position.seconds).toBe(seconds); + }); + + it('should replace track positions with new ones', () => { + const position1 = { ...jsonPosition }; + position1.id = 100; + track.fromJson([ position1 ]); + // when + track.fromJson([ jsonPosition ]); + // then + expect(track.length).toBe(1); + expect(track.plotData.length).toBe(1); + expect(track.maxId).toBe(posId); + const position2 = track.positions[0]; + + expect(position2.id).toBe(posId); + }); + + it('should append track positions with new ones', () => { + const position1 = { ...jsonPosition }; + position1.id = 100; + track.fromJson([ position1 ]); + // when + track.fromJson([ jsonPosition ], true); + // then + expect(track.length).toBe(2); + expect(track.plotData.length).toBe(2); + expect(track.maxId).toBe(Math.max(jsonPosition.id, position1.id)); + expect(track.positions[0].id).toBe(position1.id); + expect(track.positions[1].id).toBe(jsonPosition.id); + expect(track.positions[0].totalMeters).toBe(position1.meters); + expect(track.positions[1].totalMeters).toBe(position1.meters + jsonPosition.meters); + expect(track.positions[0].totalSeconds).toBe(position1.seconds); + expect(track.positions[1].totalSeconds).toBe(position1.seconds + jsonPosition.seconds); + }); + }); + + describe('ajax tests', () => { + const validListResponse = [ { 'id': 145, 'name': 'Track 1' }, { 'id': 144, 'name': 'Track 2' } ]; + const invalidListResponse = [ { 'name': 'Track 1' }, { 'id': 144, 'name': 'Track 2' } ]; + + beforeEach(() => { + spyOn(XMLHttpRequest.prototype, 'open').and.callThrough(); + spyOn(XMLHttpRequest.prototype, 'setRequestHeader').and.callThrough(); + spyOn(XMLHttpRequest.prototype, 'send'); + spyOnProperty(XMLHttpRequest.prototype, 'readyState').and.returnValue(XMLHttpRequest.DONE); + spyOnProperty(XMLHttpRequest.prototype, 'status').and.returnValue(200); + }); + + it('should make successful request and return track array', (done) => { + // given + const user = new uUser(1, 'testLogin'); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify(validListResponse)); + // when + uTrack.fetchList(user) + .then((result) => { + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', 'utils/gettracks.php?userid=1', true); + expect(result).toEqual(jasmine.arrayContaining([ new uTrack(validListResponse[0].id, validListResponse[0].name, user) ])); + expect(result.length).toBe(2); + done(); + }) + .catch((e) => done.fail(`reject callback called (${e})`)); + }); + + it('should throw error on invalid JSON', (done) => { + // given + const user = new uUser(1, 'testLogin'); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify(invalidListResponse)); + // when + uTrack.fetchList(user) + .then(() => { + done.fail('resolve callback called'); + }) + .catch((e) => { + expect(e).toEqual(jasmine.any(Error)); + done(); + }); + }); + + it('should make successful request and return latest track position for given user', (done) => { + // given + const user = new uUser(1, 'testLogin'); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify([ jsonPosition ])); + // when + uTrack.fetchLatest(user) + .then((result) => { + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', 'utils/getpositions.php?last=true&userid=1', true); + expect(result).toBeInstanceOf(uTrack); + expect(result.id).toEqual(jsonPosition.trackid); + expect(result.length).toBe(1); + done(); + }) + .catch((e) => done.fail(`reject callback called (${e})`)); + }); + + it('should make successful request and return null when there are no positions for the user', (done) => { + // given + const user = new uUser(1, 'testLogin'); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify([])); + // when + uTrack.fetchLatest(user) + .then((result) => { + expect(result).toBe(null); + done(); + }) + .catch((e) => done.fail(`reject callback called (${e})`)); + }); + + it('should make successful request and fetch track positions', (done) => { + // given + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify([ jsonPosition ])); + track.clear(); + // when + track.fetchPositions() + .then(() => { + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', `utils/getpositions.php?userid=${track.user.id}&trackid=${track.id}`, true); + expect(track.length).toBe(1); + expect(track.positions[0].id).toEqual(jsonPosition.id); + done(); + }) + .catch((e) => done.fail(`reject callback called (${e})`)); + }); + + it('should make successful request and append track positions to existing data', (done) => { + // given + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify([ jsonPosition ])); + track.clear(); + // when + track.fetchPositions() + .then(() => { + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', `utils/getpositions.php?userid=${track.user.id}&trackid=${track.id}`, true); + expect(track.length).toBe(1); + expect(track.positions[0].id).toEqual(jsonPosition.id); + // eslint-disable-next-line jasmine/no-promise-without-done-fail + track.fetchPositions().then(() => { + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', `utils/getpositions.php?userid=${track.user.id}&trackid=${track.id}&afterid=${track.positions[0].id}`, true); + expect(track.length).toBe(2); + expect(track.positions[0].id).toEqual(jsonPosition.id); + done(); + }); + }) + .catch((e) => done.fail(`reject callback called (${e})`)); + }); + + it('should make successful track import request', (done) => { + // given + const authUser = new uUser(1, 'admin'); + spyOnProperty(auth, 'isAuthenticated').and.returnValue(true); + spyOnProperty(auth, 'user').and.returnValue(authUser); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify(validListResponse)); + const form = document.createElement('form'); + // when + uTrack.import(form) + .then((tracks) => { + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('POST', 'utils/import.php', true); + expect(XMLHttpRequest.prototype.send).toHaveBeenCalledWith(new FormData(form)); + expect(tracks.length).toBe(2); + done(); + }) + .catch((e) => done.fail(`reject callback called (${e})`)); + }); + + it('should fail on import request without authorized user', () => { + // given + const form = document.createElement('form'); + // when + expect(() => uTrack.import(form)).toThrowError(/auth/); + }); + + it('should not open export url when track has no positions', () => { + // given + spyOn(uUtils, 'openUrl'); + const type = 'ext'; + // when + track.export(type); + // then + expect(uUtils.openUrl).not.toHaveBeenCalled(); + }); + + it('should open export url', () => { + // given + track.positions.push(new uPosition()); + spyOn(uUtils, 'openUrl'); + const type = 'ext'; + // when + track.export(type); + // then + expect(uUtils.openUrl).toHaveBeenCalledWith(`utils/export.php?type=${type}&userid=${track.user.id}&trackid=${track.id}`); + }); + + }); + +}); diff --git a/utils/getpositions.php b/utils/getpositions.php index 5f827ad..b418f54 100644 --- a/utils/getpositions.php +++ b/utils/getpositions.php @@ -26,7 +26,7 @@ $auth = new uAuth(); $userId = uUtils::getInt('userid'); $trackId = uUtils::getInt('trackid'); $afterId = uUtils::getInt('afterid'); -$last = uUtils::getInt('last'); +$last = uUtils::getBool('last'); $positionsArr = []; if ($userId) { @@ -54,7 +54,7 @@ if ($positionsArr === false) { $result = [ "error" => true ]; } else if (!empty($positionsArr)) { foreach ($positionsArr as $position) { - $distance = !$last && isset($prevPosition) ? $position->distanceTo($prevPosition) : 0; + $meters = !$last && isset($prevPosition) ? $position->distanceTo($prevPosition) : 0; $seconds = !$last && isset($prevPosition) ? $position->secondsTo($prevPosition) : 0; $result[] = [ "id" => $position->id, @@ -71,7 +71,7 @@ if ($positionsArr === false) { "username" => $position->userLogin, "trackid" => $position->trackId, "trackname" => $position->trackName, - "distance" => round($distance), + "meters" => round($meters), "seconds" => $seconds ]; $prevPosition = $position; diff --git a/utils/gettracks.php b/utils/gettracks.php index de3296a..fa91600 100644 --- a/utils/gettracks.php +++ b/utils/gettracks.php @@ -32,24 +32,14 @@ if ($userId) { } } -header("Content-type: text/xml"); -$xml = new XMLWriter(); -$xml->openURI("php://output"); -$xml->startDocument("1.0"); -$xml->setIndent(true); -$xml->startElement('root'); - -if (!empty($tracksArr)) { - foreach ($tracksArr as $aTrack) { - $xml->startElement("track"); - $xml->writeElement("trackid", $aTrack->id); - $xml->writeElement("trackname", $aTrack->name); - $xml->endElement(); +$result = []; +if ($tracksArr === false) { + $result = [ "error" => true ]; +} else if (!empty($tracksArr)) { + foreach ($tracksArr as $track) { + $result[] = [ "id" => $track->id, "name" => $track->name ]; } } - -$xml->endElement(); -$xml->endDocument(); -$xml->flush(); - +header("Content-type: application/json"); +echo json_encode($result); ?> diff --git a/utils/import.php b/utils/import.php index 17d87a4..9fb1cce 100644 --- a/utils/import.php +++ b/utils/import.php @@ -71,14 +71,14 @@ if ($gpx === false) { } uUtils::exitWithError($message); } -else if ($gpx->getName() != "gpx") { +else if ($gpx->getName() !== "gpx") { uUtils::exitWithError($lang["iparsefailure"]); } else if (empty($gpx->trk)) { uUtils::exitWithError($lang["idatafailure"]); } -$trackCnt = 0; +$trackList = []; foreach ($gpx->trk as $trk) { $trackName = empty($trk->name) ? $gpxName : (string) $trk->name; $metaName = empty($gpx->metadata->name) ? NULL : (string) $gpx->metadata->name; @@ -92,7 +92,7 @@ foreach ($gpx->trk as $trk) { foreach($trk->trkseg as $segment) { foreach($segment->trkpt as $point) { - if (!isset($point["lat"]) || !isset($point["lon"])) { + if (!isset($point["lat"], $point["lon"])) { $track->delete(); uUtils::exitWithError($lang["iparsefailure"]); } @@ -105,7 +105,7 @@ foreach ($gpx->trk as $trk) { $provider = "gps"; if (!empty($point->extensions)) { // parse ulogger extensions - $ext = $point->extensions->children('ulogger', TRUE); + $ext = $point->extensions->children('ulogger', true); if (count($ext->speed)) { $speed = (double) $ext->speed; } if (count($ext->bearing)) { $bearing = (double) $ext->bearing; } if (count($ext->accuracy)) { $accuracy = (int) $ext->accuracy; } @@ -122,13 +122,12 @@ foreach ($gpx->trk as $trk) { } } if ($posCnt) { - $trackCnt++; + array_unshift($trackList, [ "id" => $track->id, "name" => $track->name ]); } else { $track->delete(); } } -// return last track id and tracks count -uUtils::exitWithSuccess([ "trackid" => $trackId, "trackcnt" => $trackCnt ]); - +header("Content-type: application/json"); +echo json_encode($trackList); ?>