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);
?>