nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

commit 275a5c4cf4c18951e33c1c101f20d79e9258ac85
parent 56f2918745a806e287567a7a83e0418af698158a
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Mon, 10 Dec 2018 21:20:55 +0100

Merge branch 'release-8'

Diffstat:
Msrc/App.js | 33++++++++++++++++++++++-----------
Msrc/config.js | 8+++++---
Msrc/layers.js | 3+--
Dsrc/lib/leaflet.control.track-list/lib/endomondo.js | 66------------------------------------------------------------------
Msrc/lib/leaflet.control.track-list/lib/geo_file_exporters.js | 2+-
Dsrc/lib/leaflet.control.track-list/lib/geo_file_formats.js | 684-------------------------------------------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/gpsies.js | 50--------------------------------------------------
Asrc/lib/leaflet.control.track-list/lib/loadFromUrl.js | 12++++++++++++
Dsrc/lib/leaflet.control.track-list/lib/nktk.js | 395-------------------------------------------------------------------------------
Asrc/lib/leaflet.control.track-list/lib/parseGeoFile.js | 13+++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/codePages.js | 36++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/gpx.js | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/index.js | 14++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/jsInflate.js | 13+++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/kml.js | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/nktk.js | 323+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/lib/leaflet.control.track-list/lib/nktk.proto -> src/lib/leaflet.control.track-list/lib/parsers/nktk.proto | 0
Rsrc/lib/leaflet.control.track-list/lib/nktk_pb.js -> src/lib/leaflet.control.track-list/lib/parsers/nktk_pb.js | 0
Asrc/lib/leaflet.control.track-list/lib/parsers/ozi.js | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/urlSafeBase64.js | 25+++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/xmlUtils.js | 13+++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/zip.js | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/baseService.js | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/endomondo.js | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/gpsies.js | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/gpslib.js | 28++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/index.js | 24++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/movescount.js | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/nakarte/index.js | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/nakarte/loadTracksFromJson.js | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/osm.js | 32++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/simpleService.js | 23+++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/strava.js | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/urlEncode.js | 6++++++
Asrc/lib/leaflet.control.track-list/lib/services/yandex.js | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/lib/leaflet.control.track-list/lib/strava.js | 82-------------------------------------------------------------------------------
Msrc/lib/leaflet.control.track-list/track-list.css | 9+++++++++
Msrc/lib/leaflet.control.track-list/track-list.hash-state.js | 36+++++++++++-------------------------
Msrc/lib/leaflet.control.track-list/track-list.js | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/lib/leaflet.control.track-list/track-list.localstorage.js | 5+++--
Msrc/lib/leaflet.hashState/hashState.js | 52++++++++++++++++++++++++++++------------------------
Msrc/lib/leaflet.layer.geocaching-su/index.js | 7++++---
Msrc/lib/notifications/style.css | 7+++++--
43 files changed, 1862 insertions(+), 1376 deletions(-)

diff --git a/src/App.js b/src/App.js @@ -60,10 +60,7 @@ function setUp() { /////////// controls top-left corner - new L.Control.Caption(` - <a href="https://nakarte-me.blogspot.com/p/blog-page.html">Documentation</a> | - <a href="${config.newsUrl}">News</a> | - <a href="mailto:${config.email}" target="_self">nakarte@nakarte.me</a> `, { + new L.Control.Caption(config.caption, { position: 'topleft' } ).addTo(map); @@ -102,9 +99,9 @@ function setUp() { const defaultLocation = L.latLng(55.75185, 37.61856); const defaultZoom = 10; - let {lat, lng, zoom, valid} = map.validateState(hashState.getState('m')); + let {lat, lng, zoom, valid: validPositionInHash} = map.validateState(hashState.getState('m')); locateControl.moveMapToCurrentLocation(defaultZoom, defaultLocation, - valid ? L.latLng(lat, lng) : null, valid ? zoom : null); + validPositionInHash ? L.latLng(lat, lng) : null, validPositionInHash ? zoom : null); map.enableHashState('m'); /////////// controls top-right corner @@ -135,14 +132,28 @@ 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)); + } ////////// adaptive layout diff --git a/src/config.js b/src/config.js @@ -1,13 +1,15 @@ import secrets from './secrets'; export default Object.assign({ - email: 'nakarte@nakarte.me', + caption: ` + <a href="https://docs.nakarte.me">Documentation</a> | + <a href="https://about.nakarte.me">News</a> | + <a href="mailto:nakarte@nakarte.me" target="_self">nakarte@nakarte.me</a> `, googleApiUrl: `https://maps.googleapis.com/maps/api/js?v=3&key=${secrets.google}`, westraDataBaseUrl: 'https://nakarte.me/westraPasses/', CORSProxyUrl: 'https://proxy.nakarte.me/', elevationsServer: 'https://elevation.nakarte.me/', - newsUrl: 'https://nakarte-me.blogspot.com', wikimediaCommonsCoverageUrl: 'https://tiles.nakarte.me/wikimedia_commons_images/{z}/{x}/{y}', - geocachingSuUrl: 'https://nakarte.me/geocachingSu/geocaching_su.json', + geocachingSuUrl: 'https://nakarte.me/geocachingSu/geocaching_su2.json', tracksStorageServer: 'https://tracks.nakarte.me', }, secrets); diff --git a/src/layers.js b/src/layers.js @@ -542,8 +542,7 @@ export default function getLayers() { isOverlay: true, isOverlayTransparent: false, tms: false, - minZoom: 7, - minNativeZoom: 7, + minZoom: 2, maxNativeZoom: 15, print: true, jnx: true, diff --git a/src/lib/leaflet.control.track-list/lib/endomondo.js b/src/lib/leaflet.control.track-list/lib/endomondo.js @@ -1,66 +0,0 @@ -import urlViaCorsProxy from 'lib/CORSProxy'; - -const regexps = [ - /^https:\/\/www\.endomondo\.com\/users\/(\d+)\/workouts\/(\d+)/, - /^https:\/\/www\.endomondo\.com\/workouts\/(\d+)\/(\d+)/ -]; - -function isEndomondoUrl(url) { - return regexps[0].test(url) || regexps[1].test(url); -} - -function endomondoRequestOptions(url) { - let m = regexps[0].exec(url); - let userId, trackId; - if (m) { - [userId, trackId] = [m[1], m[2]]; - } else { - m = regexps[1].exec(url); - if (!m) { - throw new Error('Invalid endomodo url'); - } - [trackId, userId] = [m[1], m[2]]; - } - const requestOptions = [{ - url: urlViaCorsProxy(`https://www.endomondo.com/rest/v1/users/${userId}/workouts/${trackId}`), - options: {responseType: 'binarystring'} - }]; - return {requestOptions}; -} - -function endomondoParser(name, responses) { - if (responses.length !== 1) { - throw new Error(`Invalid responses array length ${responses.length}`); - } - let data; - try { - data = JSON.parse(responses[0].responseBinaryText) - } catch (e) { - return [{name: name, error: 'UNSUPPORTED'}]; - } - if (!data.points || !data.points.points) { - return [{error: 'Endomondo user disabled viewing this workout track'}]; - } - const track = data.points.points - .filter((p) => p.latitude) - .map((p) => { - return { - lat: p.latitude, - lng: p.longitude - } - } - ); - - let trackName = `${data.local_start_time.split('T')[0]}, ${data.distance.toFixed(1)} km`; - if (data.author && data.author.name) { - trackName += `, ${data.author.name} `; - }; - const geodata = { - name: trackName, - tracks: [track] - }; - return [geodata]; -} - - -export {isEndomondoUrl, endomondoRequestOptions, endomondoParser} diff --git a/src/lib/leaflet.control.track-list/lib/geo_file_exporters.js b/src/lib/leaflet.control.track-list/lib/geo_file_exporters.js @@ -1,7 +1,7 @@ import L from 'leaflet'; import utf8 from 'utf8'; import escapeHtml from 'escape-html'; -import {saveNktk} from './nktk'; +import {saveNktk} from './parsers/nktk'; function getSegmentLatForLng(latLng1, latLng2, lng) { const deltaLat = latLng2.lat - latLng1.lat; diff --git a/src/lib/leaflet.control.track-list/lib/geo_file_formats.js b/src/lib/leaflet.control.track-list/lib/geo_file_formats.js @@ -1,684 +0,0 @@ -import JSUnzip from 'vendored/github.com/augustl/js-unzip/js-unzip'; -import tynyInflate from 'tiny-inflate'; -import stripBom from 'lib/stripBom'; - -import {decode as utf8_decode} from 'utf8'; -import {fetch} from 'lib/xhr-promise'; -import urlViaCorsProxy from 'lib/CORSProxy'; -import {isGpsiesUrl, gpsiesRequestOptions, gpsiesParser} from './gpsies'; -import {isStravaUrl, stravaRequestOptions, stravaParser} from './strava'; -import {isEndomondoUrl, endomondoRequestOptions, endomondoParser} from './endomondo'; -import {parseTrackUrlData, parseNakarteUrl, isNakarteLinkUrl, nakarteLinkRequestOptions, nakarteLinkParser} from './nktk'; -import {stringToArrayBuffer, arrayBufferToString} from 'lib/binary-strings'; - -function xmlGetNodeText(node) { - if (node) { - return Array.prototype.slice.call(node.childNodes) - .map(function(node) { - return node.nodeValue; - } - ) - .join(''); - } -} - - -function inflate(compressed, originalSize) { - if (originalSize === 0) { - return ''; - } - const out = new Uint8Array(originalSize); - tynyInflate(new Uint8Array(stringToArrayBuffer(compressed)), out); - return arrayBufferToString(out); -} - -function parseGpx(txt, name, preferNameFromFile) { - var error; - - function getSegmentPoints(segment_element) { - var points_elements = segment_element.getElementsByTagName('trkpt'); - var points = []; - for (var i = 0; i < points_elements.length; i++) { - var point_element = points_elements[i]; - var lat = parseFloat(point_element.getAttribute('lat')); - var lng = parseFloat(point_element.getAttribute('lon')); - if (isNaN(lat) || isNaN(lng)) { - error = 'CORRUPT'; - break; - } - points.push({lat: lat, lng: lng}); - } - return points; - } - - var getTrackSegments = function(xml) { - var segments = []; - var segments_elements = xml.getElementsByTagName('trkseg'); - for (var i = 0; i < segments_elements.length; i++) { - var segment_points = getSegmentPoints(segments_elements[i]); - if (segment_points.length) { - segments.push(segment_points); - } - } - return segments; - }; - - function getRoutePoints(rte_element) { - var points_elements = rte_element.getElementsByTagName('rtept'); - var points = []; - for (var i = 0; i < points_elements.length; i++) { - var point_element = points_elements[i]; - var lat = parseFloat(point_element.getAttribute('lat')); - var lng = parseFloat(point_element.getAttribute('lon')); - if (isNaN(lat) || isNaN(lng)) { - error = 'CORRUPT'; - break; - } - points.push({lat: lat, lng: lng}); - } - return points; - } - - var getRoutes = function(xml) { - var routes = []; - var rte_elements = xml.getElementsByTagName('rte'); - for (var i = 0; i < rte_elements.length; i++) { - var rte_points = getRoutePoints(rte_elements[i]); - if (rte_points.length) { - routes.push(rte_points); - } - } - return routes; - }; - - var getWaypoints = function(xml) { - var waypoint_elements = xml.getElementsByTagName('wpt'); - var waypoints = []; - for (var i = 0; i < waypoint_elements.length; i++) { - var waypoint_element = waypoint_elements[i]; - var waypoint = {}; - waypoint.lat = parseFloat(waypoint_element.getAttribute('lat')); - waypoint.lng = parseFloat(waypoint_element.getAttribute('lon')); - if (isNaN(waypoint.lat) || isNaN(waypoint.lng)) { - error = 'CORRUPT'; - continue; - } - let wptName = xmlGetNodeText(waypoint_element.getElementsByTagName('name')[0]) || ''; - try { - wptName = utf8_decode((wptName)); - } catch (e) { - error = 'CORRUPT'; - wptName = '__invalid point name__'; - } - waypoint.name = wptName; - waypoint.symbol_name = xmlGetNodeText(waypoint_element.getElementsByTagName('sym')[0]); - waypoints.push(waypoint); - } - return waypoints; - }; - - txt = stripBom(txt); - // remove namespaces - txt = txt.replace(/<([^ >]+):([^ >]+)/g, '<$1_$2'); - try { - var dom = (new DOMParser()).parseFromString(txt, "text/xml"); - } catch (e) { - return null; - } - if (dom.documentElement.nodeName === 'parsererror') { - return null; - } - if (dom.getElementsByTagName('gpx').length === 0) { - return null; - } - if (preferNameFromFile) { - for (let trk of [...dom.getElementsByTagName('trk')]) { - let trkName = trk.getElementsByTagName('name')[0]; - if (trkName) { - try { - trkName = utf8_decode(xmlGetNodeText(trkName)); - } catch (e) { - error = 'CORRUPT'; - } - if (trkName.length) { - name = trkName; - break; - } - } - } - } - return [{ - name: name, - tracks: getTrackSegments(dom).concat(getRoutes(dom)), - points: getWaypoints(dom), - error: error - }]; -} - - -function parseOziRte(txt, name) { - let error, segments = []; - txt = stripBom(txt); - const lines = txt.split('\n'); - if (lines[0].indexOf('OziExplorer Route File') !== 0) { - return null; - } - let currentSegment = []; - for (let i=4; i < lines.length; i++) { - let line = lines[i].trim(); - if (!line) { - continue; - } - let fields = line.split(','); - if (fields[0] === 'R') { - if (currentSegment.length) { - segments.push(currentSegment); - } - currentSegment = []; - } else if (fields[0] === 'W') { - let lat = parseFloat(fields[5]); - let lng = parseFloat(fields[6]); - if (isNaN(lat) || isNaN(lng)) { - error = 'CORRUPT'; - break; - } - currentSegment.push({lat, lng}); - } else { - error = 'CORRUPT'; - break - } - } - if (currentSegment.length) { - segments.push(currentSegment); - } - return [{name, tracks: segments, error}]; -} - -function parseOziPlt(txt, name) { - var error; - var segments = []; - txt = stripBom(txt); - var lines = txt.split('\n'); - if (lines[0].indexOf('OziExplorer Track Point File') !== 0) { - return null; - } - var expected_points_num = parseInt(lines[5], 10); - var current_segment = []; - var total_points_num = 0; - for (var i = 6; i < lines.length; i++) { - var line = lines[i].trim(); - if (!line) { - continue; - } - var fields = line.split(','); - var lat = parseFloat(fields[0]); - var lon = parseFloat(fields[1]); - var is_start_of_segment = parseInt(fields[2], 10); - if (isNaN(lat) || isNaN(lon) || isNaN(is_start_of_segment)) { - error = 'CORRUPT'; - break; - } - if (is_start_of_segment) { - current_segment = []; - } - if (!current_segment.length) { - segments.push(current_segment); - } - current_segment.push({lat: lat, lng: lon}); - total_points_num += 1; - } - if (isNaN(expected_points_num) || (expected_points_num !== 0 && expected_points_num !== total_points_num)) { - error = 'CORRUPT'; - } - return [{name: name, tracks: segments, error: error}]; -} - -function decodeCP1251(s) { - var c, i, s2 = []; - for (i = 0; i < s.length; i++) { - c = s.charCodeAt(i); - if (c >= 192 && c <= 255) { - c += (0x410 - 192); - } else if (c === 168) { - c = 0x0401; - } else if (c === 184) { - c = 0x0451; - } - s2.push(String.fromCharCode(c)); - } - return s2.join(''); -} - -function decode866(s) { - var c, i, s2 = []; - for (i = 0; i < s.length; i++) { - c = s.charCodeAt(i); - if (c >= 128 && c <= 175) { - c += (0x410 - 128); - } else if (c >= 224 && c <= 239) { - c += (0x440 - 224); - } else if (c === 240) { - c = 0x0401; - } else if (c === 241) { - c = 0x0451; - } - s2.push(String.fromCharCode(c)); - } - return s2.join(''); -} - -function parseOziWpt(txt, name) { - var points = [], - error, - lines, line, - i, - lat, lng, pointName, fields; - txt = stripBom(txt); - lines = txt.split('\n'); - if (lines[0].indexOf('OziExplorer Waypoint File') !== 0) { - return null; - } - for (i = 4; i < lines.length; i++) { - line = lines[i].trim(); - if (!line) { - continue; - } - fields = line.split(','); - lat = parseFloat(fields[2]); - lng = parseFloat(fields[3]); - pointName = decodeCP1251(fields[1]).trim(); - if (isNaN(lat) || isNaN(lng)) { - error = 'CORRUPT'; - break; - } - points.push({ - lat: lat, - lng: lng, - name: pointName - } - ); - } - return [{name: name, points: points, error: error}]; -} - -function parseKml(txt, name) { - var error; - var getSegmentPoints = function(coordinates_element) { - // convert multiline text value of tag to single line - var coordinates_string = xmlGetNodeText(coordinates_element); - var points_strings = coordinates_string.split(/\s+/); - var points = []; - for (var i = 0; i < points_strings.length; i++) { - if (points_strings[i].length) { - var point = points_strings[i].split(','); - var lat = parseFloat(point[1]); - var lng = parseFloat(point[0]); - if (isNaN(lat) || isNaN(lng)) { - error = 'CORRUPT'; - break; - } - points.push({lat: lat, lng: lng}); - } - } - return points; - }; - - var getTrackSegments = function(xml) { - var segments_elements = xml.getElementsByTagName('LineString'); - var segments = []; - for (var i = 0; i < segments_elements.length; i++) { - var coordinates_element = segments_elements[i].getElementsByTagName('coordinates'); - if (coordinates_element.length) { - var segment_points = getSegmentPoints(coordinates_element[0]); - if (segment_points.length) { - segments.push(segment_points); - } - } - } - return segments; - }; - - function getPoints(dom) { - var points = [], - placemarks, i, coord, name, lat, lng, pointObjs; - placemarks = dom.getElementsByTagName('Placemark'); - for (i = 0; i < placemarks.length; i++) { - pointObjs = placemarks[i].getElementsByTagName('Point'); - if (pointObjs.length === 0) { - continue - } else if (pointObjs.length > 1) { - error = 'CORRUPT'; - break; - } - coord = pointObjs[0].getElementsByTagName('coordinates'); - if (coord.length !== 1) { - error = 'CORRUPT'; - break; - } - coord = xmlGetNodeText(coord[0]); - coord = coord.split(','); - lat = parseFloat(coord[1]); - lng = parseFloat(coord[0]); - if (isNaN(lat) || isNaN(lng)) { - error = 'CORRUPT'; - break; - } - name = placemarks[i].getElementsByTagName('name'); - if (name.length !== 1) { - error = 'CORRUPT'; - break; - } - try { - name = utf8_decode(xmlGetNodeText(name[0])).trim(); - } catch (e) { - error = 'CORRUPT'; - break; - } - points.push({ - name: name, - lat: lat, - lng: lng - } - ); - } - return points; - } - - txt = stripBom(txt); - txt = txt.replace(/<([^ >]+):([^ >]+)/g, '<$1_$2'); - try { - var dom = (new DOMParser()).parseFromString(txt, "text/xml"); - } catch (e) { - return null; - } - if (dom.documentElement.nodeName === 'parsererror') { - return null; - } - if (dom.getElementsByTagName('kml').length === 0) { - return null; - } - - return [{name: name, tracks: getTrackSegments(dom), points: getPoints(dom), error: error}]; -} - -function parseKmz(txt, name) { - var uncompressed; - try { - var unzipper = new JSUnzip(txt); - } catch (e) { - return null; - } - var tracks = [], - points = [], - geodata, - error; - var hasDocKml = false; - if (!unzipper.isZipFile()) { - return null; - } - try { - unzipper.readEntries(); - } catch (e) { - return null; - } - var i, entry; - for (i = 0; i < unzipper.entries.length; i++) { - entry = unzipper.entries[i]; - if (entry.fileName === 'doc.kml') { - hasDocKml = true; - break; - } - } - if (!hasDocKml) { - return null; - } - - for (i = 0; i < unzipper.entries.length; i++) { - entry = unzipper.entries[i]; - if (entry.fileName.match(/\.kml$/i)) { - if (entry.compressionMethod === 0) { - uncompressed = entry.data; - } else if (entry.compressionMethod === 8) { - uncompressed = inflate(entry.data, entry.uncompressedSize); - } else { - return null; - } - geodata = parseKml(uncompressed, 'dummmy'); - if (geodata) { - error = error || geodata[0].error; - tracks.push.apply(tracks, geodata[0].tracks); - points.push.apply(points, geodata[0].points); - } - } - } - - geodata = [{name: name, error: error, tracks: tracks, points: points}]; - return geodata; -} - -function parseYandexRulerString(s) { - var last_lat = 0; - var last_lng = 0; - var error; - var points = []; - s = s.replace(/%2C/ig, ','); - var points_str = s.split('~'); - for (var i = 0; i < points_str.length; i++) { - var point = points_str[i].split(','); - var lng = parseFloat(point[0]); - var lat = parseFloat(point[1]); - if (isNaN(lat) || isNaN(lng)) { - error = 'CORRUPT'; - break; - } - last_lng += lng; - last_lat += lat; - points.push({lat: last_lat, lng: last_lng}); - } - return {error: error, points: points}; -} - - -function parseYandexRulerUrl(s) { - var re = /yandex\..+[?&]rl=([^&]+)/; - var m = re.exec(s); - if (!m) { - return null; - } - var res = parseYandexRulerString(m[1]); - return [{name: 'Yandex ruler', error: res.error, tracks: [res.points]}]; -} - - -function parseZip(txt, name) { - try { - var unzipper = new JSUnzip(txt); - } catch (e) { - return null; - } - if (!unzipper.isZipFile()) { - return null; - } - try { - unzipper.readEntries(); - } catch (e) { - return null; - } - var geodata_array = []; - for (var i = 0; i < unzipper.entries.length; i++) { - var entry = unzipper.entries[i]; - var uncompressed; - if (entry.compressionMethod === 0) { - uncompressed = entry.data; - } else if (entry.compressionMethod === 8) { - uncompressed = inflate(entry.data, entry.uncompressedSize); - } else { - return null; - } - var file_name = decode866(entry.fileName); - var geodata = parseGeoFile(file_name, uncompressed); - for (let item of geodata) { - if (item.error === 'UNSUPPORTED' && item.name.match(/\.pdf$|\.doc$|\.txt$\.jpg$/)) { - continue; - } - geodata_array.push(item) - } - } - return geodata_array; -} - -// function parseYandexMap(txt) { -// var start_tag = '<script id="vpage" type="application/json">'; -// var json_start = txt.indexOf(start_tag); -// if (json_start === -1) { -// return null; -// } -// json_start += start_tag.length; -// var json_end = txt.indexOf('</script>', json_start); -// if (json_end === -1) { -// return null; -// } -// var map_data = txt.substring(json_start, json_end); -// map_data = JSON.parse(map_data); -// console.log(map_data); -// if (!('request' in map_data)) { -// return null; -// } -// var name = 'YandexMap'; -// var segments = []; -// var error; -// if (map_data.vpage && map_data.vpage.data && map_data.vpage.data.objects && map_data.vpage.data.objects.length) { -// var mapName = ('' + (map_data.vpage.data.name || '')).trim(); -// if (mapName.length > 3) { -// name = ''; -// } else if (mapName.length) { -// name += ': '; -// } -// name += fileutils.decodeUTF8(mapName); -// map_data.vpage.data.objects.forEach(function(obj){ -// if (obj.pts && obj.pts.length) { -// var segment = []; -// for (var i=0; i< obj.pts.length; i++) { -// var pt = obj.pts[i]; -// var lng = parseFloat(pt[0]); -// var lat = parseFloat(pt[1]); -// if (isNaN(lat) || isNaN(lng)) { -// error = 'CORRUPT'; -// break; -// } -// segment.push({lat: lat, lng:lng}); -// } -// if (segment.length) { -// segments.push(segment); -// } -// } -// }); -// } -// if (map_data.request.args && map_data.request.args.rl) { -// var res = parseYandexRulerString(map_data.request.args.rl); -// error = error || res.error; -// if (res.points && res.points.length) { -// segments.push(res.points); -// } -// } -// return [{name: name, error: error, tracks: segments}]; -// } - - -function parseTrackUrl(s) { - var i = s.indexOf('track://'); - if (i === -1) { - return null; - } - return parseTrackUrlData(s.substring(i + 8)); -} - - -function simpleRequestOptions(url) { - const requestOptions = [{ - url: urlViaCorsProxy(url), - options: {responseType: 'binarystring'} - }]; - return {requestOptions}; -} - - -function simpleTrackParser(name, responses) { - if (responses.length !== 1) { - throw new Error(`Invalid responses array length ${responses.length}`); - } - return parseGeoFile(name, responses[0].responseBinaryText); -} - - -async function loadFromUrl(url) { - let geodata; - geodata = parseGeoFile('', url); - if (geodata.length === 0 || geodata.length > 1 || geodata[0].error !== 'UNSUPPORTED') { - return Promise.resolve(geodata); - } - let requestOptionsGetter = simpleRequestOptions; - let parser = simpleTrackParser; - - - if (isGpsiesUrl(url)) { - requestOptionsGetter = gpsiesRequestOptions; - parser = gpsiesParser; - } else if (isEndomondoUrl(url)) { - requestOptionsGetter = endomondoRequestOptions; - parser = endomondoParser; - } else if (isStravaUrl(url)) { - requestOptionsGetter = stravaRequestOptions; - parser = stravaParser; - } else if (isNakarteLinkUrl(url)) { - requestOptionsGetter = nakarteLinkRequestOptions; - parser = nakarteLinkParser; - } - - const {requestOptions, extra} = requestOptionsGetter(url); - let responses; - try { - const requests = requestOptions.map((it) => fetch(it.url, it.options)); - responses = await Promise.all(requests); - } catch (e) { - return [{name: url, error: 'NETWORK'}]; - } - let responseURL = responses[0].responseURL; - try { - responseURL = decodeURIComponent(responseURL); - } catch (e) { - } - let name = responseURL - .split('#')[0] - .split('?')[0] - .replace(/\/*$/, '') - .split('/') - .pop(); - return parser(name, responses, extra); -} - - -function parseGeoFile(name, data) { - var parsers = [ - parseTrackUrl, - parseNakarteUrl, - parseKmz, - parseZip, - parseGpx, - parseOziRte, - parseOziPlt, - parseOziWpt, - parseKml, - parseYandexRulerUrl, -// parseYandexMap - ]; - for (var i = 0; i < parsers.length; i++) { - var parsed = parsers[i](data, name); - if (parsed !== null) { - return parsed; - } - } - return [{name: name, error: 'UNSUPPORTED'}]; -} - -export {parseGeoFile, parseGpx, loadFromUrl}; diff --git a/src/lib/leaflet.control.track-list/lib/gpsies.js b/src/lib/leaflet.control.track-list/lib/gpsies.js @@ -1,49 +0,0 @@ -import urlViaCorsProxy from 'lib/CORSProxy'; -import {parseGpx} from './geo_file_formats' - -function urlEncode(d) { - return Object.entries(d).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&'); -} - -const re = /^https?:\/\/www\.gpsies\.com\/map\.do[^?]*\?fileId=([a-z]+)/; - -function isGpsiesUrl(url) { - return re.test(url); -} - -function gpsiesRequestOptions(url) { - const m = re.exec(url); - if (!m) { - throw new Error('Invalid gpsies url'); - } - const trackId = m[1]; - const newUrl = urlViaCorsProxy('https://www.gpsies.com/download.do'); - const requestOptions = [{ - url: newUrl, - options: { - method: 'POST', - data: urlEncode({ - fileId: trackId, - speed: '10', - dataType: '3', - filetype: 'gpxTrk', - submitButton: '', - inappropriate: '' - } - ), - headers: [["Content-type", "application/x-www-form-urlencoded"]], - responseType: 'binarystring' - } - }]; - return {requestOptions}; -} - - -function gpsiesParser(name, responses) { - if (responses.length !== 1) { - throw new Error(`Invalid responses array length ${responses.length}`); - } - return parseGpx(responses[0].responseBinaryText, name, true); -} - -export {gpsiesRequestOptions, isGpsiesUrl, gpsiesParser} -\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/loadFromUrl.js b/src/lib/leaflet.control.track-list/lib/loadFromUrl.js @@ -0,0 +1,12 @@ +import services from './services'; + +async function loadFromUrl(url) { + for (let serviceClass of services) { + let service = new serviceClass(url); + if (service.isOurUrl()) { + return service.geoData(); + } + } +} + +export default loadFromUrl; diff --git a/src/lib/leaflet.control.track-list/lib/nktk.js b/src/lib/leaflet.control.track-list/lib/nktk.js @@ -1,394 +0,0 @@ -import Pbf from 'pbf'; -import {TrackView} from './nktk_pb'; -import {arrayBufferToString, stringToArrayBuffer} from 'lib/binary-strings'; -import utf8 from 'utf8'; -import config from 'config'; - -const arcUnit = ((1 << 24) - 1) / 360; - -function encodeUrlSafeBase64(s) { - return (btoa(s) - .replace(/\+/g, '-') - .replace(/\//g, '_') - // .replace(/=+$/, '') - ); -} - -function decodeUrlSafeBase64(s) { - var decoded; - s = s - .replace(/[\n\r \t]/g, '') - .replace(/-/g, '+') - .replace(/_/g, '/'); - try { - decoded = atob(s); - } catch (e) { - } - if (decoded && decoded.length) { - return decoded; - } - return null; -} - -function PackedStreamReader(s) { - this._string = s; - this.position = 0; -} - -PackedStreamReader.prototype.readNumber = function() { - var n = unpackNumber(this._string, this.position); - this.position += n[1]; - return n[0]; -}; - -PackedStreamReader.prototype.readString = function(size) { - var s = this._string.slice(this.position, this.position + size); - this.position += size; - return s; -}; - -function unpackNumber(s, position) { - var x, - n = 0; - x = s.charCodeAt(position); - if (isNaN(x)) { - throw new Error('Unexpected end of line while unpacking number'); - } - if (x < 128) { - n = x - 64; - return [n, 1]; - } - n = x & 0x7f; - x = s.charCodeAt(position + 1); - if (isNaN(x)) { - throw new Error('Unexpected end of line while unpacking number'); - } - if (x < 128) { - n |= x << 7; - n -= 8192; - return [n, 2]; - } - n |= (x & 0x7f) << 7; - x = s.charCodeAt(position + 2); - if (isNaN(x)) { - throw new Error('Unexpected end of line while unpacking number'); - } - if (x < 128) { - n |= x << 14; - n -= 1048576; - return [n, 3]; - } - n |= (x & 0x7f) << 14; - x = s.charCodeAt(position + 3); - if (isNaN(x)) { - throw new Error('Unexpected end of line while unpacking number'); - } - n |= x << 21; - n -= 268435456; - return [n, 4]; -} - -function deltaEncodeSegment(points) { - let deltaLats = [], - deltaLons = []; - let lastLon = 0, - lastLat = 0, - lon, lat; - for (let i = 0, len = points.length; i < len; i++) { - let p = points[i]; - lon = Math.round(p.lng * arcUnit); - lat = Math.round(p.lat * arcUnit); - let deltaLon = lon - lastLon; - let deltaLat = lat - lastLat; - deltaLats.push(deltaLat); - deltaLons.push(deltaLon); - lastLon = lon; - lastLat = lat; - } - return {deltaLats, deltaLons}; -} - -function deltaDecodeSegment(deltaLats, deltaLons) { - let encodedLat = 0, - encodedLon = 0; - const points = []; - for (let i = 0; i < deltaLats.length; i++) { - encodedLat += deltaLats[i]; - encodedLon += deltaLons[i]; - points.push({lat: encodedLat / arcUnit, lng: encodedLon / arcUnit}); - } - return points; -} - -function saveNktk(segments, name, color, measureTicksShown, waypoints, trackHidden) { - const trackView = { - view: { - color, - shown: !trackHidden, - ticksShown: measureTicksShown, - } - }; - const track = trackView.track = {name}; - if (segments && segments.length) { - let deltaEncodedSegments = []; - for (let segment of segments) { - let {deltaLats, deltaLons} = deltaEncodeSegment(segment); - deltaEncodedSegments.push({lats: deltaLats, lons: deltaLons}); - } - track.segments = deltaEncodedSegments; - } - if (waypoints && waypoints.length) { - let midLon = 0, - midLat = 0; - waypoints.forEach((wp) => { - midLon += wp.latlng.lng; - midLat += wp.latlng.lat; - } - ); - midLon = Math.round(midLon * arcUnit / waypoints.length); - midLat = Math.round(midLat * arcUnit / waypoints.length); - track.waypoints = { - midLat, midLon - }; - - let packedWaypoints = []; - for (let waypoint of waypoints) { - packedWaypoints.push({ - name: waypoint.label, - lat: Math.round(waypoint.latlng.lat * arcUnit) - midLat, - lon: Math.round(waypoint.latlng.lng * arcUnit) - midLon - }); - } - track.waypoints.waypoints = packedWaypoints; - } - const pbf = new Pbf(); - const versionStr = String.fromCharCode(4 + 64); - TrackView.write(trackView, pbf); - const s = versionStr + arrayBufferToString(pbf.finish()); - return encodeUrlSafeBase64(s); -} - -function parseTrackUrlData(s) { - s = decodeUrlSafeBase64(s); - if (!s) { - return [{name: 'Text encoded track', error: ['CORRUPT']}]; - } - return parseNktkOld(s, 0); -} - -function parseNktkOld(s, version) { - var name, - n, - segments = [], - segment, - segmentsCount, - pointsCount, - arcUnit = ((1 << 24) - 1) / 360, - x, y, - error, midX, midY, /*symbol,*/ waypointName, - wayPoints = [], color, measureTicksShown, trackHidden = false; - s = new PackedStreamReader(s); - try { - n = s.readNumber(); - name = s.readString(n); - name = utf8.decode(name); - segmentsCount = s.readNumber(); - for (; segmentsCount--;) { - segment = []; - pointsCount = s.readNumber(); - x = 0; - y = 0; - for (; pointsCount--;) { - x += s.readNumber(); - y += s.readNumber(); - segment.push({lng: x / arcUnit, lat: y / arcUnit}); - } - segments.push(segment); - segment = null; - } - } catch (e) { - if (e.message.match('Unexpected end of line while unpacking number')) { - error = ['CORRUPT']; - if (segment) { - segments.push(segment); - } - } else { - throw e; - } - } - try { - color = s.readNumber(); - measureTicksShown = s.readNumber(); - } catch (e) { - if (e.message.match('Unexpected end of line while unpacking number')) { - color = 0; - measureTicksShown = 0; - if (version > 0) { - error = ['CORRUPT']; - } - } else { - throw e; - } - } - if (version >= 3) { - try { - trackHidden = !!(s.readNumber()) - } catch (e) { - if (e.message.match('Unexpected end of line while unpacking number')) { - error = ['CORRUPT']; - } else { - throw e; - } - } - } - if (version >= 2) { - try { - pointsCount = s.readNumber(); - if (pointsCount) { - midX = s.readNumber(); - midY = s.readNumber(); - } - for (; pointsCount--;) { - n = s.readNumber(); - waypointName = s.readString(n); - waypointName = utf8.decode(waypointName); - - // let symbol = s.readNumber(); - s.readNumber(); - - x = s.readNumber() + midX; - y = s.readNumber() + midY; - wayPoints.push({ - name: waypointName, - lat: y / arcUnit, - lng: x / arcUnit, - - } - ); - } - } catch (e) { - if (e.message.match('Unexpected end of line while unpacking number')) { - error = ['CORRUPT']; - } else { - throw e; - } - } - } - var geoData = { - name: name || "Text encoded track", - tracks: segments, - error: error, - points: wayPoints, - color: color, - measureTicksShown: measureTicksShown, - trackHidden: trackHidden - }; - return [geoData]; -} - -function parseNktkProtobuf(s) { - const pbf = new Pbf(stringToArrayBuffer(s)); - let trackView; - try { - trackView = TrackView.read(pbf); - } catch (e) { - return [{name: 'Text encoded track', error: ['CORRUPT']}]; - } - const geoData = { - name: trackView.track.name || "Text encoded track", - color: trackView.view.color, - trackHidden: !trackView.view.shown, - measureTicksShown: trackView.view.ticksShown, - }; - const segments = trackView.track.segments; - if (segments && segments.length) { - geoData.tracks = segments.map((segment) => deltaDecodeSegment(segment.lats, segment.lons)); - } - if (trackView.track.waypoints && trackView.track.waypoints.waypoints.length) { - const waypoints = geoData.points = []; - for (let waypoint of trackView.track.waypoints.waypoints) { - waypoints.push({ - name: waypoint.name, - lat: (waypoint.lat + trackView.track.waypoints.midLat) / arcUnit, - lng: (waypoint.lon + trackView.track.waypoints.midLon) / arcUnit - }); - } - } - return [geoData]; - -} - -function parseNktkFragment(s) { - s = decodeUrlSafeBase64(s); - if (!s) { - return [{name: 'Text encoded track', error: ['CORRUPT']}]; - } - const reader = new PackedStreamReader(s); - let version = reader.readNumber(); - if (version === 1 || version === 2 || version === 3) { - return parseNktkOld(s.substring(reader.position), version); - } else if (version === 4) { - return parseNktkProtobuf(s.substring(reader.position)); - } else { - return [{name: 'Text encoded track', error: ['CORRUPT']}]; - } -} - -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 parseNakarteUrl(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) -} - - -const nakarteLinkRe = /#.*nktl=([A-Za-z0-9_-]+)/; - - -function isNakarteLinkUrl(url) { - return nakarteLinkRe.test(url); -} - - -function nakarteLinkRequestOptions(url) { - const m = nakarteLinkRe.exec(url); - if (!m) { - throw new Error('Invalid nakarteLink url'); - } - const trackId = m[1]; - const requestOptions = [{ - url: (`${config.tracksStorageServer}/track/${trackId}`), - options: {responseType: 'binarystring'}} - ]; - return {requestOptions} -} - -function nakarteLinkParser(_, responses) { - if (responses.length !== 1) { - throw new Error(`Invalid responses array length ${responses.length}`); - } - return parseNktkSequence(responses[0].responseBinaryText); -} - -export {saveNktk, parseTrackUrlData, parseNakarteUrl, isNakarteLinkUrl, nakarteLinkRequestOptions, - nakarteLinkParser, parseNktkSequence}; -\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/parseGeoFile.js b/src/lib/leaflet.control.track-list/lib/parseGeoFile.js @@ -0,0 +1,13 @@ +import parsers from './parsers'; + +function parseGeoFile(name, data) { + for (var i = 0; i < parsers.length; i++) { + var parsed = parsers[i](data, name); + if (parsed !== null) { + return parsed; + } + } + return [{name: name, error: 'UNSUPPORTED'}]; +} + +export default parseGeoFile; diff --git a/src/lib/leaflet.control.track-list/lib/parsers/codePages.js b/src/lib/leaflet.control.track-list/lib/parsers/codePages.js @@ -0,0 +1,36 @@ +function decode866(s) { + var c, i, s2 = []; + for (i = 0; i < s.length; i++) { + c = s.charCodeAt(i); + if (c >= 128 && c <= 175) { + c += (0x410 - 128); + } else if (c >= 224 && c <= 239) { + c += (0x440 - 224); + } else if (c === 240) { + c = 0x0401; + } else if (c === 241) { + c = 0x0451; + } + s2.push(String.fromCharCode(c)); + } + return s2.join(''); +} + +function decodeCP1251(s) { + var c, i, s2 = []; + for (i = 0; i < s.length; i++) { + c = s.charCodeAt(i); + if (c >= 192 && c <= 255) { + c += (0x410 - 192); + } else if (c === 168) { + c = 0x0401; + } else if (c === 184) { + c = 0x0451; + } + s2.push(String.fromCharCode(c)); + } + return s2.join(''); +} + +export {decode866, decodeCP1251}; + diff --git a/src/lib/leaflet.control.track-list/lib/parsers/gpx.js b/src/lib/leaflet.control.track-list/lib/parsers/gpx.js @@ -0,0 +1,128 @@ +import {decode as utf8_decode} from 'utf8'; +import {xmlGetNodeText} from './xmlUtils'; +import stripBom from 'lib/stripBom'; + +function parseGpx(txt, name, preferNameFromFile) { + var error; + + function getSegmentPoints(segment_element) { + var points_elements = segment_element.getElementsByTagName('trkpt'); + var points = []; + for (var i = 0; i < points_elements.length; i++) { + var point_element = points_elements[i]; + var lat = parseFloat(point_element.getAttribute('lat')); + var lng = parseFloat(point_element.getAttribute('lon')); + if (isNaN(lat) || isNaN(lng)) { + error = 'CORRUPT'; + break; + } + points.push({lat: lat, lng: lng}); + } + return points; + } + + var getTrackSegments = function(xml) { + var segments = []; + var segments_elements = xml.getElementsByTagName('trkseg'); + for (var i = 0; i < segments_elements.length; i++) { + var segment_points = getSegmentPoints(segments_elements[i]); + if (segment_points.length) { + segments.push(segment_points); + } + } + return segments; + }; + + function getRoutePoints(rte_element) { + var points_elements = rte_element.getElementsByTagName('rtept'); + var points = []; + for (var i = 0; i < points_elements.length; i++) { + var point_element = points_elements[i]; + var lat = parseFloat(point_element.getAttribute('lat')); + var lng = parseFloat(point_element.getAttribute('lon')); + if (isNaN(lat) || isNaN(lng)) { + error = 'CORRUPT'; + break; + } + points.push({lat: lat, lng: lng}); + } + return points; + } + + var getRoutes = function(xml) { + var routes = []; + var rte_elements = xml.getElementsByTagName('rte'); + for (var i = 0; i < rte_elements.length; i++) { + var rte_points = getRoutePoints(rte_elements[i]); + if (rte_points.length) { + routes.push(rte_points); + } + } + return routes; + }; + + var getWaypoints = function(xml) { + var waypoint_elements = xml.getElementsByTagName('wpt'); + var waypoints = []; + for (var i = 0; i < waypoint_elements.length; i++) { + var waypoint_element = waypoint_elements[i]; + var waypoint = {}; + waypoint.lat = parseFloat(waypoint_element.getAttribute('lat')); + waypoint.lng = parseFloat(waypoint_element.getAttribute('lon')); + if (isNaN(waypoint.lat) || isNaN(waypoint.lng)) { + error = 'CORRUPT'; + continue; + } + let wptName = xmlGetNodeText(waypoint_element.getElementsByTagName('name')[0]) || ''; + try { + wptName = utf8_decode((wptName)); + } catch (e) { + error = 'CORRUPT'; + wptName = '__invalid point name__'; + } + waypoint.name = wptName; + waypoint.symbol_name = xmlGetNodeText(waypoint_element.getElementsByTagName('sym')[0]); + waypoints.push(waypoint); + } + return waypoints; + }; + + txt = stripBom(txt); + // remove namespaces + txt = txt.replace(/<([^ >]+):([^ >]+)/g, '<$1_$2'); + try { + var dom = (new DOMParser()).parseFromString(txt, "text/xml"); + } catch (e) { + return null; + } + if (dom.documentElement.nodeName === 'parsererror') { + return null; + } + if (dom.getElementsByTagName('gpx').length === 0) { + return null; + } + if (preferNameFromFile) { + for (let trk of [...dom.getElementsByTagName('trk')]) { + let trkName = trk.getElementsByTagName('name')[0]; + if (trkName) { + try { + trkName = utf8_decode(xmlGetNodeText(trkName)); + } catch (e) { + error = 'CORRUPT'; + } + if (trkName.length) { + name = trkName; + break; + } + } + } + } + return [{ + name: name, + tracks: getTrackSegments(dom).concat(getRoutes(dom)), + points: getWaypoints(dom), + error: error + }]; +} + +export default parseGpx; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/parsers/index.js b/src/lib/leaflet.control.track-list/lib/parsers/index.js @@ -0,0 +1,14 @@ +import parseGpx from './gpx'; +import parseZip from './zip'; +import {parseKmz, parseKml} from './kml'; +import {parseOziPlt, parseOziRte, parseOziWpt} from './ozi'; + +export default [ + parseKmz, + parseZip, + parseGpx, + parseOziRte, + parseOziPlt, + parseOziWpt, + parseKml, +] diff --git a/src/lib/leaflet.control.track-list/lib/parsers/jsInflate.js b/src/lib/leaflet.control.track-list/lib/parsers/jsInflate.js @@ -0,0 +1,13 @@ +import tynyInflate from 'tiny-inflate'; +import {stringToArrayBuffer, arrayBufferToString} from 'lib/binary-strings'; + +function jsInflate(compressed, originalSize) { + if (originalSize === 0) { + return ''; + } + const out = new Uint8Array(originalSize); + tynyInflate(new Uint8Array(stringToArrayBuffer(compressed)), out); + return arrayBufferToString(out); +} + +export default jsInflate; diff --git a/src/lib/leaflet.control.track-list/lib/parsers/kml.js b/src/lib/leaflet.control.track-list/lib/parsers/kml.js @@ -0,0 +1,162 @@ +import {decode as utf8_decode} from 'utf8'; +import {xmlGetNodeText} from './xmlUtils'; +import stripBom from 'lib/stripBom'; +import JSUnzip from 'vendored/github.com/augustl/js-unzip/js-unzip'; +import jsInflate from './jsInflate'; + +function parseKml(txt, name) { + var error; + var getSegmentPoints = function(coordinates_element) { + // convert multiline text value of tag to single line + var coordinates_string = xmlGetNodeText(coordinates_element); + var points_strings = coordinates_string.split(/\s+/); + var points = []; + for (var i = 0; i < points_strings.length; i++) { + if (points_strings[i].length) { + var point = points_strings[i].split(','); + var lat = parseFloat(point[1]); + var lng = parseFloat(point[0]); + if (isNaN(lat) || isNaN(lng)) { + error = 'CORRUPT'; + break; + } + points.push({lat: lat, lng: lng}); + } + } + return points; + }; + + var getTrackSegments = function(xml) { + var segments_elements = xml.getElementsByTagName('LineString'); + var segments = []; + for (var i = 0; i < segments_elements.length; i++) { + var coordinates_element = segments_elements[i].getElementsByTagName('coordinates'); + if (coordinates_element.length) { + var segment_points = getSegmentPoints(coordinates_element[0]); + if (segment_points.length) { + segments.push(segment_points); + } + } + } + return segments; + }; + + function getPoints(dom) { + var points = [], + placemarks, i, coord, name, lat, lng, pointObjs; + placemarks = dom.getElementsByTagName('Placemark'); + for (i = 0; i < placemarks.length; i++) { + pointObjs = placemarks[i].getElementsByTagName('Point'); + if (pointObjs.length === 0) { + continue + } else if (pointObjs.length > 1) { + error = 'CORRUPT'; + break; + } + coord = pointObjs[0].getElementsByTagName('coordinates'); + if (coord.length !== 1) { + error = 'CORRUPT'; + break; + } + coord = xmlGetNodeText(coord[0]); + coord = coord.split(','); + lat = parseFloat(coord[1]); + lng = parseFloat(coord[0]); + if (isNaN(lat) || isNaN(lng)) { + error = 'CORRUPT'; + break; + } + name = placemarks[i].getElementsByTagName('name'); + if (name.length !== 1) { + error = 'CORRUPT'; + break; + } + try { + name = utf8_decode(xmlGetNodeText(name[0])).trim(); + } catch (e) { + error = 'CORRUPT'; + break; + } + points.push({ + name: name, + lat: lat, + lng: lng + } + ); + } + return points; + } + + txt = stripBom(txt); + txt = txt.replace(/<([^ >]+):([^ >]+)/g, '<$1_$2'); + try { + var dom = (new DOMParser()).parseFromString(txt, "text/xml"); + } catch (e) { + return null; + } + if (dom.documentElement.nodeName === 'parsererror') { + return null; + } + if (dom.getElementsByTagName('kml').length === 0) { + return null; + } + + return [{name: name, tracks: getTrackSegments(dom), points: getPoints(dom), error: error}]; +} + +function parseKmz(txt, name) { + var uncompressed; + try { + var unzipper = new JSUnzip(txt); + } catch (e) { + return null; + } + var tracks = [], + points = [], + geodata, + error; + var hasDocKml = false; + if (!unzipper.isZipFile()) { + return null; + } + try { + unzipper.readEntries(); + } catch (e) { + return null; + } + var i, entry; + for (i = 0; i < unzipper.entries.length; i++) { + entry = unzipper.entries[i]; + if (entry.fileName === 'doc.kml') { + hasDocKml = true; + break; + } + } + if (!hasDocKml) { + return null; + } + + for (i = 0; i < unzipper.entries.length; i++) { + entry = unzipper.entries[i]; + if (entry.fileName.match(/\.kml$/i)) { + if (entry.compressionMethod === 0) { + uncompressed = entry.data; + } else if (entry.compressionMethod === 8) { + uncompressed = jsInflate(entry.data, entry.uncompressedSize); + } else { + return null; + } + geodata = parseKml(uncompressed, 'dummmy'); + if (geodata) { + error = error || geodata[0].error; + tracks.push.apply(tracks, geodata[0].tracks); + points.push.apply(points, geodata[0].points); + } + } + } + + geodata = [{name: name, error: error, tracks: tracks, points: points}]; + return geodata; +} + +export {parseKml, parseKmz} +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/parsers/nktk.js b/src/lib/leaflet.control.track-list/lib/parsers/nktk.js @@ -0,0 +1,322 @@ +import Pbf from 'pbf'; +import {TrackView} from './nktk_pb'; +import {arrayBufferToString, stringToArrayBuffer} from 'lib/binary-strings'; +import utf8 from 'utf8'; +import urlSafeBase64 from './urlSafeBase64'; + +const arcUnit = ((1 << 24) - 1) / 360; + +function PackedStreamReader(s) { + this._string = s; + this.position = 0; +} + +PackedStreamReader.prototype.readNumber = function() { + var n = unpackNumber(this._string, this.position); + this.position += n[1]; + return n[0]; +}; + +PackedStreamReader.prototype.readString = function(size) { + var s = this._string.slice(this.position, this.position + size); + this.position += size; + return s; +}; + +function unpackNumber(s, position) { + var x, + n = 0; + x = s.charCodeAt(position); + if (isNaN(x)) { + throw new Error('Unexpected end of line while unpacking number'); + } + if (x < 128) { + n = x - 64; + return [n, 1]; + } + n = x & 0x7f; + x = s.charCodeAt(position + 1); + if (isNaN(x)) { + throw new Error('Unexpected end of line while unpacking number'); + } + if (x < 128) { + n |= x << 7; + n -= 8192; + return [n, 2]; + } + n |= (x & 0x7f) << 7; + x = s.charCodeAt(position + 2); + if (isNaN(x)) { + throw new Error('Unexpected end of line while unpacking number'); + } + if (x < 128) { + n |= x << 14; + n -= 1048576; + return [n, 3]; + } + n |= (x & 0x7f) << 14; + x = s.charCodeAt(position + 3); + if (isNaN(x)) { + throw new Error('Unexpected end of line while unpacking number'); + } + n |= x << 21; + n -= 268435456; + return [n, 4]; +} + +function deltaEncodeSegment(points) { + let deltaLats = [], + deltaLons = []; + let lastLon = 0, + lastLat = 0, + lon, lat; + for (let i = 0, len = points.length; i < len; i++) { + let p = points[i]; + lon = Math.round(p.lng * arcUnit); + lat = Math.round(p.lat * arcUnit); + let deltaLon = lon - lastLon; + let deltaLat = lat - lastLat; + deltaLats.push(deltaLat); + deltaLons.push(deltaLon); + lastLon = lon; + lastLat = lat; + } + return {deltaLats, deltaLons}; +} + +function deltaDecodeSegment(deltaLats, deltaLons) { + let encodedLat = 0, + encodedLon = 0; + const points = []; + for (let i = 0; i < deltaLats.length; i++) { + encodedLat += deltaLats[i]; + encodedLon += deltaLons[i]; + points.push({lat: encodedLat / arcUnit, lng: encodedLon / arcUnit}); + } + return points; +} + +function saveNktk(segments, name, color, measureTicksShown, waypoints, trackHidden) { + const trackView = { + view: { + color, + shown: !trackHidden, + ticksShown: measureTicksShown, + } + }; + const track = trackView.track = {name}; + if (segments && segments.length) { + let deltaEncodedSegments = []; + for (let segment of segments) { + let {deltaLats, deltaLons} = deltaEncodeSegment(segment); + deltaEncodedSegments.push({lats: deltaLats, lons: deltaLons}); + } + track.segments = deltaEncodedSegments; + } + if (waypoints && waypoints.length) { + let midLon = 0, + midLat = 0; + waypoints.forEach((wp) => { + midLon += wp.latlng.lng; + midLat += wp.latlng.lat; + } + ); + midLon = Math.round(midLon * arcUnit / waypoints.length); + midLat = Math.round(midLat * arcUnit / waypoints.length); + track.waypoints = { + midLat, midLon + }; + + let packedWaypoints = []; + for (let waypoint of waypoints) { + packedWaypoints.push({ + name: waypoint.label, + lat: Math.round(waypoint.latlng.lat * arcUnit) - midLat, + lon: Math.round(waypoint.latlng.lng * arcUnit) - midLon + }); + } + track.waypoints.waypoints = packedWaypoints; + } + const pbf = new Pbf(); + const versionStr = String.fromCharCode(4 + 64); + TrackView.write(trackView, pbf); + const s = versionStr + arrayBufferToString(pbf.finish()); + return urlSafeBase64.encode(s); +} + +function parseTrackUrlData(s) { + s = urlSafeBase64.decode(s); + if (!s) { + return [{name: 'Text encoded track', error: ['CORRUPT']}]; + } + return parseNktkOld(s, 0); +} + +function parseNktkOld(s, version) { + var name, + n, + segments = [], + segment, + segmentsCount, + pointsCount, + arcUnit = ((1 << 24) - 1) / 360, + x, y, + error, midX, midY, /*symbol,*/ waypointName, + wayPoints = [], color, measureTicksShown, trackHidden = false; + s = new PackedStreamReader(s); + try { + n = s.readNumber(); + name = s.readString(n); + name = utf8.decode(name); + segmentsCount = s.readNumber(); + for (; segmentsCount--;) { + segment = []; + pointsCount = s.readNumber(); + x = 0; + y = 0; + for (; pointsCount--;) { + x += s.readNumber(); + y += s.readNumber(); + segment.push({lng: x / arcUnit, lat: y / arcUnit}); + } + segments.push(segment); + segment = null; + } + } catch (e) { + if (e.message.match('Unexpected end of line while unpacking number')) { + error = ['CORRUPT']; + if (segment) { + segments.push(segment); + } + } else { + throw e; + } + } + try { + color = s.readNumber(); + measureTicksShown = s.readNumber(); + } catch (e) { + if (e.message.match('Unexpected end of line while unpacking number')) { + color = 0; + measureTicksShown = 0; + if (version > 0) { + error = ['CORRUPT']; + } + } else { + throw e; + } + } + if (version >= 3) { + try { + trackHidden = !!(s.readNumber()) + } catch (e) { + if (e.message.match('Unexpected end of line while unpacking number')) { + error = ['CORRUPT']; + } else { + throw e; + } + } + } + if (version >= 2) { + try { + pointsCount = s.readNumber(); + if (pointsCount) { + midX = s.readNumber(); + midY = s.readNumber(); + } + for (; pointsCount--;) { + n = s.readNumber(); + waypointName = s.readString(n); + waypointName = utf8.decode(waypointName); + + // let symbol = s.readNumber(); + s.readNumber(); + + x = s.readNumber() + midX; + y = s.readNumber() + midY; + wayPoints.push({ + name: waypointName, + lat: y / arcUnit, + lng: x / arcUnit, + + } + ); + } + } catch (e) { + if (e.message.match('Unexpected end of line while unpacking number')) { + error = ['CORRUPT']; + } else { + throw e; + } + } + } + var geoData = { + name: name || "Text encoded track", + tracks: segments, + error: error, + points: wayPoints, + color: color, + measureTicksShown: measureTicksShown, + trackHidden: trackHidden + }; + return [geoData]; +} + +function parseNktkProtobuf(s) { + const pbf = new Pbf(stringToArrayBuffer(s)); + let trackView; + try { + trackView = TrackView.read(pbf); + } catch (e) { + return [{name: 'Text encoded track', error: ['CORRUPT']}]; + } + const geoData = { + name: trackView.track.name || "Text encoded track", + color: trackView.view.color, + trackHidden: !trackView.view.shown, + measureTicksShown: trackView.view.ticksShown, + }; + const segments = trackView.track.segments; + if (segments && segments.length) { + geoData.tracks = segments.map((segment) => deltaDecodeSegment(segment.lats, segment.lons)); + } + if (trackView.track.waypoints && trackView.track.waypoints.waypoints.length) { + const waypoints = geoData.points = []; + for (let waypoint of trackView.track.waypoints.waypoints) { + waypoints.push({ + name: waypoint.name, + lat: (waypoint.lat + trackView.track.waypoints.midLat) / arcUnit, + lng: (waypoint.lon + trackView.track.waypoints.midLon) / arcUnit + }); + } + } + return [geoData]; + +} + +function parseNktkFragment(s) { + s = urlSafeBase64.decode(s); + if (!s) { + return [{name: 'Text encoded track', error: ['CORRUPT']}]; + } + const reader = new PackedStreamReader(s); + let version = reader.readNumber(); + if (version === 1 || version === 2 || version === 3) { + return parseNktkOld(s.substring(reader.position), version); + } else if (version === 4) { + return parseNktkProtobuf(s.substring(reader.position)); + } else { + return [{name: 'Text encoded track', error: ['CORRUPT']}]; + } +} + +function parseNktkSequence(s) { + return s.split('/') + .map(parseNktkFragment) + .reduce((acc, cur) => { + acc.push(...cur); + return acc; + }); +} + +export {saveNktk, parseTrackUrlData, parseNktkSequence, parseNktkFragment}; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/nktk.proto b/src/lib/leaflet.control.track-list/lib/parsers/nktk.proto diff --git a/src/lib/leaflet.control.track-list/lib/nktk_pb.js b/src/lib/leaflet.control.track-list/lib/parsers/nktk_pb.js diff --git a/src/lib/leaflet.control.track-list/lib/parsers/ozi.js b/src/lib/leaflet.control.track-list/lib/parsers/ozi.js @@ -0,0 +1,116 @@ +import {decodeCP1251} from './codePages'; +import stripBom from 'lib/stripBom'; + +function parseOziRte(txt, name) { + let error, segments = []; + txt = stripBom(txt); + const lines = txt.split('\n'); + if (lines[0].indexOf('OziExplorer Route File') !== 0) { + return null; + } + let currentSegment = []; + for (let i=4; i < lines.length; i++) { + let line = lines[i].trim(); + if (!line) { + continue; + } + let fields = line.split(','); + if (fields[0] === 'R') { + if (currentSegment.length) { + segments.push(currentSegment); + } + currentSegment = []; + } else if (fields[0] === 'W') { + let lat = parseFloat(fields[5]); + let lng = parseFloat(fields[6]); + if (isNaN(lat) || isNaN(lng)) { + error = 'CORRUPT'; + break; + } + currentSegment.push({lat, lng}); + } else { + error = 'CORRUPT'; + break + } + } + if (currentSegment.length) { + segments.push(currentSegment); + } + return [{name, tracks: segments, error}]; +} + +function parseOziPlt(txt, name) { + var error; + var segments = []; + txt = stripBom(txt); + var lines = txt.split('\n'); + if (lines[0].indexOf('OziExplorer Track Point File') !== 0) { + return null; + } + var expected_points_num = parseInt(lines[5], 10); + var current_segment = []; + var total_points_num = 0; + for (var i = 6; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line) { + continue; + } + var fields = line.split(','); + var lat = parseFloat(fields[0]); + var lon = parseFloat(fields[1]); + var is_start_of_segment = parseInt(fields[2], 10); + if (isNaN(lat) || isNaN(lon) || isNaN(is_start_of_segment)) { + error = 'CORRUPT'; + break; + } + if (is_start_of_segment) { + current_segment = []; + } + if (!current_segment.length) { + segments.push(current_segment); + } + current_segment.push({lat: lat, lng: lon}); + total_points_num += 1; + } + if (isNaN(expected_points_num) || (expected_points_num !== 0 && expected_points_num !== total_points_num)) { + error = 'CORRUPT'; + } + return [{name: name, tracks: segments, error: error}]; +} + + +function parseOziWpt(txt, name) { + var points = [], + error, + lines, line, + i, + lat, lng, pointName, fields; + txt = stripBom(txt); + lines = txt.split('\n'); + if (lines[0].indexOf('OziExplorer Waypoint File') !== 0) { + return null; + } + for (i = 4; i < lines.length; i++) { + line = lines[i].trim(); + if (!line) { + continue; + } + fields = line.split(','); + lat = parseFloat(fields[2]); + lng = parseFloat(fields[3]); + pointName = decodeCP1251(fields[1]).trim(); + if (isNaN(lat) || isNaN(lng)) { + error = 'CORRUPT'; + break; + } + points.push({ + lat: lat, + lng: lng, + name: pointName + } + ); + } + return [{name: name, points: points, error: error}]; +} + +export {parseOziPlt, parseOziRte, parseOziWpt}; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/parsers/urlSafeBase64.js b/src/lib/leaflet.control.track-list/lib/parsers/urlSafeBase64.js @@ -0,0 +1,25 @@ +function encode(s) { + return (btoa(s) + .replace(/\+/g, '-') + .replace(/\//g, '_') + // .replace(/=+$/, '') + ); +} + +function decode(s) { + var decoded; + s = s + .replace(/[\n\r \t]/g, '') + .replace(/-/g, '+') + .replace(/_/g, '/'); + try { + decoded = atob(s); + } catch (e) { + } + if (decoded && decoded.length) { + return decoded; + } + return null; +} + +export default {encode, decode} diff --git a/src/lib/leaflet.control.track-list/lib/parsers/xmlUtils.js b/src/lib/leaflet.control.track-list/lib/parsers/xmlUtils.js @@ -0,0 +1,12 @@ +function xmlGetNodeText(node) { + if (node) { + return Array.prototype.slice.call(node.childNodes) + .map(function(node) { + return node.nodeValue; + } + ) + .join(''); + } +} + +export {xmlGetNodeText} +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/parsers/zip.js b/src/lib/leaflet.control.track-list/lib/parsers/zip.js @@ -0,0 +1,43 @@ +import JSUnzip from 'vendored/github.com/augustl/js-unzip/js-unzip'; +import jsInflate from './jsInflate'; +import {decode866} from './codePages'; +import parseGeoFile from '../parseGeoFile'; + +function parseZip(txt, name) { + try { + var unzipper = new JSUnzip(txt); + } catch (e) { + return null; + } + if (!unzipper.isZipFile()) { + return null; + } + try { + unzipper.readEntries(); + } catch (e) { + return null; + } + var geodata_array = []; + for (var i = 0; i < unzipper.entries.length; i++) { + var entry = unzipper.entries[i]; + var uncompressed; + if (entry.compressionMethod === 0) { + uncompressed = entry.data; + } else if (entry.compressionMethod === 8) { + uncompressed = jsInflate(entry.data, entry.uncompressedSize); + } else { + return null; + } + var file_name = decode866(entry.fileName); + var geodata = parseGeoFile(file_name, uncompressed); + for (let item of geodata) { + if (item.error === 'UNSUPPORTED' && item.name.match(/\.pdf$|\.doc$|\.txt$\.jpg$/)) { + continue; + } + geodata_array.push(item) + } + } + return geodata_array; +} + +export default parseZip; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/services/baseService.js b/src/lib/leaflet.control.track-list/lib/services/baseService.js @@ -0,0 +1,49 @@ +import {fetch} from 'lib/xhr-promise'; + +class BaseService { + constructor(url) { + this.origUrl = url; + } + + isOurUrl() { + throw new Error('Method not implemented'); + } + + requestOptions() { + throw new Error('Method not implemented'); + } + + parseResponse() { + throw new Error('Method not implemented'); + } + + async geoData() { + if (!this.isOurUrl) { + throw new Error('Unsupported url'); + } + const requests = this.requestOptions().map((it) => fetch(it.url, it.options)); + let responses; + try { + responses = await Promise.all(requests); + } catch (e) { + return [{name: this.origUrl, error: 'NETWORK'}]; + } + + return this.parseResponse(responses); + } + + nameFromUrl(url) { + try { + url = decodeURIComponent(url); + } catch (e) { + } + + return url + .split('#')[0] + .split('?')[0] + .replace(/\/*$/, '') + .split('/') + .pop(); + } +} + export default BaseService; diff --git a/src/lib/leaflet.control.track-list/lib/services/endomondo.js b/src/lib/leaflet.control.track-list/lib/services/endomondo.js @@ -0,0 +1,79 @@ +import BaseService from './baseService'; +import urlViaCorsProxy from 'lib/CORSProxy'; + +class Endomondo extends BaseService { + urlRegexps = [ + /^https:\/\/www\.endomondo\.com\/users\/(\d+)\/workouts\/(\d+)/, + /^https:\/\/www\.endomondo\.com\/workouts\/(\d+)\/(\d+)/ + ]; + + isOurUrl() { + return this.urlRegexps.some((re) => re.test(this.origUrl)); + } + + requestOptions() { + let userId, trackId; + let m = this.urlRegexps[0].exec(this.origUrl); + if (m) { + [userId, trackId] = [m[1], m[2]]; + } else { + m = this.urlRegexps[1].exec(this.origUrl); + [trackId, userId] = [m[1], m[2]]; + } + return [{ + url: urlViaCorsProxy(`https://www.endomondo.com/rest/v1/users/${userId}/workouts/${trackId}`), + options: { + responseType: 'binarystring', + isResponseSuccess: (xhr) => (xhr.status === 200 || xhr.status === 404) + }, + + }]; + } + + parseResponse(responses) { + const response = responses[0]; + if (response.status === 404) { + return [{error: 'Invalid link or user disabled viewing this workout track'}]; + } + + let data; + try { + data = JSON.parse(response.responseBinaryText) + } catch (e) { + return [{name: 'Endomondo activity', error: 'UNSUPPORTED'}]; + } + if (!data.points || !data.points.points) { + return [{error: 'Endomondo user disabled viewing this workout track'}]; + } + + const track = data.points.points + .filter((p) => p.latitude) + .map((p) => { + return { + lat: p.latitude, + lng: p.longitude + } + } + ); + if (track.length === 0) { + return [{error: 'Endomondo user disabled viewing this workout track'}]; + } + + const date = data.local_start_time.split('T')[0]; + const dist = `${data.distance.toFixed(1)} km`; + let trackName = `${date}, ${dist}`; + if (data.author && data.author.name) { + trackName += ` ${data.author.name}`; + } + if (data.title) { + trackName += `: ${data.title}`; + } + return [{ + name: trackName, + tracks: [track] + }]; + } + +} + +export default Endomondo; diff --git a/src/lib/leaflet.control.track-list/lib/services/gpsies.js b/src/lib/leaflet.control.track-list/lib/services/gpsies.js @@ -0,0 +1,43 @@ +import BaseService from './baseService'; +import urlViaCorsProxy from 'lib/CORSProxy'; +import parseGpx from '../parsers/gpx'; +import urlEncode from './urlEncode'; + +class Gpsies extends BaseService { + urlRe = /^https?:\/\/www\.gpsies\.com\/map\.do[^?]*\?fileId=([a-z]+)/; + + isOurUrl() { + return this.urlRe.test(this.origUrl); + } + + requestOptions() { + const m = this.urlRe.exec(this.origUrl); + const trackId = m[1]; + const newUrl = urlViaCorsProxy('https://www.gpsies.com/download.do'); + return [{ + url: newUrl, + options: { + method: 'POST', + data: urlEncode({ + fileId: trackId, + speed: '10', + dataType: '3', + filetype: 'gpxTrk', + submitButton: '', + inappropriate: '' + } + ), + headers: [["Content-type", "application/x-www-form-urlencoded"]], + responseType: 'binarystring' + } + }]; + } + + parseResponse(responses) { + const response = responses[0]; + return parseGpx(response.responseBinaryText, this.nameFromUrl(response.responseURL), true); + } +} + + +export default Gpsies +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/services/gpslib.js b/src/lib/leaflet.control.track-list/lib/services/gpslib.js @@ -0,0 +1,28 @@ +import urlViaCorsProxy from 'lib/CORSProxy'; +import BaseService from './baseService'; +import parseGpx from '../parsers/gpx'; + +class Gpslib extends BaseService { + urlRe = /^https?:\/\/(?:.+\.)?gpslib\.[^.]+\/tracks\/info\/(\d+)/; + + isOurUrl() { + return this.urlRe.test(this.origUrl); + } + + requestOptions() { + const m = this.urlRe.exec(this.origUrl); + const trackId = this.trackId = m[1]; + return [{ + url: urlViaCorsProxy(`https://www.gpslib.ru/tracks/download/${trackId}.gpx`), + options: {responseType: 'binarystring'} + }] + } + + parseResponse(responses) { + const response = responses[0]; + return parseGpx(response.responseBinaryText, `GPSLib ${this.trackId}`, true); + } +} + + +export default Gpslib; diff --git a/src/lib/leaflet.control.track-list/lib/services/index.js b/src/lib/leaflet.control.track-list/lib/services/index.js @@ -0,0 +1,23 @@ +import SimpleService from './simpleService' +import Endomondo from './endomondo'; +import Gpsies from './gpsies'; +import Gpslib from './gpslib'; +import Osm from './osm'; +import Strava from './strava'; +import {YandexRuler} from './yandex'; +import {NakarteTrack, NakarteUrl} from './nakarte'; +import {MovescountMove, MovescountRoute} from './movescount'; + +export default [ + YandexRuler, + NakarteTrack, + NakarteUrl, + Endomondo, + Gpsies, + Gpslib, + Osm, + Strava, + MovescountMove, + MovescountRoute, + SimpleService +] +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/services/movescount.js b/src/lib/leaflet.control.track-list/lib/services/movescount.js @@ -0,0 +1,108 @@ +import BaseService from './baseService'; +import urlViaCorsProxy from 'lib/CORSProxy'; + +class MovescountBase extends BaseService { + // urlRe = null; + + isOurUrl() { + return this.urlRe.test(this.origUrl); + } +} + +class MovescountRoute extends MovescountBase { + urlRe = /^https?:\/\/www.movescount.com\/([a-z]{2}\/)?map.*[?&]route=(\d+)/; + + requestOptions() { + const m = this.urlRe.exec(this.origUrl); + const trackId = this.trackId = m[2]; + return [{ + url: urlViaCorsProxy(`http://www.movescount.com/Move/Route/${trackId}`), + options: { + responseType: 'binarystring', + isResponseSuccess: (xhr) => xhr.status === 200 || xhr.status === 403 + }, + }] + } + + parseResponse(responses) { + const response = responses[0]; + if (response.status === 403) { + return [{error: 'Movescount user disabled viewing this route'}]; + } + let data; + let name = `Movescount route ${this.trackId}`; + try { + data = JSON.parse(response.responseBinaryText) + } catch (e) { + return [{name, error: 'UNSUPPORTED'}]; + } + const track = data.points.latitudes.map((lat, i) => ({ + lat, + lng: data.points.longitudes[i] + }) + ); + + name = data.routeName ? data.routeName : name; + + return [{ + name, + tracks: [track] + }]; + } +} + +class MovescountMove extends MovescountBase { + urlRe = /^https?:\/\/www.movescount.com\/([a-z]{2}\/)?moves\/move(\d+)/; + + requestOptions() { + const m = this.urlRe.exec(this.origUrl); + const trackId = m[2]; + return [ + { + url: urlViaCorsProxy(`http://www.movescount.com/Move/Track2/${trackId}`), + options: { + responseType: 'binarystring', + isResponseSuccess: (xhr) => xhr.status === 200 || xhr.status === 403 + } + }, + { + url: urlViaCorsProxy(`https://www.movescount.com/moves/move${trackId}`), + options: { + responseType: 'binarystring', + isResponseSuccess: (xhr) => xhr.status === 200 || xhr.status === 403 || xhr.status === 404 + } + } + ] + } + + parseResponse(responses) { + const [trackResponse, pageResponse] = responses; + if (trackResponse.status === 403) { + return [{error: 'Movescount user disabled viewing this activity'}]; + } + let data; + let name = `Movescount move ${this.trackId}`; + try { + data = JSON.parse(trackResponse.responseBinaryText) + } catch (e) { + return [{name, error: 'UNSUPPORTED'}]; + } + const track = data.TrackPoints.map(trackPoint => ({ + lat: trackPoint.Latitude, + lng: trackPoint.Longitude + }) + ); + + const dom = (new DOMParser()).parseFromString(pageResponse.responseBinaryText, "text/html"); + const title = dom.querySelector('title').text.trim(); + name = title ? title : name; + + return [{ + name, + tracks: [track] + }]; + + } +} + +export {MovescountRoute, MovescountMove}; +\ No newline at end of file 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 flattenArray(await Promise.all(values.map(loadTracksFromJson))); + } + + 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,114 @@ +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(value) { + const errCorrupt = [{name: 'Track in url', error: 'CORRUPT'}]; + + const jsonString = urlSafeBase64.decode(value); + 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 one 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/lib/services/osm.js b/src/lib/leaflet.control.track-list/lib/services/osm.js @@ -0,0 +1,32 @@ +import urlViaCorsProxy from 'lib/CORSProxy'; +import BaseService from './baseService'; +import parseGpx from '../parsers/gpx'; + +class Osm extends BaseService { + urlRe = /^https?:\/\/www\.openstreetmap\.org\/user\/(?:.*)\/traces\/(\d+)/; + + getTrackId() { + const m = this.urlRe.exec(this.origUrl); + return m[1]; + } + + isOurUrl() { + return this.urlRe.test(this.origUrl); + } + + requestOptions() { + const trackId = this.getTrackId(); + return [{ + url: urlViaCorsProxy(`https://www.openstreetmap.org/trace/${trackId}/data`), + options: {responseType: 'binarystring'} + }] + } + + parseResponse(responses) { + const trackId = this.getTrackId(); + const response = responses[0]; + return parseGpx(response.responseBinaryText, `OSM track ${trackId}`, true); + } +} + +export default Osm; diff --git a/src/lib/leaflet.control.track-list/lib/services/simpleService.js b/src/lib/leaflet.control.track-list/lib/services/simpleService.js @@ -0,0 +1,23 @@ +import BaseService from './baseService'; +import parseGeoFile from '../parseGeoFile'; +import urlViaCorsProxy from 'lib/CORSProxy'; + +class SimpleService extends BaseService { + isOurUrl() { + return true; + } + + requestOptions() { + return [{ + url: urlViaCorsProxy(this.origUrl), + options: {responseType: 'binarystring'} + }]; + } + + parseResponse(responses) { + const response = responses[0]; + return parseGeoFile(this.nameFromUrl(response.responseURL), response.responseBinaryText); + } +} + +export default SimpleService; diff --git a/src/lib/leaflet.control.track-list/lib/services/strava.js b/src/lib/leaflet.control.track-list/lib/services/strava.js @@ -0,0 +1,80 @@ +import BaseService from './baseService'; +import urlViaCorsProxy from 'lib/CORSProxy'; +import {decode as utf8_decode} from 'utf8'; + +class Strava extends BaseService { + urlRe = /^https?:\/\/(?:.+\.)?strava\.com\/activities\/(\d+)/; + + isOurUrl() { + return this.urlRe.test(this.origUrl); + } + + requestOptions() { + const m = this.urlRe.exec(this.origUrl); + const trackId = this.trackId = m[1]; + return [ + { + url: urlViaCorsProxy(`https://www.strava.com/activities/${trackId}?hl=en-GB`), + options: { + responseType: 'binarystring', + isResponseSuccess: (xhr) => (xhr.status === 200 || xhr.status === 404) + } + }, + { + url: urlViaCorsProxy(`https://www.strava.com/stream/${trackId}?streams%5B%5D=latlng`), + options: { + responseType: 'binarystring', + isResponseSuccess: (xhr) => (xhr.status === 200 || xhr.status === 401) + } + }]; + } + + parseResponse(responses) { + let data; + const [pageResponse, trackResponse] = responses; + if (trackResponse.status === 401) { + return [{error: 'Strava user disabled viewing this track (track is private)'}]; + } + let name = `Strava ${this.trackId}`; + try { + data = JSON.parse(trackResponse.responseBinaryText); + } catch (e) { + return [{name, error: 'UNSUPPORTED'}]; + } + if (!data.latlng) { + return [{name, error: 'UNSUPPORTED'}]; + } + const tracks = [data.latlng.map((p) => ({lat: p[0],lng: p[1]}))]; + + try { + let name2; + const dom = (new DOMParser()).parseFromString(pageResponse.responseBinaryText, "text/html"); + let title = dom.querySelector('meta[property=og\\:title]').content; + title = utf8_decode(title); + // name and description + const m = title.match(/^(.+) - ([^-]+)/); + if (m) { + // reverse name and description + name2 = `${m[2]} ${m[1]}`; + title = dom.querySelector('title').text; + let date = title.match(/ (on \d{1,2} \w+ \d{4}) /)[1]; + if (date) { + name2 += ' ' + date; + } + } + name = name2; + } catch (e) {} + + + return [{ + name, + tracks + }]; + } +} + + + + + +export default Strava; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/services/urlEncode.js b/src/lib/leaflet.control.track-list/lib/services/urlEncode.js @@ -0,0 +1,5 @@ +function urlEncode(d) { + return Object.entries(d).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&'); +} + +export default urlEncode; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/services/yandex.js b/src/lib/leaflet.control.track-list/lib/services/yandex.js @@ -0,0 +1,102 @@ +import BaseService from './baseService'; + +class YandexRuler extends BaseService { + urlRe = /yandex\..+[?&]rl=([^&]+)/; + + isOurUrl() { + return this.urlRe.test(this.origUrl); + } + + requestOptions() { + return []; + } + + parseResponse() { + let last_lat = 0; + let last_lng = 0; + let error; + const points = []; + let s = this.urlRe.exec(this.origUrl)[1]; + s = s.replace(/%2C/ig, ','); + const points_str = s.split('~'); + for (let i = 0; i < points_str.length; i++) { + let point = points_str[i].split(','); + let lng = parseFloat(point[0]); + let lat = parseFloat(point[1]); + if (isNaN(lat) || isNaN(lng)) { + error = 'CORRUPT'; + break; + } + last_lng += lng; + last_lat += lat; + points.push({lat: last_lat, lng: last_lng}); + } + return [{ + error: error, + tracks: [points], + name: 'Yandex ruler' + }]; + } +} + + + +// function parseYandexMap(txt) { +// var start_tag = '<script id="vpage" type="application/json">'; +// var json_start = txt.indexOf(start_tag); +// if (json_start === -1) { +// return null; +// } +// json_start += start_tag.length; +// var json_end = txt.indexOf('</script>', json_start); +// if (json_end === -1) { +// return null; +// } +// var map_data = txt.substring(json_start, json_end); +// map_data = JSON.parse(map_data); +// console.log(map_data); +// if (!('request' in map_data)) { +// return null; +// } +// var name = 'YandexMap'; +// var segments = []; +// var error; +// if (map_data.vpage && map_data.vpage.data && map_data.vpage.data.objects && map_data.vpage.data.objects.length) { +// var mapName = ('' + (map_data.vpage.data.name || '')).trim(); +// if (mapName.length > 3) { +// name = ''; +// } else if (mapName.length) { +// name += ': '; +// } +// name += fileutils.decodeUTF8(mapName); +// map_data.vpage.data.objects.forEach(function(obj){ +// if (obj.pts && obj.pts.length) { +// var segment = []; +// for (var i=0; i< obj.pts.length; i++) { +// var pt = obj.pts[i]; +// var lng = parseFloat(pt[0]); +// var lat = parseFloat(pt[1]); +// if (isNaN(lat) || isNaN(lng)) { +// error = 'CORRUPT'; +// break; +// } +// segment.push({lat: lat, lng:lng}); +// } +// if (segment.length) { +// segments.push(segment); +// } +// } +// }); +// } +// if (map_data.request.args && map_data.request.args.rl) { +// var res = parseYandexRulerString(map_data.request.args.rl); +// error = error || res.error; +// if (res.points && res.points.length) { +// segments.push(res.points); +// } +// } +// return [{name: name, error: error, tracks: segments}]; +// } + + +export {YandexRuler}; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/strava.js b/src/lib/leaflet.control.track-list/lib/strava.js @@ -1,81 +0,0 @@ -import urlViaCorsProxy from 'lib/CORSProxy'; -import {decode as utf8_decode} from 'utf8'; - -const re = /^https:\/\/www\.strava\.com\/activities\/(\d+)/; - -function isStravaUrl(url) { - return re.test(url); -} - -function stravaRequestOptions(url) { - const m = re.exec(url); - if (!m) { - throw new Error('Invalid strava url'); - } - const trackId = m[1]; - const requestOptions = [ - { - url: urlViaCorsProxy(`https://www.strava.com/activities/${trackId}?hl=en-GB`), - options: {responseType: 'binarystring'} - }, - { - url: urlViaCorsProxy(`https://www.strava.com/stream/${trackId}?streams%5B%5D=latlng`), - options: {responseType: 'binarystring'} - }]; - return { - requestOptions, - extra: {trackId} - } -} - - -function stravaParser(name, responses, extra) { - if (responses.length !== 2) { - throw new Error(`Invalid responses array length ${responses.length}`); - } - - - let data; - try { - data = JSON.parse(responses[1].responseBinaryText); - } catch (e) { - return [{name: name, error: 'UNSUPPORTED'}]; - } - if (!data.latlng) { - return [{name: name, error: 'UNSUPPORTED'}]; - } - - name = `Strava ${extra.trackId}`; - try { - const dom = (new DOMParser()).parseFromString(responses[0].responseBinaryText, "text/html"); - let title = dom.querySelector('meta[property=og\\:title]').content; - title = utf8_decode(title); - // name and description - const m = title.match(/^(.+) - ([^-]+)/); - if (m) { - // reverse name and description - name = `${m[2]} ${m[1]}`; - title = dom.querySelector('title').text; - let date = title.match(/ (on \d{1,2} \w+ \d{4}) /)[1]; - if (date) { - name += ' ' + date; - } - } - - } catch (e) {} - - const geodata = { - name: name || 'Strava', - tracks: [data.latlng.map((p) => { - return { - lat: p[0], - lng: p[1] - } - } - )] - }; - return [geodata]; -} - - -export {isStravaUrl, stravaRequestOptions, stravaParser} -\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/track-list.css b/src/lib/leaflet.control.track-list/track-list.css @@ -269,4 +269,13 @@ .leaflet-control-tracklist .edit { background-color: rgb(217, 232, 255) !important; +} + +.leaflet-control-tracklist .formats-hint-more { + background-color: hsl(100, 0%, 92%); + padding: 2px 6px; + display: inline-block; + line-height: 10px; + border-radius: 5px; + color: hsl(100, 0%, 40%); } \ 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/geo_file_formats'; -import logging from 'lib/logging'; -import {parseNktkSequence} from './lib/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 @@ -4,8 +4,8 @@ import Contextmenu from 'lib/contextmenu'; import 'lib/knockout.component.progress/progress'; import './track-list.css'; import {selectFiles, readFiles} from 'lib/file-read'; -import {parseGeoFile, loadFromUrl} from './lib/geo_file_formats'; - +import parseGeoFile from './lib/parseGeoFile'; +import loadFromUrl from './lib/loadFromUrl'; import geoExporters from './lib/geo_file_exporters'; import copyToClipboard from 'lib/clipboardCopy'; import {saveAs} from 'vendored/github.com/eligrey/FileSaver'; @@ -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,14 +46,14 @@ 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() { L.Control.prototype.initialize.call(this); this.tracks = ko.observableArray(); this.url = ko.observable(''); - this.readingFiles = ko.observable(false); + this.readingFiles = ko.observable(0); this.readProgressRange = ko.observable(); this.readProgressDone = ko.observable(); this._lastTrackColor = 0; @@ -70,8 +73,10 @@ L.Control.TrackList = L.Control.extend({ title="Load, edit and save tracks"></div> <div class="leaflet-control-content"> <div class="header"> - <div class="hint"> - gpx kml Ozi zip YandexMaps GPSies Strava endomondo + <div class="hint" + title="gpx kml Ozi zip YandexMaps GPSies Strava GPSLib Endomondo Movescount OSM"> + gpx kml Ozi zip YandexMaps GPSies Strava + <span class="formats-hint-more">&hellip;</span> </div> <div class="button-minimize" data-bind="click: setMinimized"></div> </div> @@ -214,9 +219,7 @@ L.Control.TrackList = L.Control.extend({ }, loadFilesFromFilesObject: function(files) { - this.readProgressDone(undefined); - this.readProgressRange(1); - this.readingFiles(true); + this.readingFiles(this.readingFiles() + 1); readFiles(files).then(function(fileDataArray) { const geodataArray = []; @@ -229,7 +232,7 @@ L.Control.TrackList = L.Control.extend({ content: fileData.data.length <= 7500 ? btoa(fileData.data) : null }); } - this.readingFiles(false); + this.readingFiles(this.readingFiles() - 1); this.addTracksFromGeodataArray(geodataArray, debugFileData); }.bind(this)); @@ -246,19 +249,30 @@ L.Control.TrackList = L.Control.extend({ return; } - this.readingFiles(true); - this.readProgressDone(undefined); - this.readProgressRange(1); + this.readingFiles(this.readingFiles() + 1); logging.captureBreadcrumb({message: 'load track from url', data: {url: url}}); loadFromUrl(url) .then((geodata) => { this.addTracksFromGeodataArray(geodata); - this.readingFiles(false) + this.readingFiles(this.readingFiles() - 1); }); this.url(''); }, + whenLoadDone: function(cb) { + if (this.readingFiles() === 0) { + cb(); + return; + } + const subscription = this.readingFiles.subscribe((value) =>{ + if (value === 0) { + subscription.dispose(); + cb(); + } + }); + }, + addTracksFromGeodataArray: function(geodata_array, debugData) { let hasData = false; var messages = []; @@ -367,10 +381,33 @@ L.Control.TrackList = L.Control.extend({ }, setViewToTrack: function(track) { - var lines = this.getTrackPolylines(track); - var points = this.getTrackPoints(track); + this.setViewToBounds(this.getTrackBounds(track)); + }, + + setViewToAllTracks: function(immediate) { + const bounds = L.latLngBounds([]); + for (let track of this.tracks()) { + bounds.extend(this.getTrackBounds(track)); + } + this.setViewToBounds(bounds, immediate); + }, + + setViewToBounds: function(bounds, immediate) { + if (bounds && bounds.isValid()) { + bounds = wrapLatLngBoundsToTarget(bounds, this.map.getCenter()); + if (L.Browser.mobile || immediate) { + this.map.fitBounds(bounds, {maxZoom: 16}); + } else { + this.map.flyToBounds(bounds, {maxZoom: 16}); + } + } + }, + + getTrackBounds: function(track) { + const lines = this.getTrackPolylines(track); + const points = this.getTrackPoints(track); + const bounds = L.latLngBounds([]); if (lines.length || points.length) { - var bounds = L.latLngBounds([]); lines.forEach((l) => { if (l.getLatLngs().length > 1) { bounds.extend(wrapLatLngBoundsToTarget(l.getBounds(), bounds)); @@ -381,16 +418,8 @@ L.Control.TrackList = L.Control.extend({ bounds.extend(wrapLatLngToTarget(p.latlng, bounds)); } ); - if (bounds.isValid()) { - bounds = wrapLatLngBoundsToTarget(bounds, this.map.getCenter()); - if (L.Browser.mobile) { - this.map.fitBounds(bounds); - } else { - this.map.flyToBounds(bounds); - } - } - } + return bounds; }, attachColorSelector: function(track) { @@ -1156,3 +1185,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 {parseGeoFile} from './lib/geo_file_formats'; +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 = parseGeoFile('', 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}; diff --git a/src/lib/leaflet.layer.geocaching-su/index.js b/src/lib/leaflet.layer.geocaching-su/index.js @@ -63,12 +63,12 @@ const GeocachingSu = L.Layer.CanvasMarkers.extend({ return zoom >= 10 ? marker._label : null; }; - const markers = data.map(([label, url, lat, lng]) => { + const markers = data.map(([label, cacheId, lat, lng]) => { return { latlng: {lat, lng}, _label: label, label: getLabel, - icon, url + icon, cacheId } }); this.addMarkers(markers); @@ -77,7 +77,8 @@ const GeocachingSu = L.Layer.CanvasMarkers.extend({ }, openCachePage: function(e) { - openPopupWindow(e.marker.url, 900, 'geocaching_su'); + const url = `https://geocaching.su/?pn=101&cid=${e.marker.cacheId}` + openPopupWindow(url, 900, 'geocaching_su'); } }); diff --git a/src/lib/notifications/style.css b/src/lib/notifications/style.css @@ -4,4 +4,8 @@ .alertify .alert>*, .alertify .dialog>* { overflow-wrap: break-word; -} -\ No newline at end of file +} + +.alertify .alert .msg, .alertify .dialog .msg { + white-space: pre-line; +}