commit d63628b4f72c692d3db0dd9c8b7f9011c18d00ef
parent 6a553f8168fe664c9a0076afc42cd320fc55b314
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Thu, 6 Dec 2018 22:37:02 +0100
[tracks] load tracks from urls in url hash #131
new hash params added:
* nktu -- urlencoded urls
* nktp -- point
* ntkj -- json strictures with links, track data, point data and view params
Diffstat:
10 files changed, 312 insertions(+), 147 deletions(-)
diff --git a/src/App.js b/src/App.js
@@ -135,14 +135,25 @@ function setUp() {
return tracklist.tracks().map((track) => track.name());
}
tracklist.addTo(map);
- if (!hashState.getState('nktk') && !hashState.getState('nktl')) {
+ const tracksHashParams = tracklist.hashParams();
+
+ let hasTrackParamsInHash = false;
+ for (let param of tracksHashParams) {
+ if (hashState.hasKey(param)) {
+ hasTrackParamsInHash = true;
+ break;
+ }
+ }
+ if (!hasTrackParamsInHash) {
tracklist.loadTracksFromStorage();
}
startInfo.tracksAfterLoadFromStorage = trackNames();
- bindHashStateReadOnly('nktk', tracklist.loadNktkFromHash.bind(tracklist));
- startInfo.tracksAfterLoadFromNktk = trackNames();
- bindHashStateReadOnly('nktl', tracklist.loadNktlFromHash.bind(tracklist));
- startInfo.tracksAfterLoadFromNktl = trackNames();
+
+ for (let param of tracksHashParams ) {
+ bindHashStateReadOnly(param, tracklist.loadTrackFromParam.bind(tracklist, param));
+ }
+ startInfo.tracksAfterLoadFromHash = trackNames();
+
if (!validPositionInHash) {
tracklist.whenLoadDone(() => tracklist.setViewToAllTracks(true));
}
diff --git a/src/lib/leaflet.control.track-list/lib/parsers/nktk.js b/src/lib/leaflet.control.track-list/lib/parsers/nktk.js
@@ -311,29 +311,12 @@ function parseNktkFragment(s) {
}
function parseNktkSequence(s) {
- if (typeof s === "string") {
- s = s.split('/');
- }
- var geodataArray = [];
- for (let i = 0; i < s.length; i++) {
- if (s[i]) {
- geodataArray.push.apply(geodataArray, parseNktkFragment(s[i]));
- }
- }
- return geodataArray;
-}
-
-function parseNktkParam(s) {
- let i = s.indexOf('#');
- if (i === -1) {
- return null;
- }
- i = s.indexOf('nktk=', i + 1);
- if (i === -1) {
- return null;
- }
- s = s.substring(i + 5);
- return parseNktkSequence(s)
+ return s.split('/')
+ .map(parseNktkFragment)
+ .reduce((acc, cur) => {
+ acc.push(...cur);
+ return acc;
+ });
}
-export {saveNktk, parseNktkParam, parseTrackUrlData, parseNktkSequence};
-\ No newline at end of file
+export {saveNktk, parseTrackUrlData, parseNktkSequence, parseNktkFragment};
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.track-list/lib/services/index.js b/src/lib/leaflet.control.track-list/lib/services/index.js
@@ -5,14 +5,13 @@ import Gpslib from './gpslib';
import Osm from './osm';
import Strava from './strava';
import {YandexRuler} from './yandex';
-import {NakarteTrack, NakarteNktk, NakarteNktl} from './nakarte';
+import {NakarteTrack, NakarteUrl} from './nakarte';
import {MovescountMove, MovescountRoute} from './movescount';
export default [
YandexRuler,
NakarteTrack,
- NakarteNktk,
- NakarteNktl,
+ NakarteUrl,
Endomondo,
Gpsies,
Gpslib,
diff --git a/src/lib/leaflet.control.track-list/lib/services/nakarte.js b/src/lib/leaflet.control.track-list/lib/services/nakarte.js
@@ -1,62 +0,0 @@
-import BaseService from './baseService';
-import {parseNktkParam, parseNktkSequence, parseTrackUrlData} from '../parsers/nktk';
-import config from 'config';
-
-class NakarteTrack extends BaseService {
- isOurUrl() {
- return this.origUrl.indexOf('track://') > -1;
- }
-
- requestOptions() {
- return [];
- }
-
- parseResponse() {
- const i = this.origUrl.indexOf('track://');
- return parseTrackUrlData(this.origUrl.substring(i + 8));
- }
-}
-
-class NakarteNktk extends BaseService {
- constructor(url) {
- super(null);
- this._geoData = parseNktkParam(url);
- }
-
- isOurUrl() {
- return !!this._geoData;
- }
-
- requestOptions() {
- return [];
- }
-
- parseResponse() {
- return this._geoData;
- }
-
-}
-
-class NakarteNktl extends BaseService {
- urlRe = /#.*nktl=([A-Za-z0-9_-]+)/;
-
- isOurUrl() {
- return this.urlRe.test(this.origUrl);
- }
-
- requestOptions() {
- const m = this.urlRe.exec(this.origUrl);
- const trackId = m[1];
- return [{
- url: (`${config.tracksStorageServer}/track/${trackId}`),
- options: {responseType: 'binarystring'}
- }];
- }
-
- parseResponse(responses) {
- return parseNktkSequence(responses[0].responseBinaryText);
- }
-}
-
-
-export {NakarteTrack, NakarteNktk, NakarteNktl};
diff --git a/src/lib/leaflet.control.track-list/lib/services/nakarte/index.js b/src/lib/leaflet.control.track-list/lib/services/nakarte/index.js
@@ -0,0 +1,120 @@
+import BaseService from '../baseService';
+import {parseNktkSequence, parseTrackUrlData, parseNktkFragment} from '../../parsers/nktk';
+import config from 'config';
+import {parseHashParams} from 'lib/leaflet.hashState/hashState';
+import loadFromUrl from '../../loadFromUrl';
+import loadTracksFromJson from './loadTracksFromJson';
+import {fetch} from 'lib/xhr-promise';
+
+function flattenArray(ar) {
+ return ar.reduce((cur, acc) => {
+ acc.push(...cur);
+ return acc;
+ }, []);
+
+}
+
+class NakarteTrack extends BaseService {
+ isOurUrl() {
+ return this.origUrl.indexOf('track://') > -1;
+ }
+
+ requestOptions() {
+ return [];
+ }
+
+ parseResponse() {
+ const i = this.origUrl.indexOf('track://');
+ return parseTrackUrlData(this.origUrl.substring(i + 8));
+ }
+}
+
+class NakarteUrlLoader {
+ constructor() {
+ this.loaders = {
+ nktk: this.loadFromTextEncodedTrack,
+ nktl: this.loadFromTextEncodedTrackId,
+ nktu: this.loadFromUrlencodedUrls,
+ nktp: this.loadPoint,
+ nktj: this.loadFromJSON
+ };
+ }
+
+ paramNames() {
+ return Object.keys(this.loaders);
+ }
+
+ async geoData(paramName, values) {
+ const loader = this.loaders[paramName];
+ return loader.call(this, values);
+ }
+
+ async loadFromTextEncodedTrack(values) {
+ return flattenArray(values.map(parseNktkFragment));
+ }
+
+ async loadFromTextEncodedTrackId(values) {
+ const requests = values.map((trackId) =>
+ fetch(
+ `${config.tracksStorageServer}/track/${trackId}`,
+ {responseType: 'binarystring'}
+ )
+ );
+ let responses;
+ try {
+ responses = await Promise.all(requests);
+ } catch (e) {
+ return [{name: 'Track from nakarte server', error: 'NETWORK'}];
+ }
+ return flattenArray(responses.map((r) => parseNktkSequence(r.responseBinaryText)));
+ }
+
+ async loadFromJSON(values) {
+ return loadTracksFromJson(values);
+ }
+
+ async loadFromUrlencodedUrls(values) {
+ return flattenArray(await Promise.all(values.map(loadFromUrl)));
+ }
+
+ async loadPoint(values) {
+ return parsePointFromHashValues(values)
+ }
+}
+
+class NakarteUrl {
+ constructor(url) {
+ const paramNames = new NakarteUrlLoader().paramNames();
+ this._params = Object.entries(parseHashParams(url))
+ .filter(([name, ]) => paramNames.includes(name));
+ }
+
+ isOurUrl(url) {
+ return this._params.length > 0;
+ }
+
+ async geoData() {
+ const promises = this._params.map(([paramName, value]) => {
+ return new NakarteUrlLoader().geoData(paramName, value);
+ });
+ return flattenArray(await Promise.all(promises));
+ }
+}
+
+
+function parsePointFromHashValues(values) {
+ if (values.length < 2) {
+ return [{name: 'Point in url', error: 'CORRUPT'}]
+ }
+ const lat = parseFloat(values[0]);
+ const lng = parseFloat(values[1]);
+ if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
+ return [{name: 'Point in url', error: 'CORRUPT'}]
+ }
+ const name = ((values[2] || '').trim()) || 'Point';
+ return [{name, points: [{name, lat, lng}]}];
+
+}
+
+
+export {NakarteTrack, NakarteUrl, NakarteUrlLoader};
diff --git a/src/lib/leaflet.control.track-list/lib/services/nakarte/loadTracksFromJson.js b/src/lib/leaflet.control.track-list/lib/services/nakarte/loadTracksFromJson.js
@@ -0,0 +1,116 @@
+import urlSafeBase64 from '../../parsers/urlSafeBase64';
+import {TRACKLIST_TRACK_COLORS} from '../../../track-list';
+import loadFromUrl from '../../loadFromUrl';
+
+function parseWaypoint(rawPoint) {
+ let name = rawPoint.n;
+ let lat = Number(rawPoint.lt);
+ let lng = Number(rawPoint.ln);
+ if (typeof name !== 'string' || !name || isNaN(lat) || isNaN(lng) ||
+ lat < -90 || lat > 90 || lng < -180 || lng > 180) {
+ return {valid: false}
+ }
+ return {
+ valid: true,
+ point: {lat, lng, name}
+ }
+}
+
+function parseTrack(rawTrack) {
+ if (!rawTrack.length) {
+ return {valid: false};
+ }
+ const track = [];
+ for (let rawSegment of rawTrack) {
+ let segment = [];
+ if (!rawSegment || !rawSegment.length) {
+ return {valid: false};
+ }
+ for (let rawPoint of rawSegment) {
+ if (!rawPoint || rawPoint.length !== 2) {
+ return {valid: false};
+ }
+ let [lat, lng] = rawPoint.map(Number);
+ if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90) {
+ return {valid: false};
+ }
+ segment.push({lat, lng});
+ }
+ track.push(segment);
+ }
+ return {valid: true, track};
+
+}
+
+async function loadTracksFromJson(values) {
+ const errCorrupt = [{name: 'Track in url', error: 'CORRUPT'}];
+ if (values.length < 1) {
+ return errCorrupt;
+ }
+ const jsonString = urlSafeBase64.decode(values[0]);
+ let data;
+ try {
+ data = JSON.parse(jsonString)
+ } catch (e) {
+ return errCorrupt;
+ }
+ if (!data || !data.length) {
+ return errCorrupt;
+ }
+ const geoDataArray = [];
+
+ for (let el of data) {
+ // Each track should contain either url or at least ono of tracks and points
+ if (!el.u && !(el.p || el.t)) {
+ return errCorrupt;
+ }
+ let geodata;
+ if (el.u) {
+ geodata = await
+ loadFromUrl(el.u);
+ if (el.n && geodata.length === 1 && !geodata[0].error) {
+ geodata[0].name = el.n;
+ }
+ } else {
+ geodata = {};
+ geodata.name = el.name || 'Track';
+ if (el.t) {
+ const res = parseTrack(el.t);
+ if (!res.valid) {
+ return errCorrupt;
+ }
+ geodata.tracks = res.track;
+ }
+ if (el.p) {
+ geodata.points = [];
+ for (let rawPoint of el.p) {
+ let res = parseWaypoint(rawPoint);
+ if (!res.valid) {
+ return errCorrupt;
+ }
+ geodata.points.push(res.point);
+ }
+ }
+ geodata = [geodata];
+ }
+ let viewProps = {};
+ if ('c' in el) {
+ let color = Number(el.c);
+ if (color < 0 || color >= TRACKLIST_TRACK_COLORS.length) {
+ return errCorrupt;
+ }
+ viewProps.color = color;
+ }
+ if ('v' in el) {
+ viewProps.trackHidden = !el.v;
+ }
+ if ('m' in el) {
+ viewProps.measureTicksShown = !!el.m;
+ }
+ geodata.forEach((el) => Object.assign(el, viewProps));
+ geoDataArray.push(...geodata)
+ }
+ return geoDataArray;
+}
+
+export default loadTracksFromJson;
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.track-list/track-list.hash-state.js b/src/lib/leaflet.control.track-list/track-list.hash-state.js
@@ -1,38 +1,24 @@
import L from 'leaflet';
-import loadFromUrl from './lib/loadFromUrl';
-import logging from 'lib/logging';
-import {parseNktkSequence} from './lib/parsers/nktk';
+import {NakarteUrlLoader} from './lib/services/nakarte'
L.Control.TrackList.include({
- loadNktkFromHash: function(values) {
- if (!values || !(values.length)) {
+ hashParams: function() {
+ return new NakarteUrlLoader().paramNames();
+ },
+
+ loadTrackFromParam: async function(paramName, values) {
+ if (!values || !values.length) {
return false;
}
- logging.captureBreadcrumb({message: 'load nktk from hashState'});
- const geodata = parseNktkSequence(values);
- const notEmpty = this.addTracksFromGeodataArray(geodata, {href: window.location.href});
+ this.readingFiles(this.readingFiles() + 1);
+ const geodata = await new NakarteUrlLoader().geoData(paramName, values);
+ const notEmpty = this.addTracksFromGeodataArray(geodata, {paramName, values});
+ this.readingFiles(this.readingFiles() - 1);
if (notEmpty) {
this.setExpanded();
}
},
-
- loadNktlFromHash: function(values) {
- if (!values || !(values.length)) {
- return false;
- }
- logging.captureBreadcrumb({message: 'load nktl from hashState'});
- const url = `#nktl=${values[0]}`;
- const href = window.location.href;
- loadFromUrl(url).then(
- (geodata) => {
- const notEmpty = this.addTracksFromGeodataArray(geodata, {href});
- if (notEmpty) {
- this.setExpanded();
- }
- }
- );
- }
}
);
diff --git a/src/lib/leaflet.control.track-list/track-list.js b/src/lib/leaflet.control.track-list/track-list.js
@@ -25,6 +25,9 @@ import config from 'config';
import md5 from './lib/md5';
import {wrapLatLngToTarget, wrapLatLngBoundsToTarget} from 'lib/leaflet.fixes/fixWorldCopyJump';
+const TRACKLIST_TRACK_COLORS = ['#77f', '#f95', '#0ff', '#f77', '#f7f', '#ee5'];
+
+
const TrackSegment = L.MeasuredLine.extend({
includes: L.Polyline.EditMixin,
@@ -43,7 +46,7 @@ L.Control.TrackList = L.Control.extend({
options: {position: 'bottomright'},
includes: L.Mixin.Events,
- colors: ['#77f', '#f95', '#0ff', '#f77', '#f7f', '#ee5'],
+ colors: TRACKLIST_TRACK_COLORS,
initialize: function() {
@@ -1181,3 +1184,5 @@ L.Control.TrackList = L.Control.extend({
}
}
);
+
+export {TRACKLIST_TRACK_COLORS};
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.track-list/track-list.localstorage.js b/src/lib/leaflet.control.track-list/track-list.localstorage.js
@@ -1,6 +1,6 @@
import './track-list'
import L from 'leaflet';
-import {parseNktkParam} from './lib/parsers/nktk';
+import {parseNktkSequence} from './lib/parsers/nktk';
import safeLocalStorage from 'lib/safe-localstorage';
import logging from 'lib/logging';
@@ -80,7 +80,8 @@ L.Control.TrackList.include({
safeLocalStorage.removeItem(key);
if (s) {
logging.captureBreadcrumb({message: 'load track from localStorage'});
- geodata = parseNktkParam(s);
+ s = s.slice(6); // remove "#nktk=" prefix
+ geodata = parseNktkSequence(s);
this.addTracksFromGeodataArray(geodata, {localStorage: {key, value: s}});
}
}
diff --git a/src/lib/leaflet.hashState/hashState.js b/src/lib/leaflet.hashState/hashState.js
@@ -9,6 +9,30 @@ function arrayItemsEqual(l1, l2) {
return true;
}
+
+function parseHashParams(s) {
+ const args = {},
+ i = s.indexOf('#');
+ if (i >= 0) {
+ s = s.substr(i + 1).trim();
+ let m, key, value;
+ for (let pair of s.split('&')) {
+ m = /^([^=]+?)(?:=(.*))?$/.exec(pair);
+ if (m) {
+ [, key, value] = m;
+ if (value) {
+ value = value.split('/');
+ value = value.map(decodeURIComponent);
+ } else {
+ value = [];
+ }
+ args[key] = value;
+ }
+ }
+ }
+ return args;
+}
+
const hashState = {
_listeners: [],
_state: {},
@@ -45,28 +69,8 @@ const hashState = {
return this._state[key];
},
- _parseHash: function() {
- let hash = location.hash;
- const args = {},
- i = hash.indexOf('#');
- if (i >= 0) {
- hash = hash.substr(i + 1).trim();
- let m, key, value;
- for (let pair of hash.split('&')) {
- m = /^([^=]+?)(?:=(.*))?$/.exec(pair);
- if (m) {
- [, key, value] = m;
- if (value) {
- value = value.split('/');
- value = value.map(decodeURIComponent);
- } else {
- value = [];
- }
- args[key] = value;
- }
- }
- }
- return args;
+ hasKey: function(key) {
+ return this._state.hasOwnProperty(key);
},
_saveStateToHash: function() {
@@ -93,7 +97,7 @@ const hashState = {
if (this._ignoreChanges) {
return;
}
- const newState = this._parseHash();
+ const newState = parseHashParams(location.hash);
const changedKeys = {};
for (let key of Object.keys(newState)) {
if (!(key in this._state) || !arrayItemsEqual(newState[key], this._state[key])) {
@@ -130,5 +134,5 @@ window.addEventListener('hashchange', hashState.onHashChanged.bind(hashState));
hashState.onHashChanged();
-export {hashState, bindHashStateReadOnly};
+export {hashState, bindHashStateReadOnly, parseHashParams};