nakarte

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

commit 646149ea30acf746535bcee513c0ace1e4826688
parent 3d0f057e3aa3120b0ce903d38e311d93290a0a2f
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Sat, 24 Nov 2018 17:03:15 +0100

[tracks] rafctor loading

* moved parsers and services to separate directories
* services are now classes
* improved handling private workouts in endomondo

Diffstat:
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 | 103-------------------------------------------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/gpsies.js | 50--------------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/gpslib.js | 31-------------------------------
Dsrc/lib/leaflet.control.track-list/lib/gpx.js | 129-------------------------------------------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/kml.js | 163-------------------------------------------------------------------------------
Asrc/lib/leaflet.control.track-list/lib/loadFromUrl.js | 12++++++++++++
Dsrc/lib/leaflet.control.track-list/lib/nktk.js | 403-------------------------------------------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/ozi.js | 117-------------------------------------------------------------------------------
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++++++++++++++
Rsrc/lib/leaflet.control.track-list/lib/jsInflate.js -> src/lib/leaflet.control.track-list/lib/parsers/jsInflate.js | 0
Asrc/lib/leaflet.control.track-list/lib/parsers/kml.js | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/parsers/nktk.js | 327+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 | 20++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/nakarte.js | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/simpleService.js | 23+++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/services/strava.js | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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-------------------------------------------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/utils.js | 47-----------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/yandex.js | 93-------------------------------------------------------------------------------
Dsrc/lib/leaflet.control.track-list/lib/zip.js | 44--------------------------------------------
Msrc/lib/leaflet.control.track-list/track-list.hash-state.js | 4++--
Msrc/lib/leaflet.control.track-list/track-list.js | 4++--
Msrc/lib/leaflet.control.track-list/track-list.localstorage.js | 2+-
40 files changed, 1393 insertions(+), 1334 deletions(-)

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,103 +0,0 @@ -import {fetch} from 'lib/xhr-promise'; -import urlViaCorsProxy from 'lib/CORSProxy'; -import {isGpsiesUrl, gpsiesRequestOptions, gpsiesParser} from './gpsies'; -import {isGpslibUrl, gpslibRequestOptions, gpslibParser} from './gpslib'; -import {isStravaUrl, stravaRequestOptions, stravaParser} from './strava'; -import {isEndomondoUrl, endomondoRequestOptions, endomondoParser} from './endomondo'; -import {parseTrackUrl, parseNakarteUrl, isNakarteLinkUrl, nakarteLinkRequestOptions, nakarteLinkParser} from './nktk'; -import {stringToArrayBuffer, arrayBufferToString} from 'lib/binary-strings'; -import parseGpx from './gpx'; -import {parseKmz, parseKml} from './kml'; -import parseZip from './zip'; -import {parseYandexRulerUrl} from './yandex'; -import {parseOziPlt, parseOziRte, parseOziWpt} from './ozi'; - -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 (isGpslibUrl(url)) { - requestOptionsGetter = gpslibRequestOptions; - parser = gpslibParser; - } 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/gpslib.js b/src/lib/leaflet.control.track-list/lib/gpslib.js @@ -1,31 +0,0 @@ -import urlViaCorsProxy from 'lib/CORSProxy'; -import {parseGpx} from './geo_file_formats' - -const re = /^https?:\/\/(?:.*\.)?gpslib\.[^.]+\/tracks\/info\/(\d+)/; - -function isGpslibUrl(url) { - return re.test(url); -} - -function gpslibRequestOptions(url) { - const m = re.exec(url); - if (!m) { - throw new Error('Invalid gpslib url'); - } - const trackId = m[1]; - const requestOptions = [ - { - url: urlViaCorsProxy(`https://www.gpslib.ru/tracks/download/${trackId}.gpx`), - options: {responseType: 'binarystring'} - }]; - return {requestOptions}; -} - -function gpslibParser(name, responses) { - if (responses.length !== 1) { - throw new Error(`Invalid responses array length ${responses.length}`); - } - return parseGpx(responses[0].responseBinaryText, name, true); -} - -export {gpslibRequestOptions, isGpslibUrl, gpslibParser} diff --git a/src/lib/leaflet.control.track-list/lib/gpx.js b/src/lib/leaflet.control.track-list/lib/gpx.js @@ -1,128 +0,0 @@ -import {decode as utf8_decode} from 'utf8'; -import {xmlGetNodeText} from './utils'; -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/kml.js b/src/lib/leaflet.control.track-list/lib/kml.js @@ -1,162 +0,0 @@ -import {decode as utf8_decode} from 'utf8'; -import {xmlGetNodeText} from './utils'; -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/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,402 +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); -} - -function parseTrackUrl(s) { - var i = s.indexOf('track://'); - if (i === -1) { - return null; - } - return parseTrackUrlData(s.substring(i + 8)); -} - -export {saveNktk, parseTrackUrlData, parseNakarteUrl, isNakarteLinkUrl, nakarteLinkRequestOptions, - nakarteLinkParser, parseNktkSequence, parseTrackUrl}; -\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/ozi.js b/src/lib/leaflet.control.track-list/lib/ozi.js @@ -1,116 +0,0 @@ -import {decodeCP1251} from './utils'; -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/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/jsInflate.js b/src/lib/leaflet.control.track-list/lib/parsers/jsInflate.js 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,326 @@ +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) { + 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; +} + +export {saveNktk, parseNktkSequence, parseTrackUrlData}; +\ 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,75 @@ +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: 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 + } + } + ); + if (track.length === 0) { + return [{error: 'Endomondo user disabled viewing this workout track'}]; + } + + const author = data.author && data.author.name ? ` ${data.author.name}` : ''; + const date = data.local_start_time.split('T')[0]; + const dist = `${data.distance.toFixed(1)} km`; + let trackName = `${date}, ${dist}${author}: ${data.title}`; + console.log(data, track); + 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 = 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, this.nameFromUrl(response.responseURL), 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,19 @@ +import SimpleService from './simpleService' +import Endomondo from './endomondo'; +import Gpsies from './gpsies'; +import Gpslib from './gpslib'; +import Strava from './strava'; +import {YandexRuler} from './yandex'; +import {NakarteTrack, NakarteNktk, NakarteNktl} from './nakarte'; + +export default [ + YandexRuler, + NakarteTrack, + NakarteNktk, + NakarteNktl, + Endomondo, + Gpsies, + Gpslib, + Strava, + SimpleService +] +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/services/nakarte.js b/src/lib/leaflet.control.track-list/lib/services/nakarte.js @@ -0,0 +1,72 @@ +import BaseService from './baseService'; +import {parseNktkSequence, parseTrackUrlData} from '../parsers/nktk'; +import config from 'config'; + +class NakarteTrack extends BaseService { + isOurUrl() { + return this.origUrl.indexOf('track://') > -1; + } + + requestOptions() { + return []; + } + + parseResponse() { + const i = this.origUrl.indexOf('track://'); + return parseTrackUrlData(this.origUrl.substring(i + 8)); + } +} + +class NakarteNktk extends BaseService { + constructor(url) { + super(url); + this._data = null; + let i = url.indexOf('#'); + if (i === -1) { + return; + } + i = url.indexOf('nktk=', i + 1); + if (i === -1) { + return; + } + this._data = url.substring(i + 5); + + } + + isOurUrl() { + return !!this._data; + } + + requestOptions() { + return []; + } + + parseResponse() { + return parseNktkSequence(this._data); + } + +} + +class NakarteNktl extends BaseService { + urlRe = /#.*nktl=([A-Za-z0-9_-]+)/; + + isOurUrl() { + return this.urlRe.test(this.origUrl); + } + + requestOptions() { + const m = this.urlRe.exec(this.origUrl); + const trackId = m[1]; + return [{ + url: (`${config.tracksStorageServer}/track/${trackId}`), + options: {responseType: 'binarystring'} + }]; + } + + parseResponse(responses) { + return parseNktkSequence(responses[0].responseBinaryText); + } +} + + +export {NakarteTrack, NakarteNktk, NakarteNktl}; diff --git a/src/lib/leaflet.control.track-list/lib/services/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,73 @@ +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'} + }, + { + url: urlViaCorsProxy(`https://www.strava.com/stream/${trackId}?streams%5B%5D=latlng`), + options: {responseType: 'binarystring'} + }]; + } + + parseResponse(responses) { + let data; + const pageResponse = responses[0]; + const trackResponse = responses[1]; + try { + data = JSON.parse(trackResponse.responseBinaryText); + } catch (e) { + return [{name: name, error: 'UNSUPPORTED'}]; + } + if (!data.latlng) { + return [{name: name, error: 'UNSUPPORTED'}]; + } + + const tracks = [data.latlng.map((p) => ({lat: p[0],lng: p[1]}))]; + + let name = `Strava ${this.trackId}`; + try { + let name2; + 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 + 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: 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/lib/utils.js b/src/lib/leaflet.control.track-list/lib/utils.js @@ -1,47 +0,0 @@ -function xmlGetNodeText(node) { - if (node) { - return Array.prototype.slice.call(node.childNodes) - .map(function(node) { - return node.nodeValue; - } - ) - .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 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 {xmlGetNodeText, decode866, decodeCP1251}; - diff --git a/src/lib/leaflet.control.track-list/lib/yandex.js b/src/lib/leaflet.control.track-list/lib/yandex.js @@ -1,92 +0,0 @@ -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 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 {parseYandexRulerUrl} -\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/lib/zip.js b/src/lib/leaflet.control.track-list/lib/zip.js @@ -1,43 +0,0 @@ -import JSUnzip from 'vendored/github.com/augustl/js-unzip/js-unzip'; -import jsInflate from './jsInflate'; -import {decode866} from './utils'; -import {parseGeoFile} from './geo_file_formats'; - -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/track-list.hash-state.js b/src/lib/leaflet.control.track-list/track-list.hash-state.js @@ -1,7 +1,7 @@ import L from 'leaflet'; -import {loadFromUrl} from './lib/geo_file_formats'; +import loadFromUrl from './lib/loadFromUrl'; import logging from 'lib/logging'; -import {parseNktkSequence} from './lib/nktk'; +import {parseNktkSequence} from './lib/parsers/nktk'; L.Control.TrackList.include({ 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'; 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 parseGeoFile from './lib/parseGeoFile'; import safeLocalStorage from 'lib/safe-localstorage'; import logging from 'lib/logging';