nakarte

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

commit 9a950acfd006215c9e00738b590fa6ca6027a4af
parent 2be83846917df8002739f99395c47d4f6d40184b
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Sun, 27 Nov 2016 02:52:50 +0300

tradded tracks control

Diffstat:
Mconfig/webpack.config.dev.js | 15+++++++++++++--
Mconfig/webpack.config.prod.js | 14+++++++++++++-
Mpackage.json | 4+++-
Msrc/App.js | 12++++++++++--
Msrc/config.js | 3++-
Asrc/lib/CORSProxy/proxy.js | 5+++++
Asrc/lib/file-read/file-read.js | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/images/add_line.png | 0
Asrc/lib/leaflet.control.track-list/images/add_point.png | 0
Asrc/lib/leaflet.control.track-list/images/arrow_bottom.png | 0
Asrc/lib/leaflet.control.track-list/images/folder_open.png | 0
Asrc/lib/leaflet.control.track-list/images/marker_1.png | 0
Asrc/lib/leaflet.control.track-list/images/marker_2.png | 0
Asrc/lib/leaflet.control.track-list/images/marker_3.png | 0
Asrc/lib/leaflet.control.track-list/images/marker_4.png | 0
Asrc/lib/leaflet.control.track-list/images/marker_5.png | 0
Asrc/lib/leaflet.control.track-list/images/marker_6.png | 0
Asrc/lib/leaflet.control.track-list/images/menu.png | 0
Asrc/lib/leaflet.control.track-list/images/plus.png | 0
Asrc/lib/leaflet.control.track-list/lib/geo_file_exporters.js | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/lib/geo_file_formats.js | 652+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/track-list.css | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.track-list/track-list.js | 1012+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.lineutil.simplifyLatLngs/simplify.js | 22++++++++++++++++++++++
Asrc/lib/leaflet.polyline-edit/edit_line.css | 34++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.polyline-edit/edit_line.js | 349+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.polyline-measure/measured_line.css | 26++++++++++++++++++++++++++
Asrc/lib/leaflet.polyline-measure/measured_line.js | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/xhr-promise/xhr-promise.js | 8+++++++-
Asrc/vendored/github.com/augustl/js-unzip/js-unzip.js | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/vendored/github.com/dankogai/js-deflate/rawinflate.js | 755+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
31 files changed, 3734 insertions(+), 8 deletions(-)

diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js @@ -33,6 +33,7 @@ module.exports = { // This means they will be the "root" imports that are included in JS bundle. // The first two entry points enable "hot" CSS and auto-refreshes for JS. entry: [ + 'babel-polyfill', // Include an alternative client for WebpackDevServer. A client's job is to // connect to WebpackDevServer by a socket and get notified about changes. // When you save a file, the client will either apply hot updates (in case @@ -83,7 +84,7 @@ module.exports = { 'react-native': 'react-native-web' } }, - + module: { // First, run the linter. // It's important to do this before Babel processes the JS. @@ -92,16 +93,26 @@ module.exports = { test: /\.(js|jsx)$/, loader: 'eslint', include: paths.appSrc, + exclude: /augustl\/js-unzip|dankogai\/js-deflate/, } ], loaders: [ + { + test: /\.js$/, + include: [ + paths.appSrc + '/vendored/github.com/augustl/js-unzip', + paths.appSrc + '/vendored/github.com/dankogai/js-deflate' + ], + loader: 'legacy' + }, // Process JS with Babel. { test: /\.(js|jsx)$/, include: paths.appSrc, + exclude: /augustl\/js-unzip|dankogai\/js-deflate/, loader: 'babel', query: { - + // This is a feature of `babel-loader` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/react-scripts/ // directory for faster rebuilds. We use findCacheDir() because of: diff --git a/config/webpack.config.prod.js b/config/webpack.config.prod.js @@ -54,6 +54,7 @@ module.exports = { devtool: 'source-map', // In production, we only want to load the polyfills and the app code. entry: [ + 'babel-polyfill', require.resolve('./polyfills'), paths.appIndexJs ], @@ -95,13 +96,24 @@ module.exports = { { test: /\.(js|jsx)$/, loader: 'eslint', - include: paths.appSrc + include: paths.appSrc, + exclude: /augustl\/js-unzip|dankogai\/js-deflate/, } ], loaders: [ + { + test: /\.js$/, + include: [ + paths.appSrc + '/vendored/github.com/augustl/js-unzip', + paths.appSrc + '/vendored/github.com/dankogai/js-deflate' + ], + loader: 'legacy' + }, + // Process JS with Babel. { test: /\.(js|jsx)$/, + exclude: /augustl\/js-unzip|dankogai\/js-deflate/, include: paths.appSrc, loader: 'babel', diff --git a/package.json b/package.json @@ -33,6 +33,7 @@ "http-proxy-middleware": "0.17.2", "jest": "16.0.2", "json-loader": "0.5.4", + "legacy-loader": "0.0.2", "object-assign": "4.1.0", "path-exists": "2.1.0", "postcss-loader": "1.0.0", @@ -56,7 +57,8 @@ "knockout": "^3.4.0", "leaflet": "^1.0.1", "load-script": "^1.0.0", - "rbush": "^2.0.1" + "rbush": "^2.0.1", + "utf8": "^2.1.2" }, "scripts": { "start": "node scripts/start.js", diff --git a/src/App.js b/src/App.js @@ -13,6 +13,7 @@ import 'lib/leaflet.hashState/Leaflet.Control.Layers'; import fixAnimationBug from 'lib/leaflet.fixAnimationBug/leaflet.fixAnimationBug' import './adaptive.css'; import 'lib/leaflet.control.panoramas/panoramas'; +import 'lib/leaflet.control.track-list/track-list'; function autoSizeControl(map, control) { // для контрола Layers есть аналогичная функция при разворачивании из кнопки. @@ -68,12 +69,19 @@ function setUp() { new L.Control.PrintPages({position: 'bottomleft'}).addTo(map); new L.Control.Coordinates().addTo(map); - const panoramas = new L.Control.Panoramas(document.getElementById('street-view')).addTo(map); + new L.Control.Panoramas(document.getElementById('street-view')) + .addTo(map) + .enableHashState('n'); + const tracksControl = new L.Control.TrackList() + .addTo(map); + - panoramas.enableHashState('n'); map.on('resize', autoSizeControl.bind(null, map, layersControl)); autoSizeControl(map, layersControl); + map.on('resize', autoSizeControl.bind(null, map, tracksControl)); + autoSizeControl(map, tracksControl); + raiseControlsOnMouse(); } diff --git a/src/config.js b/src/config.js @@ -2,5 +2,6 @@ export default { email: 'nakarte@nakarte.tk', googleApiUrl: 'https://maps.googleapis.com/maps/api/js?v=3', bingKey: 'AhZy06XFi8uAADPQvWNyVseFx4NHYAOH-7OTMKDPctGtYo86kMfx2T0zUrF5AAaM', - westraDataBaseUrl: 'http://nakarte.tk/westraPasses/' + westraDataBaseUrl: 'http://nakarte.tk/westraPasses/', + CORSProxyUrl: 'http://proxy.nakarte.tk/' } diff --git a/src/lib/CORSProxy/proxy.js b/src/lib/CORSProxy/proxy.js @@ -0,0 +1,5 @@ +import config from 'config'; + +export default function urlViaCorsProxy(url) { + return config.CORSProxyUrl + url.replace(/^(https?):\/\//, '$1/'); +}; diff --git a/src/lib/file-read/file-read.js b/src/lib/file-read/file-read.js @@ -0,0 +1,52 @@ +function intArrayToString(arr) { + var s = []; + var chunk; + for (var i = 0; i < arr.length; i+=4096) { + chunk = arr.subarray(i, i + 4096); + chunk = String.fromCharCode.apply(null,chunk); + s.push(chunk); + } + return s.join(''); +} + +function arrayBufferToString(arBuf) { + var arr = new Uint8Array(arBuf); + return intArrayToString(arr); +} + +function selectFiles(multiple=false) { + var fileInput = document.createElement('input'); + document.body.appendChild(fileInput); + fileInput.type = 'file'; + fileInput.multiple = !!multiple; + fileInput.style.left = '-100000px'; + const result = new Promise(function(resolve) { + fileInput.onchange = function() { + document.body.removeChild(fileInput); + resolve(fileInput.files); + }; + }); + setTimeout(fileInput.click.bind(fileInput), 0); + return result; +} + +function readFile(file) { + return new Promise(function(resolve) { + var reader = new FileReader(); + reader.onload = function (e) { + resolve({ + data: arrayBufferToString(e.target.result), + filename: file.name + }); + }; + reader.readAsArrayBuffer(file); + }); +} + +function readFiles(files) { + files = Array.prototype.slice.apply(files); + return Promise.all(files.map(readFile)); +} + + +export {selectFiles, readFile, readFiles}; +\ No newline at end of file diff --git a/src/lib/leaflet.control.track-list/images/add_line.png b/src/lib/leaflet.control.track-list/images/add_line.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/add_point.png b/src/lib/leaflet.control.track-list/images/add_point.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/arrow_bottom.png b/src/lib/leaflet.control.track-list/images/arrow_bottom.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/folder_open.png b/src/lib/leaflet.control.track-list/images/folder_open.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/marker_1.png b/src/lib/leaflet.control.track-list/images/marker_1.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/marker_2.png b/src/lib/leaflet.control.track-list/images/marker_2.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/marker_3.png b/src/lib/leaflet.control.track-list/images/marker_3.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/marker_4.png b/src/lib/leaflet.control.track-list/images/marker_4.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/marker_5.png b/src/lib/leaflet.control.track-list/images/marker_5.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/marker_6.png b/src/lib/leaflet.control.track-list/images/marker_6.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/menu.png b/src/lib/leaflet.control.track-list/images/menu.png Binary files differ. diff --git a/src/lib/leaflet.control.track-list/images/plus.png b/src/lib/leaflet.control.track-list/images/plus.png Binary files differ. 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 @@ -0,0 +1,214 @@ +import utf8 from 'utf8'; +import escapeHtml from 'escape-html'; + +function saveGpx(segments, name, points) { + var gpx = [], + x, y; + + gpx.push('<?xml version="1.0" encoding="UTF-8" standalone="no" ?>'); + gpx.push( + '<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="http://nakarte.tk" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">' + ); + if (segments.length) { + name = name || 'Track'; + name = escapeHtml(name); + name = utf8.encode(name); + gpx.push('\t<trk>'); + gpx.push('\t\t<name>' + name + '</name>'); + + segments.forEach(function(points) { + if (points.length > 1) { + gpx.push('\t\t<trkseg>'); + points.forEach(function(p) { + x = p.lng.toFixed(6); + y = p.lat.toFixed(6); + gpx.push('\t\t\t<trkpt lat="' + y + '" lon="' + x + '"></trkpt>'); + } + ); + gpx.push('\t\t</trkseg>'); + } + } + ); + gpx.push('\t</trk>'); + } + points.forEach(function(marker) { + var label = marker.label; + label = escapeHtml(label); + label = utf8.encode(label); + gpx.push('\t<wpt lat="' + marker.latlng.lat.toFixed(6) + '" lon="' + marker.latlng.lng.toFixed(6) + '">'); + gpx.push('\t\t<name>' + label + '</name>'); + gpx.push('\t</wpt>'); + } + ); + gpx.push('</gpx>'); + gpx = gpx.join('\n'); + return gpx; +} + + +function saveKml(segments, name, points) { + var kml = [], + x, y; + + name = name || 'Track'; + name = escapeHtml(name); + name = utf8.encode(name); + + kml.push('<?xml version="1.0" encoding="UTF-8"?>'); + kml.push('<kml xmlns="http://earth.google.com/kml/2.2">'); + kml.push('\t<Document>'); + kml.push('\t\t<name>' + name + '</name>'); + + segments.forEach(function(points, i) { + if (points.length > 1) { + kml.push('\t\t<Placemark>', + '\t\t\t<name>Line ' + (i + 1) + '</name>', + '\t\t\t<LineString>', + '\t\t\t\t<tessellate>1</tessellate>', + '\t\t\t\t<coordinates>' + ); + points.forEach(function(p) { + x = p.lng.toFixed(6); + y = p.lat.toFixed(6); + kml.push('\t\t\t\t\t' + x + ',' + y); + } + ); + kml.push('\t\t\t\t</coordinates>', + '\t\t\t</LineString>', + '\t\t</Placemark>' + ); + } + } + ); + points.forEach(function(marker) { + var label = marker.label; + label = escapeHtml(label); + label = utf8.encode(label); + var coordinates = marker.latlng.lng.toFixed(6) + ',' + marker.latlng.lat.toFixed(6) + ',0' + + kml.push('\t\t<Placemark>'); + kml.push('\t\t\t<name>' + label + '</name>'); + kml.push('\t\t\t<Point>'); + kml.push('\t\t\t\t<coordinates>' + coordinates + '</coordinates>'); + kml.push('\t\t\t</Point>'); + kml.push('\t\t</Placemark>'); + } + ); + + kml.push('\t</Document>'); + kml.push('\t</kml>'); + + kml = kml.join('\n'); + return kml; +} + +function packNumber(n) { + var bytes = []; + if (n >= -64 && n <= 63) { + n += 64; + bytes.push(n); + } else if (n >= -8192 && n <= 8191) { + n += 8192; + bytes.push((n & 0x7f) | 0x80); + bytes.push(n >> 7); + /* } else if (n >= -2097152 && n <= 2097151) { + n += 2097152; + bytes.push((n & 0x7f) | 0x80); + bytes.push(((n >> 7) & 0x7f) | 0x80); + bytes.push(n >> 14); + */ + } else if (n >= -1048576 && n <= 1048575) { + n += 1048576; + bytes.push((n & 0x7f) | 0x80); + bytes.push(((n >> 7) & 0x7f) | 0x80); + bytes.push(n >> 14); + } else if (n >= -268435456 && n <= 268435455) { + n += 268435456; + bytes.push((n & 0x7f) | 0x80); + bytes.push(((n >> 7) & 0x7f) | 0x80); + bytes.push(((n >> 14) & 0x7f) | 0x80); + bytes.push(n >> 21); + } else { + throw new Error('Number ' + n + ' too big to pack in 29 bits'); + } + return String.fromCharCode.apply(null, bytes); +} + + +function encodeUrlSafeBase64(s) { + return (btoa(s) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, '') + ); +} + +function saveToString(segments, name, color, measureTicksShown, wayPoints) { + var stringified = []; + stringified.push(packNumber(2)); // version + name = utf8.encode(name); + stringified.push(packNumber(name.length)); + stringified.push(name); + + var arcUnit = ((1 << 24) - 1) / 360; + segments = segments.filter(function(segment) { + return segment.length > 1; + } + ); + + stringified.push(packNumber(segments.length)); + segments.forEach(function(points) { + var lastX = 0, + lastY = 0, + x, y, + deltaX, deltaY, + p; + stringified.push(packNumber(points.length)); + for (var i = 0, len = points.length; i < len; i++) { + p = points[i]; + x = Math.round(p.lng * arcUnit); + y = Math.round(p.lat * arcUnit); + deltaX = x - lastX; + deltaY = y - lastY; + stringified.push(packNumber(deltaX)); + stringified.push(packNumber(deltaY)); + lastX = x; + lastY = y; + } + } + ); + stringified.push(packNumber(+color || 0)); + stringified.push(packNumber(measureTicksShown ? 1 : 0)); + + stringified.push(packNumber(wayPoints.length)); + if (wayPoints.length) { + var midX = 0, midY = 0; + wayPoints.forEach(function(p) { + midX += p.latlng.lng; + midY += p.latlng.lat; + } + ); + midX = Math.round(midX * arcUnit / wayPoints.length); + midY = Math.round(midY * arcUnit / wayPoints.length); + stringified.push(packNumber(midX)); + stringified.push(packNumber(midY)); + wayPoints.forEach(function(p) { + var deltaX = Math.round(p.latlng.lng * arcUnit) - midX, + deltaY = Math.round(p.latlng.lat * arcUnit) - midY, + symbol = 1, + name = utf8.encode(p.label); + stringified.push(packNumber(name.length)); + stringified.push(name); + stringified.push(packNumber(symbol)); + stringified.push(packNumber(deltaX)); + stringified.push(packNumber(deltaY)); + } + ); + } + + return encodeUrlSafeBase64(stringified.join('')); +} + + +export default {saveGpx, saveKml,saveToString}; + 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 @@ -0,0 +1,652 @@ +import JSUnzip from 'vendored/github.com/augustl/js-unzip/js-unzip'; +import RawDeflate from 'vendored/github.com/dankogai/js-deflate/rawinflate'; + +import {decode as utf8_decode} from 'utf8'; + +function xmlGetNodeText(node) { + if (node) { + return Array.prototype.slice.call(node.childNodes) + .map(function(node) { + return node.nodeValue; + } + ) + .join(''); + } +} + + +function parseGpx(txt, name) { + 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; + }; + + 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; + } + waypoint.name = utf8_decode(xmlGetNodeText(waypoint_element.getElementsByTagName('name')[0])); + waypoint.symbol_name = xmlGetNodeText(waypoint_element.getElementsByTagName('sym')[0]); + waypoints.push(waypoint); + } + return waypoints; + }; + + // remove namespaces + txt = txt.replace(/<([^ >]+):([^ >]+)/g, '<$1_$2'); + var dom = (new DOMParser()).parseFromString(txt, "text/xml"); + if (dom.documentElement.nodeName === 'parsererror') { + return null; + } + if (dom.getElementsByTagName('gpx').length === 0) { + return null; + } + return [{ + name: name, + tracks: getTrackSegments(dom), + points: getWaypoints(dom), + error: error + }]; +} + + +function parseOziPlt(txt, name) { + var error; + var segments = []; + 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 !== 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 parseOziWpt(txt, name) { + var points = [], + error, + lines, line, + i, + lat, lng, pointName, fields; + 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; + } + name = utf8_decode(xmlGetNodeText(name[0]).trim()); + points.push({ + name: name, + lat: lat, + lng: lng + } + ); + } + return points; + } + + txt = txt.replace(/<([^ >]+):([^ >]+)/g, '<$1_$2'); + var dom = (new DOMParser()).parseFromString(txt, "text/xml"); + 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; + var unzipper = new JSUnzip(txt); + var tracks = [], + points = [], + geodata, + error; + var hasDocKml = false; + if (!unzipper.isZipFile()) { + return null; + } + unzipper.readEntries(); + 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 = window.RawDeflate.inflate(entry.data); + } else { + return null; + } + geodata = parseKml(uncompressed, 'dummmy'); + if (geodata) { + error = error || geodata.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) { + var unzipper = new JSUnzip(txt); + if (!unzipper.isZipFile()) { + return null; + } + unzipper.readEntries(); + 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 = RawDeflate.inflate(entry.data); + } else { + return null; + } + var file_name = name + '/' + entry.fileName; + var geodata = parseGeoFile(file_name, uncompressed); + geodata_array.push.apply(geodata_array, geodata); + } + 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 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 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 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 parseStringified(s, oldVersion) { + var name, + n, + segments = [], + segment, + segmentsCount, + pointsCount, + arcUnit = ((1 << 24) - 1) / 360, + x, y, + error, version, midX, midY, /*symbol,*/ waypointName, + wayPoints = [], color, measureTicksShown; + s = decodeUrlSafeBase64(s); + if (!s) { + return [{name: 'Text encoded track', error: ['CORRUPT']}]; + } + s = new PackedStreamReader(s); + try { + if (oldVersion) { + version = 0; + } else { + version = s.readNumber(); + } + if (version !== 0 && version !== 1 && version !== 2) { + return [{name: 'Text encoded track', error: ['CORRUPT']}]; + } + 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 (version === 0 && e.message.match('Unexpected end of line while unpacking number')) { + color = 0; + measureTicksShown = 0; + } 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); + // symbol = 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 + }; + return [geoData]; +} + +function parseTrackUrl(s) { + var i = s.indexOf('track://'); + if (i === -1) { + return null; + } + return parseStringified(s.substring(i + 8), true); +} + +function parseNakarteUrl(s) { + var i = s.indexOf('#'); + if (i === -1) { + return null; + } + i = s.indexOf('nktk=', i + 1); + if (i === -1) { + return null; + } + s = s.substring(i + 5).split('/'); + var geodataArray = []; + for (i = 0; i < s.length; i++) { + if (s[i]) { + geodataArray.push.apply(geodataArray, parseStringified(s[i])); + } + } + return geodataArray; +} + +function parseGeoFile(name, data) { + var parsers = [ + parseTrackUrl, + parseNakarteUrl, + parseKmz, + parseZip, + parseGpx, + 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}; diff --git a/src/lib/leaflet.control.track-list/track-list.css b/src/lib/leaflet.control.track-list/track-list.css @@ -0,0 +1,230 @@ +.leaflet-control-tracklist { + position: relative; + background-color: white; + border-radius: 5px 5px 5px 5px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4); + padding: 8px 8px 8px 8px; + width: 290px; +} + +.leaflet-control-tracklist .hint { + display: inline-block; + color: #999; + font-size: 10px; + padding-bottom: 2px; +} + +.leaflet-control-tracklist .inputs-row { + white-space: nowrap; +} + +.leaflet-control-tracklist .button{ + display: inline-block; + height: 26px; + border-radius: 4px 4px 4px 4px; + border: 1px solid #ccc; + cursor: pointer; + width: 26px; + background-position: 50% 50%; + background-repeat: no-repeat; + vertical-align: middle; +} + +.leaflet-control-tracklist .button:hover, +.leaflet-control-tracklist .button-length:hover, +.leaflet-control-tracklist .button-add-track:hover, +.leaflet-control-tracklist .button-add-point:hover { + background-color: #f4f4f4; +} + +.leaflet-control-tracklist .input-url { + display: inline-block; + width: 156px; + height: 26px; + line-height: 26px; + padding: 0 3px; + border: 1px solid #ccc; + border-radius: 3px 0 0 3px; + vertical-align: middle; + margin-right: -1px; +} + +.leaflet-control-tracklist .open-file { + background-image: url("images/folder_open.png"); + margin-right: 5px; +} + +.leaflet-control-tracklist .add-track { + background-image: url("images/plus.png"); + margin-right: 5px; +} + +.leaflet-control-tracklist .menu-icon { + background-image: url("images/menu.png"); + margin-left: 5px; + margin-right: 0 !important; +} + + +.leaflet-control-tracklist .download-url { + background-image: url("images/arrow_bottom.png"); + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.leaflet-control-tracklist .visibility-switch { + display: inline-block; + margin: 0; + vertical-align: middle; +} + +.leaflet-control-tracklist .color-sample { + display: inline-block; + width: 16px; + height: 4px; + vertical-align: middle; + cursor: pointer; + margin-left: 2px; + border-top-width: 6px; + border-bottom-width: 6px; + border-color: white; + border-style: solid; +} + +.leaflet-control-tracklist .track-name-wrapper { + display: block; + width: 136px; + margin-left: 2px; +} + +.leaflet-control-tracklist .track-name { + display: inline-block; + font-size: 11px; + color: #333; + cursor: pointer; + border-bottom: 1px dashed #999; + max-width: 140px; + white-space:nowrap; + text-overflow: ellipsis; + overflow: hidden; + vertical-align: text-bottom; +} + +.leaflet-control-tracklist .track-text-button { + display: inline-block; + font-weight: bold; + color: #333; + cursor: pointer; + vertical-align: top; + line-height: 16px; +} + +.leaflet-control-tracklist .button-length { + font-size: 9px; + line-height: 16px; + color: #777; + display: block; + border-radius: 4px; + border: 1px solid #aaa; + margin: 0 4px; + padding: 0 3px; + cursor: pointer; + white-space: nowrap; + width: 40px; + text-align: right; +} + +.leaflet-control-tracklist .button-add-track { + background-image: url("images/add_line.png"); +} + + +.leaflet-control-tracklist .button-add-point { + background-image: url("images/add_point.png"); +} + + +.leaflet-control-tracklist .button-add-track, +.leaflet-control-tracklist .button-add-point { + display: block; + border-radius: 4px; + border: 1px solid #aaa; + width: 16px; + height: 16px; + margin-right: 4px; + cursor: pointer; + +} + +.leaflet-control-tracklist .ticks-enabled { + color: #333; + border-color: #333; +} + +.leaflet-control-tracklist .tracks-rows, +.leaflet-control-tracklist .tracks-rows tr, +.leaflet-control-tracklist .tracks-rows td { + margin: 0; + padding: 0; +} + +.leaflet-control-tracklist .tracks-rows td { + padding-top: 0; +} + +.leaflet-control-tracklist .tracks-rows { + border-collapse: collapse; + width: 100%; + line-height: 20px; +} + +.track-waypoint { + display: block; + width: 21px !important; + height: 21px !important; + background-repeat: no-repeat; + margin-top: -21px !important; + margin-left: -10.5px !important; + padding-left: 20px; + padding-bottom: 15px; + font-size: 10px; + font-weight: bold; + font-family: Verdana, Arial, sans-serif; + white-space: nowrap; + text-shadow: 1px 0 1px #fff, + 1px 1px 1px #fff, + 0 1px 1px #fff, + -1px 1px 1px #fff, + -1px 0 1px #fff, + -1px -1px 1px #fff, + 0 -1px 1px #fff, + 1px -1px 1px #fff; +} + +.track-waypoint.symbol-marker-1 { + background-image: url("images/marker_1.png"); +} +.track-waypoint.symbol-marker-2 { + background-image: url("images/marker_2.png"); +} +.track-waypoint.symbol-marker-3 { + background-image: url("images/marker_3.png"); +} +.track-waypoint.symbol-marker-4 { + background-image: url("images/marker_4.png"); +} +.track-waypoint.symbol-marker-5 { + background-image: url("images/marker_5.png"); +} +.track-waypoint.symbol-marker-6 { + background-image: url("images/marker_6.png"); +} + +.leaflet-point-placing { + cursor: crosshair !important; +} + + +.leaflet-point-placing svg * { + pointer-events: none; +} diff --git a/src/lib/leaflet.control.track-list/track-list.js b/src/lib/leaflet.control.track-list/track-list.js @@ -0,0 +1,1012 @@ +import L from 'leaflet'; +import ko from 'knockout'; +import Contextmenu from 'lib/contextmenu/contextmenu'; +import 'lib/knockout.component.progress/progress'; +import './track-list.css'; +import {selectFiles, readFiles} from 'lib/file-read/file-read'; +import {parseGeoFile} from './lib/geo_file_formats'; +import urlViaCorsProxy from 'lib/CORSProxy/proxy'; +import {XMLHttpRequestPromise} from 'lib/xhr-promise/xhr-promise'; +import geoExporters from './lib/geo_file_exporters'; +import copyToClipboard from 'lib/clipboardCopy/clipboardCopy'; +import {saveAs} from 'browser-filesaver'; +import 'lib/leaflet.polyline-edit/edit_line'; +import 'lib/leaflet.polyline-measure/measured_line'; +import 'lib/leaflet.layer.canvasMarkers/canvasMarkers'; +import 'lib/leaflet.lineutil.simplifyLatLngs/simplify'; +import iconFromBackgroundImage from 'lib/iconFromBackgroundImage/iconFromBackgroundImage'; + +var MeasuredEditableLine = L.MeasuredLine.extend({}); +MeasuredEditableLine.include(L.Polyline.EditMixin); + +var Waypoints = L.Layer.CanvasMarkers.extend({ + options: { + scaleDependent: true + }, + + clone: function() { + var markers = this.rtree.all(), + markersCopy; + + function cloneMarker(marker) { + return { + latlng: {lat: marker.latlng.lat, lng: marker.latlng.lng}, + label: marker.label, + icon: marker.icon + }; + } + + markersCopy = markers.map(cloneMarker); + var options = {}; + L.extend(options, this.options, {iconScale: 1.5, labelFontSize: 14}); + return new Waypoints(markersCopy, options); + } + } +); + + +L.Control.TrackList = L.Control.extend({ + options: {position: 'bottomright'}, + + colors: ['#77f', '#f95', '#0ff', '#f77', '#f7f', '#ee5'], + + + initialize: function() { + L.Control.prototype.initialize.call(this); + this.tracks = ko.observableArray(); + this.url = ko.observable(''); + this.readingFiles = ko.observable(false); + this.readProgressRange = ko.observable(10); + this.readProgressDone = ko.observable(2); + this._lastTrackColor = 0; + }, + + onAdd: function(map) { + this.map = map; + this.tracks.removeAll(); + var container = L.DomUtil.create('div', 'leaflet-control leaflet-control-tracklist'); + L.DomEvent.disableClickPropagation(container); + if (!L.Browser.touch) { + L.DomEvent.disableScrollPropagation(container); + } + + container.innerHTML = ` + <div class="hint"> + GPX Ozi GoogleEarth ZIP YandexMaps + </div> + <div class="inputs-row" data-bind="visible: !readingFiles()"> + <a class="button add-track" title="New track" data-bind="click: function(){this.addNewTrack()}"></a + ><a class="button open-file" title="Open file" data-bind="click: loadFilesFromDisk"></a + ><input type="text" class="input-url" placeholder="Track URL" data-bind="textInput: url, event: {keypress: onEnterPressedInInput}" + ><a class="button download-url" title="Download URL" data-bind="click: loadFilesFromUrl"></a + ><a class="button menu-icon" data-bind="click: function(_,e){this.showMenu(e)}"></a> + </div> + <div style="text-align: center"> + <div data-bind=" + component: { + name: 'progress-indicator', + params: {progressRange: readProgressRange, progressDone: readProgressDone} + }, + visible: readingFiles"></div> + </div> + <table class="tracks-rows" data-bind="foreach: {data: tracks, as: 'track'}"> + <tr data-bind="event: {contextmenu: $parent.showTrackMenu.bind($parent)}"> + <td><input type="checkbox" class="visibility-switch" data-bind="checked: track.visible"></td> + <td><div class="color-sample" data-bind="style: {backgroundColor: $parent.colors[track.color()]}, click: $parent.onColorSelectorClicked.bind($parent)"></div></td> + <td><div class="track-name-wrapper"><div class="track-name" data-bind="text: track.name, attr: {title: track.name}, click: $parent.setViewToTrack.bind($parent)"></div></div></td> + <td><div class="button-length" data-bind="text: $parent.formatLength(track.length()), css: {'ticks-enabled': track.measureTicksShown}, click: $parent.switchMeasureTicksVisibility.bind($parent)"></div></td> + <td><div class="button-add-track" title="Add track segment" data-bind="click: $parent.addSegmentAndEdit.bind($parent, track)"></div></td> + <td><div class="button-add-point" title="Add point" data-bind="click: $parent.placeNewPoint.bind($parent, track)"></div></td> + <td><a class="track-text-button" title="Actions" data-bind="click: $parent.showTrackMenu.bind($parent)">&hellip;</a></td> + </tr> + </table> + `; + + ko.applyBindings(this, container); + // FIXME: add onRemove method and unsubscribe + L.DomEvent.addListener(map.getContainer(), 'drop', this.onFileDragDrop, this); + L.DomEvent.addListener(map.getContainer(), 'dragover', this.onFileDraging, this); + this.menu = new Contextmenu([ + {text: 'Copy all tracks to clipboard', callback: this.copyAllTracks.bind(this)}, + {text: 'Copy visible tracks to clipboard', callback: this.copyVisibleTracks.bind(this)}, + '-', + {text: 'Delete all tracks', callback: this.deleteAllTracks.bind(this)}, + {text: 'Delete hidden tracks', callback: this.deleteHiddenTracks.bind(this)} + ] + ); + this._markerLayer = new Waypoints(null, {print: true, zIndex: 100001}).addTo(map); + this._markerLayer.on('markerclick markercontextmenu', this.onMarkerClick, this); + return container; + }, + + onFileDraging: function(e) { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + e.dataTransfer.dropEffect = 'copy'; + }, + + onFileDragDrop: function(e) { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + this.loadFilesFromFilesObject(e.dataTransfer.files); + }, + + onEnterPressedInInput: function(this_, e) { + if (e.keyCode === 13) { + this_.loadFilesFromUrl(); + L.DomEvent.stop(e); + } else { + return true; + } + }, + + getTrackPolylines: function(track) { + return track.feature.getLayers().filter(function(layer) { + return layer instanceof L.Polyline; + } + ) + }, + + getTrackPoints: function(track) { + return track.markers; + }, + + addNewTrack: function(name) { + if (!name) { + name = this.url().slice(0, 50); + if (!name.length) { + name = 'New track'; + } else { + this.url(''); + } + } + var track = this.addTrack({name: name}), + line = this.addTrackSegment(track); + this.startEditTrackSegement(track, line); + line.startDrawingLine(); + return track; + }, + + loadFilesFromFilesObject: function(files) { + readFiles(files).then(function(fileDataArray) { + var geodataArray = fileDataArray.map(function(fileData) { + return parseGeoFile(fileData.filename, fileData.data); + } + ).reduce(function(prev, next) { + Array.prototype.push.apply(prev, next); + return prev; + }, [] + ); + this.addTracksFromGeodataArray(geodataArray); + }.bind(this) + ); + }, + + loadFilesFromDisk: function() { + selectFiles(true).then(this.loadFilesFromFilesObject.bind(this)); + }, + + loadFilesFromUrl: function() { + var url = this.url().trim(); + try { + url = decodeURIComponent(url); + } catch (e) { + } + var geodata; + if (url.length > 0) { + this.readingFiles(true); + this.readProgressDone(undefined); + this.readProgressRange(1); + geodata = parseGeoFile('', url); + if (geodata.length === 0 || geodata.length > 1 || geodata[0].error !== 'UNSUPPORTED') { + this.addTracksFromGeodataArray(geodata); + } else { + var url_for_request = urlViaCorsProxy(url); + var name = url + .split('#')[0] + .split('?')[0] + .replace(/\/*$/, '') + .split('/') + .pop(); + XMLHttpRequestPromise(url_for_request, {responseType: 'binarystring'}) + .then(function(xhr) { + var geodata = parseGeoFile(name, xhr.response); + this.addTracksFromGeodataArray(geodata); + }.bind(this), + function() { + var geodata = [{name: url, error: 'NETWORK'}]; + this.addTracksFromGeodataArray(geodata); + }.bind(this) + ); + } + } + this.url(''); + }, + + addTracksFromGeodataArray: function(geodata_array) { + var messages = []; + if (geodata_array.length === 0) { + messages.push('No tracks loaded'); + } + geodata_array.forEach(function(geodata) { + var data_empty = !((geodata.tracks && geodata.tracks.length) || (geodata.points && geodata.points.length)); + + if (!data_empty) { + if (geodata.tracks) { + geodata.tracks = geodata.tracks.map(function(line) { + return L.LineUtil.simplifyLatlngs(line, 360 / (1 << 24)); + } + ); + } + this.addTrack(geodata); + } + var error_messages = { + 'CORRUPT': 'File "{name}" is corrupt', + 'UNSUPPORTED': 'File "{name}" has unsupported format or is badly corrupt', + 'NETWORK': 'Could not download file from url "{name}"' + }; + var message; + if (geodata.error) { + message = error_messages[geodata.error] || geodata.error; + if (data_empty) { + message += ', no data could be loaded'; + } else { + message += ', loaded data can be invalid or incomplete'; + } + } + if (message) { + message = L.Util.template(message, {name: geodata.name}); + messages.push(message); + } + }.bind(this) + ); + this.readingFiles(false); + if (messages.length) { + alert(messages.join('\n')); + } + }, + + + onTrackColorChanged: function(track) { + var color = this.colors[track.color()]; + this.getTrackPolylines(track).forEach( + function(polyline) { + polyline.setStyle({color: color}); + } + ); + var markers = this.getTrackPoints(track); + markers.forEach(this.setMarkerIcon.bind(this)); + this._markerLayer.updateMarkers(markers); + }, + + onTrackVisibilityChanged: function(track) { + if (track.visible()) { + this.map.addLayer(track.feature); + this._markerLayer.addMarkers(track.markers); + } else { + this.map.removeLayer(track.feature); + this.stopPlacingPoint(); + this._markerLayer.removeMarkers(track.markers); + } + }, + + onTrackLengthChanged: function(track) { + const lines = this.getTrackPolylines(track); + let length = 0; + for (let line of lines) { + length += line.getLength(); + } + track.length(length); + }, + + formatLength: function(x) { + var digits = 0; + if (x < 10000) { + digits = 2; + } else if (x < 100000) { + digits = 1; + } + return (x / 1000).toFixed(digits) + ' km'; + }, + + setTrackMeasureTicksVisibility: function(track) { + var visible = track.measureTicksShown(), + lines = this.getTrackPolylines(track); + lines.forEach((line) => line.setMeasureTicksVisible(visible)); + }, + + switchMeasureTicksVisibility: function(track) { + track.measureTicksShown(!(track.measureTicksShown())); + }, + + onColorSelectorClicked: function(track, e) { + track._contextmenu.show(e); + }, + + setViewToTrack: function(track) { + var lines = this.getTrackPolylines(track); + var points = this.getTrackPoints(track); + if (lines.length || points.length) { + var bounds = L.latLngBounds([]); + lines.forEach(function(l) { + bounds.extend(l.getBounds()); + } + ); + points.forEach(function(p) { + bounds.extend([p.latlng.lat, p.latlng.lng]); + } + ); + this.map.flyToBounds(bounds); + + } + }, + + attachColorSelector: function(track) { + var items = this.colors.map(function(color, index) { + return { + text: '<div style="display: inline-block; vertical-align: middle; width: 50px; height: 0; border-top: 4px solid ' + + color + '"></div>', + callback: track.color.bind(null, index) + }; + } + ); + track._contextmenu = new Contextmenu(items); + }, + + attachActionsMenu: function(track) { + var items = [ + function() { + return {text: track.name(), disabled: true}; + }, + '-', + {text: 'Rename', callback: this.renameTrack.bind(this, track)}, + {text: 'Duplicate', callback: this.duplicateTrack.bind(this, track)}, + {text: 'Reverse', callback: this.reverseTrack.bind(this, track)}, + {text: 'Show elevation profile', callback: this.showElevationProfileForTrack.bind(this, track)}, + '-', + {text: 'Delete', callback: this.removeTrack.bind(this, track)}, + '-', + {text: 'Download GPX', callback: this.saveTrackAsFile.bind(this, track, geoExporters.saveGpx, '.gpx')}, + {text: 'Download KML', callback: this.saveTrackAsFile.bind(this, track, geoExporters.saveKml, '.kml')}, + {text: 'Copy link to clipboard', callback: this.copyLinkToClipboard.bind(this, track)}, + ]; + track._actionsMenu = new Contextmenu(items); + }, + + addSegmentAndEdit: function(track) { + if (!track.visible()) { + return; + } + this.stopPlacingPoint(); + var polyline = this.addTrackSegment(track, []); + this.startEditTrackSegement(track, polyline); + polyline.startDrawingLine(1); + }, + + duplicateTrack: function(track) { + var segments = [], segment, + line, + lines = this.getTrackPolylines(track); + for (var i = 0; i < lines.length; i++) { + segment = []; + line = lines[i].getLatLngs(); + for (var j = 0; j < line.length; j++) { + segment.push([line[j].lat, line[j].lng]); + } + segments.push(segment); + } + this.addTrack({name: track.name(), tracks: segments}); + }, + + reverseTrackSegment: function(trackSegment) { + trackSegment.stopDrawingLine(); + var latlngs = trackSegment.getLatLngs(); + latlngs = latlngs.map(function(ll) { + return [ll.lat, ll.lng]; + } + ); + latlngs.reverse(); + var isEdited = (this._editedLine === trackSegment); + this.deleteTrackSegment(trackSegment); + var newTrackSegment = this.addTrackSegment(trackSegment._parentTrack, latlngs); + if (isEdited) { + this.startEditTrackSegement(trackSegment._parentTrack, newTrackSegment); + } + }, + + reverseTrack: function(track) { + var self = this; + this.getTrackPolylines(track).forEach(function(trackSegment) { + self.reverseTrackSegment(trackSegment); + } + ); + }, + + copyLinkToClipboard: function(track, mouseEvent) { + this.stopActiveDraw(); + var s = this.trackToString(track); + var url = window.location + '&nktk=' + s; + copyToClipboard(url, mouseEvent); + }, + + saveTrackAsFile: function(track, exporter, extension) { + this.stopActiveDraw(); + var lines = this.getTrackPolylines(track) + .map(function(line) { + return line.getLatLngs(); + } + ); + var points = this.getTrackPoints(track); + var name = track.name(), + i = name.lastIndexOf('.'); + if (i > -1 && i >= name.length - 5) { + name = name.slice(0, i); + } + + if (lines.length === 0 && points.length === 0) { + alert('Track is empty, nothing to save'); + return; + } + + var fileText = exporter(lines, name, points); + var filename = name + extension; + saveAs(new Blob([fileText], {type: 'application/download'}), filename); + }, + + renameTrack: function(track) { + var newName = prompt('Enter new name', track.name()); + if (newName && newName.length) { + track.name(newName); + } + }, + + showTrackMenu: function(track, e) { + track._actionsMenu.show(e); + }, + + showMenu: function(e) { + this.menu.show(e); + }, + + stopActiveDraw: function() { + if (this._editedLine) { + this._editedLine.stopDrawingLine(); + } + }, + + stopEditLine: function() { + if (this._editedLine) { + this._editedLine.stopEdit(); + this._editedLine = null; + } + }, + + onTrackSegmentClick: function(track, trackSegment, e) { + if (this._editedPoint) { + return; + } + if (this._lineJoinCursor) { + L.DomEvent.stopPropagation(e); + this.joinTrackSegments(trackSegment); + } else { + this.startEditTrackSegement(track, trackSegment); + L.DomEvent.stopPropagation(e); + } + }, + + startEditTrackSegement: function(track, polyline) { + if (this._editedLine && this._editedLine !== polyline) { + this._editedLine.stopEdit(); + } + polyline.startEdit(); + this._editedLine = polyline; + polyline.once('editend', function() { + setTimeout(this.onLineEditEnd.bind(this, track, polyline), 0); + }.bind(this) + ); + }, + + placeNewPoint: function(track) { + if (!track.visible()) { + return; + } + this.startPlacingPoint({_parentTrack: track, _new: true}); + }, + + startPlacingPoint: function(marker) { + this.stopPlacingPoint(); + this.stopEditLine(); + L.DomUtil.addClass(this._map._container, 'leaflet-point-placing'); + this._editedPoint = marker; + this.map.on('click', this.placePoint, this); + L.DomEvent.on(document, 'keyup', this.stopPlacingPointOnEscPressed, this); + }, + + placePoint: function(e) { + if (!this._editedPoint) { + return; + } + var marker = this._editedPoint; + if (marker._new) { + marker._parentTrack._pointAutoInc += 1; + var name = '' + marker._parentTrack._pointAutoInc; + while (name.length < 3) { + name = '0' + name; + } + marker = this.addPoint(marker._parentTrack, {name: name, lat: e.latlng.lat, lng: e.latlng.lng}); + this._markerLayer.addMarker(marker); + } else { + this._markerLayer.setMarkerPosition(marker, e.latlng); + } + this.stopPlacingPoint(); + }, + + stopPlacingPointOnEscPressed: function(e) { + if (e.keyCode === 27) { + this.stopPlacingPoint(); + } + }, + + stopPlacingPoint: function() { + this._editedPoint = null; + L.DomUtil.removeClass(this._map._container, 'leaflet-point-placing'); + L.DomEvent.off(document, 'keyup', this.stopPlacingPointOnEscPressed, this); + this.map.off('click', this.placePoint, this); + }, + + joinTrackSegments: function(newSegment) { + this.stopLineJoinSelection(); + var originalSegment = this._editedLine; + var latlngs = originalSegment.getLatLngs(), + latngs2 = newSegment.getLatLngs(); + if (this._lineJoinToStart === this._lineJoinFromStart) { + latngs2.reverse(); + } + if (this._lineJoinFromStart) { + latlngs.unshift.apply(latlngs, latngs2); + } else { + latlngs.push.apply(latlngs, latngs2); + } + latlngs = latlngs.map(function(ll) { + return [ll.lat, ll.lng]; + } + ); + this.deleteTrackSegment(originalSegment); + if (originalSegment._parentTrack === newSegment._parentTrack) { + this.deleteTrackSegment(newSegment); + } + this.addTrackSegment(originalSegment._parentTrack, latlngs); + + }, + + onLineEditEnd: function(track, polyline) { + if (polyline.getLatLngs().length < 2) { + track.feature.removeLayer(polyline); + } + this.onTrackLengthChanged(track); + if (this._editedLine === polyline) { + this._editedLine = null; + } + }, + + addTrackSegment: function(track, sourcePoints) { + var polyline = new MeasuredEditableLine(sourcePoints || [], { + weight: 6, + color: this.colors[track.color()], + lineCap: 'butt', + className: 'leaflet-editable-line', + opacity: 0.5, + } + ); + polyline._parentTrack = track; + polyline.setMeasureTicksVisible(track.measureTicksShown()); + polyline.on('click', this.onTrackSegmentClick.bind(this, track, polyline)); + polyline.on('nodeschanged', this.onTrackLengthChanged.bind(this, track)); + polyline.on('noderightclick', this.onNodeRightClickShowMenu, this); + polyline.on('segmentrightclick', this.onSegmentRightClickShowMenu, this); + polyline.on('mousemove', this.onMouseMoveOnSegmentUpdateLineJoinCursor, this); + + //polyline.on('editingstart', polyline.setMeasureTicksVisible.bind(polyline, false)); + //polyline.on('editingend', this.setTrackMeasureTicksVisibility.bind(this, track)); + track.feature.addLayer(polyline); + return polyline; + }, + + onNodeRightClickShowMenu: function(e) { + var items = []; + if (e.nodeIndex > 0 && e.nodeIndex < e.line.getLatLngs().length - 1) { + items.push({ + text: 'Cut', + callback: this.splitTrackSegment.bind(this, e.line, e.nodeIndex, null) + } + ); + } + if (e.nodeIndex === 0 || e.nodeIndex === e.line.getLatLngs().length - 1) { + items.push({text: 'Join', callback: this.startLineJoinSelection.bind(this, e)}); + } + items.push({text: 'Reverse', callback: this.reverseTrackSegment.bind(this, e.line)}); + items.push({text: 'Delete segment', callback: this.deleteTrackSegment.bind(this, e.line)}); + items.push({text: 'New track from segment', callback: this.newTrackFromSegment.bind(this, e.line)}); + items.push({ + text: 'Show elevation profile for segment', + callback: this.showElevationProfileForSegment.bind(this, e.line) + } + ); + + var menu = new Contextmenu(items); + menu.show(e.mouseEvent); + + }, + + onSegmentRightClickShowMenu: function(e) { + var menu = new Contextmenu([ + { + text: 'Cut', + callback: this.splitTrackSegment.bind(this, e.line, e.nodeIndex, e.mouseEvent.latlng) + }, + {text: 'Reverse', callback: this.reverseTrackSegment.bind(this, e.line)}, + {text: 'Delete segment', callback: this.deleteTrackSegment.bind(this, e.line)}, + {text: 'New track from segment', callback: this.newTrackFromSegment.bind(this, e.line)}, + { + text: 'Show elevation profile for segment', + callback: this.showElevationProfileForSegment.bind(this, e.line) + } + ] + ); + menu.show(e.mouseEvent); + }, + + startLineJoinSelection: function(e) { + this.stopLineJoinSelection(); + this._editedLine.stopDrawingLine(); + this._lineJoinFromStart = (e.nodeIndex === 0); + var p = this._editedLine.getLatLngs()[e.nodeIndex]; + p = [p.lat, p.lng]; + this._lineJoinCursor = L.polyline([p, e.mouseEvent.latlng], { + clickable: false, + color: 'red', + weight: 1.5, + opacity: 1, + dashArray: '7,7' + } + ) + .addTo(this.map); + this.map.on('mousemove', this.onMouseMoveUpdateLineJoinCursor, this); + this.map.on('click', this.stopLineJoinSelection, this); + L.DomEvent.on(document, 'keyup', this.onEscPressedStopLineJoinSelection, this); + var self = this; + setTimeout(function() { + self._editedLine.preventStopEdit = true; + }, 0 + ); + }, + + onMouseMoveUpdateLineJoinCursor: function(e) { + if (this._lineJoinCursor) { + this._lineJoinCursor.getLatLngs().splice(1, 1, e.latlng); + this._lineJoinCursor.redraw(); + this._lineJoinCursor.setStyle({color: 'red'}); + } + }, + + onMouseMoveOnSegmentUpdateLineJoinCursor: function(e) { + if (!this._lineJoinCursor) { + return; + } + var trackSegment = e.target, + latlngs = trackSegment.getLatLngs(), + distToStart = e.latlng.distanceTo(latlngs[0]), + distToEnd = e.latlng.distanceTo(latlngs[latlngs.length - 1]); + this._lineJoinToStart = (distToStart < distToEnd); + var cursorEnd = this._lineJoinToStart ? latlngs[0] : latlngs[latlngs.length - 1]; + this._lineJoinCursor.setStyle({color: 'green'}); + this._lineJoinCursor.getLatLngs().splice(1, 1, cursorEnd); + this._lineJoinCursor.redraw(); + L.DomEvent.stopPropagation(e); + }, + + onEscPressedStopLineJoinSelection: function(e) { + if ('input' === e.target.tagName.toLowerCase()) { + return; + } + switch (e.keyCode) { + case 27: + case 13: + this.stopLineJoinSelection(); + L.DomEvent.stop(e); + break; + default: + } + }, + + stopLineJoinSelection: function() { + if (this._lineJoinCursor) { + this.map.off('mousemove', this.onMouseMoveUpdateLineJoinCursor, this); + this.map.off('click', this.stopLineJoinSelection, this); + L.DomEvent.off(document, 'keyup', this.onEscPressedStopLineJoinSelection, this); + this.map.removeLayer(this._lineJoinCursor); + this._lineJoinCursor = null; + var self = this; + setTimeout(function() { + self._editedLine.preventStopEdit = false; + }, 0 + ); + } + }, + + splitTrackSegment: function(trackSegment, nodeIndex, latlng) { + var latlngs = trackSegment.getLatLngs(); + latlngs = latlngs.map(function(ll) { + return [ll.lat, ll.lng]; + } + ); + var latlngs1 = latlngs.slice(0, nodeIndex + 1), + latlngs2 = latlngs.slice(nodeIndex + 1); + if (latlng) { + var p = this.map.project(latlng), + p1 = this.map.project(latlngs[nodeIndex]), + p2 = this.map.project(latlngs[nodeIndex + 1]), + pnew = L.LineUtil.closestPointOnSegment(p, p1, p2); + latlng = this.map.unproject(pnew); + latlngs1.push(latlng); + latlng = [latlng.lat, latlng.lng]; + } else { + latlng = latlngs[nodeIndex]; + } + latlngs2.unshift(latlng); + this.deleteTrackSegment(trackSegment); + var segment1 = this.addTrackSegment(trackSegment._parentTrack, latlngs1); + this.addTrackSegment(trackSegment._parentTrack, latlngs2); + this.startEditTrackSegement(trackSegment._parentTrack, segment1); + }, + + deleteTrackSegment: function(trackSegment) { + trackSegment._parentTrack.feature.removeLayer(trackSegment); + }, + + newTrackFromSegment: function(trackSegment) { + var srcNodes = trackSegment.getLatLngs(), + newNodes = [], + i; + for (i = 0; i < srcNodes.length; i++) { + newNodes.push([srcNodes[i].lat, srcNodes[i].lng]); + } + this.addTrack({name: "New track", tracks: [newNodes]}); + }, + + addTrack: function(geodata) { + var color; + color = geodata.color; + if (!(color >= 0 && color < this.colors.length)) { + color = this._lastTrackColor; + this._lastTrackColor = (this._lastTrackColor + 1) % this.colors.length; + } + var track = { + name: ko.observable(geodata.name), + color: ko.observable(color), + visible: ko.observable(true), + length: ko.observable('empty'), + measureTicksShown: ko.observable(geodata.measureTicksShown || false), + feature: L.featureGroup([]), + _pointAutoInc: 0, + markers: [] + }; + (geodata.tracks || []).forEach(this.addTrackSegment.bind(this, track)); + (geodata.points || []).forEach(this.addPoint.bind(this, track)); + + this.tracks.push(track); + + track.visible.subscribe(this.onTrackVisibilityChanged.bind(this, track)); + track.measureTicksShown.subscribe(this.setTrackMeasureTicksVisibility.bind(this, track)); + track.color.subscribe(this.onTrackColorChanged.bind(this, track)); + + //this.onTrackColorChanged(track); + this.onTrackVisibilityChanged(track); + this.attachColorSelector(track); + this.attachActionsMenu(track); + this.onTrackLengthChanged(track); + return track; + }, + + + setMarkerIcon: function(marker) { + var symbol = 'marker', + colorInd = marker._parentTrack.color() + 1, + className = 'symbol-' + symbol + '-' + colorInd; + marker.icon = iconFromBackgroundImage('track-waypoint ' + className); + }, + + setMarkerLabel: function(marker, label) { + if (label.match(/\d{3,}/)) { + var n = parseInt(label, 10); + marker._parentTrack._pointAutoInc = + Math.max(n, marker._parentTrack._pointAutoInc | 0); + } + marker.label = label; + }, + + addPoint: function(track, srcPoint) { + var marker = { + latlng: L.latLng([srcPoint.lat, srcPoint.lng]), + _parentTrack: track, + }; + this.setMarkerIcon(marker); + this.setMarkerLabel(marker, srcPoint.name); + track.markers.push(marker); + marker._parentTrack = track; + return marker; + }, + + onMarkerClick: function(e) { + new Contextmenu([ + {text: 'Rename', callback: this.renamePoint.bind(this, e.marker)}, + {text: 'Move', callback: this.startPlacingPoint.bind(this, e.marker)}, + {text: 'Delete', callback: this.removePoint.bind(this, e.marker)}, + ] + ).show(e); + }, + + removePoint: function(marker) { + this.stopPlacingPoint(); + this._markerLayer.removeMarker(marker); + marker._parentTrack.markers.remove(marker); + }, + + renamePoint: function(marker) { + this.stopPlacingPoint(); + var newLabel = prompt('New point name', marker.label); + if (newLabel !== null) { + this.setMarkerLabel(marker, newLabel); + this._markerLayer.updateMarker(marker); + } + }, + + removeTrack: function(track) { + track.visible(false); + this.tracks.remove(track); + this.stopPlacingPoint(); + }, + + deleteAllTracks: function() { + var tracks = this.tracks().slice(0), + i; + for (i = 0; i < tracks.length; i++) { + this.removeTrack(tracks[i]); + } + }, + + deleteHiddenTracks: function() { + var tracks = this.tracks().slice(0), + i, track; + for (i = 0; i < tracks.length; i++) { + track = tracks[i]; + if (!track.visible()) { + this.removeTrack(tracks[i]); + } + } + }, + + trackToString: function(track) { + var lines = this.getTrackPolylines(track).map(function(line) { + var points = line.getLatLngs(); + points = L.LineUtil.simplifyLatlngs(points, 360 / (1 << 24)); + return points; + } + ); + return geoExporters.saveToString(lines, track.name(), track.color(), track.measureTicksShown(), + this.getTrackPoints(track) + ); + }, + + copyAllTracks: function(mouseEvent) { + this.stopActiveDraw(); + var tracks = this.tracks(), + serialized = [], + i, track, s; + for (i = 0; i < tracks.length; i++) { + track = tracks[i]; + s = this.trackToString(track); + serialized.push(s); + } + var url = window.location + '&nktk=' + serialized.join('/'); + copyToClipboard(url, mouseEvent); + }, + + copyVisibleTracks: function(mouseEvent) { + this.stopActiveDraw(); + var tracks = this.tracks(), + serialized = [], + i, track, s; + for (i = 0; i < tracks.length; i++) { + track = tracks[i]; + if (track.visible()) { + s = this.trackToString(track); + serialized.push(s); + } + } + var url = window.location + '&nktk=' + serialized.join('/'); + copyToClipboard(url, mouseEvent); + }, + + exportTracks: function(minTicksIntervalMeters) { + var self = this; + return this.tracks() + .filter(function(track) { + return self.getTrackPolylines(track).length; + } + ) + .map(function(track) { + var capturedTrack = track.feature.getLayers().map(function(pl) { + return pl.getLatLngs().map(function(ll) { + return [ll.lat, ll.lng]; + } + ); + } + ); + var bounds = track.feature.getBounds(); + var capturedBounds = [[bounds.getSouth(), bounds.getWest()], [bounds.getNorth(), bounds.getEast()]]; + return { + color: track.color(), + visible: track.visible(), + segments: capturedTrack, + bounds: capturedBounds, + measureTicksShown: track.measureTicksShown(), + measureTicks: [].concat.apply([], track.feature.getLayers().map(function(pl) { + return pl.getTicksPositions(minTicksIntervalMeters); + } + ) + ) + }; + } + ); + }, + + calcSamplingInterval: function(length) { + var targetPointsN = 2000; + var maxPointsN = 9999; + var samplingIntgerval = length / targetPointsN; + if (samplingIntgerval < 10) { + samplingIntgerval = 10; + } + if (samplingIntgerval > 50) { + samplingIntgerval = 50; + } + if (length / samplingIntgerval > maxPointsN) { + samplingIntgerval = length / maxPointsN; + } + return samplingIntgerval; + }, + + showElevationProfileForSegment: function(line) { + if (this._elevationControl) { + this._elevationControl.removeFrom(this._map); + } + this.stopEditLine(); + this._elevationControl = new L.Control.ElevationProfile(line.getLatLngs(), { + samplingInterval: this.calcSamplingInterval(line.getLength()) + } + ).addTo(this._map); + }, + + showElevationProfileForTrack: function(track) { + var lines = this.getTrackPolylines(track), + path = [], + i; + for (i = 0; i < lines.length; i++) { + if (lines[i] === this._editedLine) { + this.stopEditLine(); + } + path = path.concat(lines[i].getLatLngs()); + } + if (this._elevationControl) { + this._elevationControl.removeFrom(this._map); + } + this._elevationControl = new L.Control.ElevationProfile(path, { + samplingInterval: this.calcSamplingInterval(track.length()) + } + ).addTo(this._map); + + } + + } +); diff --git a/src/lib/leaflet.lineutil.simplifyLatLngs/simplify.js b/src/lib/leaflet.lineutil.simplifyLatLngs/simplify.js @@ -0,0 +1,22 @@ +import L from 'leaflet'; + +L.LineUtil.simplifyLatlngs = function simplifyLatlngs(points, tolerance) { + function latlngToXy(p) { + return { + x: p.lng, + y: p.lat + }; + } + + function xyToLatlng(p) { + return { + lat: p.y, + lng: p.x + }; + } + + points = points.map(latlngToXy); + points = L.LineUtil.simplify(points, tolerance); + points = points.map(xyToLatlng); + return points; +}; diff --git a/src/lib/leaflet.polyline-edit/edit_line.css b/src/lib/leaflet.polyline-edit/edit_line.css @@ -0,0 +1,33 @@ +.line-editor-node-marker-halo { + box-sizing: border-box; + width: 14px !important; + height: 14px !important; + border-radius: 7px !important; + border: 4px solid #eee; + margin-left: -7px !important; + margin-top: -7px !important; + outline: none; +} + +.line-editor-node-marker { + box-sizing: border-box; + width: 12px !important; + height: 12px !important; + border-radius: 6px !important; + border: 2px solid #333; + margin-left: -3px !important; + margin-top: -3px !important; + outline: none; +} + +.line-editor-node-marker:hover, .line-editor-node-marker:active { + border-color: orange; +} + +.leaflet-line-drawing { + cursor: crosshair !important; +} + +.leaflet-line-drawing .leaflet-editable-line { + pointer-events: none !important; +} +\ No newline at end of file diff --git a/src/lib/leaflet.polyline-edit/edit_line.js b/src/lib/leaflet.polyline-edit/edit_line.js @@ -0,0 +1,348 @@ +import L from 'leaflet'; +import './edit_line.css'; + +function cloneLatLng(ll) { + return L.latLng(ll.lat, ll.lng); +} + +L.Polyline.EditMixin = { + _nodeMarkersZOffset: 10000, + + startEdit: function() { + if (this._map && !this._editing) { + this._editing = true; + this._drawingDirection = 0; + this.setupMarkers(); + this.on('remove', this.stopEdit.bind(this)); + this._map + .on('click', this.onMapClick, this) + .on('dragend', this.onMapEndDrag, this); + L.DomEvent.on(document, 'keyup', this.onKeyPress, this); + this._storedStyle = {weight: this.options.weight, opacity: this.options.opacity}; + this.setStyle({weight: 1.5, opacity: 1}); + L.DomUtil.addClass(this._map._container, 'leaflet-line-editing'); + } + }, + + stopEdit: function() { + if (this._editing) { + this.stopDrawingLine(); + this._editing = false; + this.removeMarkers(); + L.DomEvent.off(document, 'keyup', this.onKeyPress, this); + this.off('remove', this.stopEdit.bind(this)); + this._map + .off('click', this.onMapClick, this) + .off('dragend', this.onMapEndDrag, this); + this.setStyle(this._storedStyle); + L.DomUtil.removeClass(this._map._container, 'leaflet-line-editing'); + this.fire('editend', {target: this}); + } + }, + + removeMarkers: function() { + this.getLatLngs().forEach(function(node) { + if (node._nodeMarker) { + this._map.removeLayer(node._nodeMarker); + delete node._nodeMarker._lineNode; + delete node._nodeMarker; + } + if (node._segmentOverlay) { + this._map.removeLayer(node._segmentOverlay); + delete node._segmentOverlay._lineNode; + delete node._segmentOverlay; + } + }.bind(this) + ); + }, + + onNodeMarkerDragEnd: function(e) { + var marker = e.target, + nodeIndex = this.getLatLngs().indexOf(marker._lineNode); + this.replaceNode(nodeIndex, marker.getLatLng()); + }, + + onNodeMarkerMovedChangeNode: function(e) { + var marker = e.target, + latlng = marker.getLatLng(), + //nodeIndex = this.getLatLngs().indexOf(marker._lineNode); + node = marker._lineNode; + node.lat = latlng.lat; + node.lng = latlng.lng; + this.redraw(); + this.fire('nodeschanged'); + }, + + onNodeMarkerDblClickedRemoveNode: function(e) { + var marker = e.target, + nodeIndex = this.getLatLngs().indexOf(marker._lineNode); + this.removeNode(nodeIndex); + this.fire('nodeschanged'); + }, + + onMapClick: function(e) { + if (this._drawingDirection) { + var newNodeIndex = this._drawingDirection === -1 ? 1 : this.getLatLngs().length - 1; + this.addNode(newNodeIndex, e.latlng); + } else { + if (!this.preventStopEdit) { + this.stopEdit(); + } + } + }, + + onMapEndDrag: function(e) { + if (e.distance < 15) { + // get mouse position from map drag handler + var handler = e.target.dragging._draggable; + var mousePos = handler._startPoint.add(handler._newPos).subtract(handler._startPos); + var latlng = e.target.mouseEventToLatLng({clientX: mousePos.x, clientY: mousePos.y}); + this.onMapClick({latlng: latlng}); + } + }, + + startDrawingLine: function(direction, e) { + if (!this._editing) { + return; + } + if (direction === undefined) { + direction = 1; + } + if (this._drawingDirection === direction) { + return; + } + this.stopDrawingLine(); + this._drawingDirection = direction; + + if (e) { + var newNodeIndex = this._drawingDirection === -1 ? 0 : this.getLatLngs().length; + this.spliceLatLngs(newNodeIndex, 0, e.latlng); + this.fire('nodeschanged'); + } + + this._map.on('mousemove', this.onMouseMoveFollowEndNode, this); + L.DomUtil.addClass(this._map._container, 'leaflet-line-drawing'); + }, + + + stopDrawingLine: function() { + if (!this._drawingDirection) { + return; + } + this._map.off('mousemove', this.onMouseMoveFollowEndNode, this); + var nodeIndex = this._drawingDirection === -1 ? 0 : this.getLatLngs().length - 1; + this.spliceLatLngs(nodeIndex, 1); + this.fire('nodeschanged'); + this._drawingDirection = 0; + L.DomUtil.removeClass(this._map._container, 'leaflet-line-drawing'); + + }, + + onKeyPress: function(e) { + if ('input' === e.target.tagName.toLowerCase()) { + return; + } + var code = e.keyCode; + switch (code) { + case 27: + case 13: + if (this._drawingDirection) { + this.stopDrawingLine(); + } else { + if (!this.preventStopEdit) { + this.stopEdit(); + } + } + L.DomEvent.stop(e); + break; + default: + } + }, + + onMouseMoveFollowEndNode: function(e) { + var nodeIndex = this._drawingDirection === -1 ? 0 : this.getLatLngs().length - 1; + this.spliceLatLngs(nodeIndex, 1, e.latlng); + this.fire('nodeschanged'); + }, + + makeNodeMarker: function(nodeIndex) { + var node = this.getLatLngs()[nodeIndex], + marker = L.marker(cloneLatLng(node), { + icon: L.divIcon( + {className: 'line-editor-node-marker-halo', 'html': '<div class="line-editor-node-marker"></div>'} + ), + draggable: true, + zIndexOffset: this._nodeMarkersZOffset + } + ); + marker + .on('drag', this.onNodeMarkerMovedChangeNode, this) + //.on('dragstart', this.fire.bind(this, 'editingstart')) + .on('dragend', this.onNodeMarkerDragEnd, this) + .on('dblclick', this.onNodeMarkerDblClickedRemoveNode, this) + .on('click', this.onNodeMarkerClickStartStopDrawing, this) + .on('contextmenu', function(e) { + this.stopDrawingLine(); + this.fire('noderightclick', { + nodeIndex: this.getLatLngs().indexOf(marker._lineNode), + line: this, + mouseEvent: e + } + ); + }, this + ); + marker._lineNode = node; + node._nodeMarker = marker; + marker.addTo(this._map); + + }, + + onNodeMarkerClickStartStopDrawing: function(e) { + var marker = e.target, + latlngs = this.getLatLngs(), + latlngs_n = latlngs.length, + nodeIndex = latlngs.indexOf(marker._lineNode); + if ((this._drawingDirection === -1 && nodeIndex === 1) || + ((this._drawingDirection === 1 && nodeIndex === latlngs_n - 2))) { + this.stopDrawingLine(); + } else if (nodeIndex === 0) { + this.startDrawingLine(-1, e); + } else if (nodeIndex === this.getLatLngs().length - 1) { + this.startDrawingLine(1, e); + } + }, + + makeSegmentOverlay: function(nodeIndex) { + var latlngs = this.getLatLngs(), + p1 = latlngs[nodeIndex], + p2 = latlngs[nodeIndex + 1], + segmentOverlay = L.polyline([p1, p2], {weight: 10, opacity: 0.0}); + segmentOverlay.on('mousedown', this.onSegmentMouseDownAddNode, this); + segmentOverlay.on('contextmenu', function(e) { + this.stopDrawingLine(); + this.fire('segmentrightclick', { + nodeIndex: this.getLatLngs().indexOf(segmentOverlay._lineNode), + mouseEvent: e, + line: this + } + ); + }, this + ); + segmentOverlay._lineNode = p1; + p1._segmentOverlay = segmentOverlay; + segmentOverlay.addTo(this._map); + }, + + onSegmentMouseDownAddNode: function(e) { + if (e.originalEvent.button !== 0) { + return; + } + var segmentOverlay = e.target, + latlngs = this.getLatLngs(), + nodeIndex = latlngs.indexOf(segmentOverlay._lineNode) + 1; + this.addNode(nodeIndex, e.latlng); + // TODO: hack, may be replace with sending mouse event + latlngs[nodeIndex]._nodeMarker.dragging._draggable._onDown(e.originalEvent); + this.fire('nodeschanged'); + }, + + addNode: function(index, latlng) { + var nodes = this.getLatLngs(), + isAddingLeft = (index === 1 && this._drawingDirection === -1), + isAddingRight = (index === nodes.length - 1 && this._drawingDirection === 1); + latlng = cloneLatLng(latlng); + this.spliceLatLngs(index, 0, latlng); + this.makeNodeMarker(index); + if (!isAddingLeft && (index >= 1)) { + if (!isAddingRight) { + var prevNode = nodes[index - 1]; + this._map.removeLayer(prevNode._segmentOverlay); + delete prevNode._segmentOverlay._lineNode; + delete prevNode._segmentOverlay; + } + this.makeSegmentOverlay(index - 1); + } + if (!isAddingRight) { + this.makeSegmentOverlay(index); + } + }, + + removeNode: function(index) { + var nodes = this.getLatLngs(), + node = nodes[index], + marker = node._nodeMarker; + delete node._nodeMarker; + delete marker._lineNode; + this.spliceLatLngs(index, 1); + this._map.removeLayer(marker); + if (node._segmentOverlay) { + this._map.removeLayer(node._segmentOverlay); + delete node._segmentOverlay._lineNode; + delete node._segmentOverlay; + } + var prevNode = nodes[index - 1]; + if (prevNode && prevNode._segmentOverlay) { + this._map.removeLayer(prevNode._segmentOverlay); + delete prevNode._segmentOverlay._lineNode; + delete prevNode._segmentOverlay; + if ((index < nodes.length - 1) || (index < nodes.length && this._drawingDirection !== 1)) { + this.makeSegmentOverlay(index - 1); + } + } + }, + + replaceNode: function(index, latlng) { + var nodes = this.getLatLngs(), + oldNode = nodes[index], + oldMarker = oldNode._nodeMarker; + this._map.removeLayer(oldNode._nodeMarker); + delete oldNode._nodeMarker; + delete oldMarker._lineNode; + latlng = cloneLatLng(latlng); + this.spliceLatLngs(index, 1, latlng); + this.makeNodeMarker(index); + if (oldNode._segmentOverlay) { + this._map.removeLayer(oldNode._segmentOverlay); + delete oldNode._segmentOverlay._lineNode; + delete oldNode._segmentOverlay; + this.makeSegmentOverlay(index); + } + var prevNode = nodes[index - 1]; + if (prevNode && prevNode._segmentOverlay) { + this._map.removeLayer(prevNode._segmentOverlay); + delete prevNode._segmentOverlay._lineNode; + delete prevNode._segmentOverlay; + this.makeSegmentOverlay(index - 1); + } + }, + + setupMarkers: function() { + this.removeMarkers(); + var latlngs = this.getLatLngs(), + startNode = 0, + endNode = latlngs.length - 1; + if (this._drawingDirection === -1) { + startNode += 1; + } + if (this._drawingDirection === 1) { + endNode -= 1; + } + for (var i = startNode; i <= endNode; i++) { + this.makeNodeMarker(i); + if (i < endNode) { + this.makeSegmentOverlay(i); + } + } + + }, + + spliceLatLngs: function(...args) { + const latlngs = this.getLatLngs(); + const res = latlngs.splice(...args); + this.setLatLngs(latlngs); + return res; + // this._latlngs.splice(...args); + // this.redraw(); + } + +}; +\ No newline at end of file diff --git a/src/lib/leaflet.polyline-measure/measured_line.css b/src/lib/leaflet.polyline-measure/measured_line.css @@ -0,0 +1,25 @@ +.measure-tick-icon { + line-height: 0; + margin-left: 0 !important; + margin-top: 0 !important; + opacity: 0.85; + pointer-events: none; +} + +.measure-tick-icon-text { + padding-left: 0.7em; + color: black; + font-family: Verdana, Arial, sans-serif; + font-size: 10px; + white-space: nowrap; + font-weight: bold; + transform-origin: 0 0; + text-shadow: 1px 0 0 #fff, + 1px 1px 0 #fff, + 0 1px 0 #fff, + -1px 1px 0 #fff, + -1px 0 0 #fff, + -1px -1px 0 #fff, + 0 -1px 0 #fff, + 1px -1px 0 #fff; +} +\ No newline at end of file diff --git a/src/lib/leaflet.polyline-measure/measured_line.js b/src/lib/leaflet.polyline-measure/measured_line.js @@ -0,0 +1,192 @@ +import L from 'leaflet'; +import './measured_line.css'; + +function pointOnSegmentAtDistance(p1, p2, dist) { + //FIXME: we should place markers along projected line to avoid transformation distortions + var q = dist / p1.distanceTo(p2), + x = p1.lng + (p2.lng - p1.lng) * q, + y = p1.lat + (p2.lat - p1.lat) * q; + return L.latLng(y, x); + +} + +function sinCosFromSegment(segment) { + var p1 = segment[0], + p2 = segment[1], + dx = p2.x - p1.x, + dy = p2.y - p1.y, + len = Math.sqrt(dx * dx + dy * dy), + sin = dy / len, + cos = dx / len; + return [sin, cos]; +} + +L.MeasuredLine = L.Polyline.extend({ + options: { + minTicksIntervalMm: 15, + }, + + onAdd: function(map) { + L.Polyline.prototype.onAdd.call(this, map); + this._ticks = {}; + this.updateTicks(); + this._map.on('zoomend', this.updateTicks, this); + this._map.on('dragend', this.updateTicks, this); + this.on('nodeschanged', this.updateTicksLater, this); + }, + + updateTicksLater: function() { + setTimeout(this.updateTicks.bind(this), 0); + }, + + onRemove: function(map) { + this._map.off('zoomend', this.updateTicks, this); + this._map.off('dragend', this.updateTicks, this); + this.off('nodeschanged', this.updateTicks, this); + this._clearTicks(); + L.Polyline.prototype.onRemove.call(this, map); + }, + + _clearTicks: function() { + if (this._map) { + Object.values(this._ticks).forEach((tick) => this._map.removeLayer(tick)); + this._ticks = {}; + } + }, + + _addTick: function(tick, marker) { + var transformMatrixString = 'matrix(' + tick.transformMatrix.join(',') + ')'; + if (marker) { + marker._icon.childNodes[0].style.transform = transformMatrixString; + marker.setLatLng(tick.position); + } else { + var labelText = Math.round((tick.distanceValue / 10)) / 100 + ' km', + icon = L.divIcon( + { + html: '<div class="measure-tick-icon-text" style="transform:' + transformMatrixString + '">' + + labelText + '</div>', + className: 'measure-tick-icon' + } + ); + marker = L.marker(tick.position, {icon: icon, clickable: false, keyboard: false}); + marker.addTo(this._map); + } + this._ticks[tick.distanceValue.toString()] = marker; + }, + + setMeasureTicksVisible: function(visible) { + this.options.measureTicksShown = visible; + this.updateTicks(); + }, + + getTicksPositions: function(minTicksIntervalMeters, bounds) { + if (!this._map) { + return []; + } + var steps = [500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000]; + var ticks = [], + self = this; + + function addTick(position, segment, distanceValue) { + if (bounds && (!bounds.contains(position))) { + return; + } + segment = [self._map.project(segment[0], 1), self._map.project(segment[1], 1)]; + var sinCos = sinCosFromSegment(segment), + sin = sinCos[0], cos = sinCos[1], + transformMatrix; + + if (sin > 0) { + transformMatrix = [sin, -cos, cos, sin, 0, 0]; + } else { + transformMatrix = [-sin, cos, -cos, -sin, 0, 0]; + } + ticks.push({position: position, distanceValue: distanceValue, transformMatrix: transformMatrix}); + } + + let step; + for (step of steps) { + if (step >= minTicksIntervalMeters) { + break; + } + } + + var lastTickMeasure = 0, + lastPointMeasure = 0, + points = this._latlngs, + points_n = points.length, + nextPointMeasure, + segmentLength; + if (points_n < 2) { + return ticks; + } + + for (var i = 1; i < points_n; i++) { + segmentLength = points[i].distanceTo(points[i - 1]); + nextPointMeasure = lastPointMeasure + segmentLength; + if (nextPointMeasure >= lastTickMeasure + step) { + while (lastTickMeasure + step <= nextPointMeasure) { + lastTickMeasure += step; + addTick( + pointOnSegmentAtDistance(points[i - 1], points[i], lastTickMeasure - lastPointMeasure), + [points[i - 1], points[i]], + lastTickMeasure + ); + } + } + lastPointMeasure = nextPointMeasure; + } + if (lastPointMeasure > minTicksIntervalMeters / 2) { + addTick(points[0], [points[0], points[1]], 0); + addTick(points[points_n - 1], [points[points_n - 2], points[points_n - 1]], lastPointMeasure); + } + return ticks; + }, + + updateTicks: function() { + if (!this._map) { + return; + } + if (!this.options.measureTicksShown) { + this._clearTicks(); + return; + } + var bounds = this._map.getBounds().pad(1), + rad = Math.PI / 180, + dpi = 96, + mercatorMetersPerPixel = 20003931 / (this._map.project([180, 0]).x), + referencePoint = this.getLatLngs().length ? this.getBounds().getCenter() : this._map.getCenter(), + realMetersPerPixel = mercatorMetersPerPixel * Math.cos(referencePoint.lat * rad), + mapScale = 1 / dpi * 2.54 / 100 / realMetersPerPixel, + minTicksIntervalMeters = this.options.minTicksIntervalMm / mapScale / 1000, + ticks = this.getTicksPositions(minTicksIntervalMeters, bounds), + oldTicks = this._ticks; + this._ticks = {}; + ticks.forEach(function(tick) { + var oldMarker = oldTicks[tick.distanceValue.toString()]; + this._addTick(tick, oldMarker); + if (oldMarker) { + delete oldTicks[tick.distanceValue.toString()]; + } + }.bind(this) + ); + Object.values(oldTicks).forEach((tick) => this._map.removeLayer(tick)); + }, + + getLength: function() { + var points = this._latlngs, + points_n = points.length, + length = 0; + + for (var i = 1; i < points_n; i++) { + length += points[i].distanceTo(points[i - 1]); + } + return length; + } + } +); + + +L.measuredLine = function(latlngs, options) { + return new L.MeasuredLine(latlngs, options); +}; +\ No newline at end of file diff --git a/src/lib/xhr-promise/xhr-promise.js b/src/lib/xhr-promise/xhr-promise.js @@ -1,8 +1,14 @@ function makeRequest(url, {method='GET', data=null, responseType='', timeout=0} = {}) { const xhr = new XMLHttpRequest(); xhr.open(method, url); - xhr.responseType = responseType; xhr.timeout = timeout; + if (responseType === 'binarystring') { + xhr.responseType = 'text'; + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } else { + xhr.responseType = responseType; + } + const promise = new Promise(function(resolve, reject) { xhr.onreadystatechange = function() { if (xhr.readyState === 4) { diff --git a/src/vendored/github.com/augustl/js-unzip/js-unzip.js b/src/vendored/github.com/augustl/js-unzip/js-unzip.js @@ -0,0 +1,141 @@ +(function (GLOBAL) { + var JSUnzip = function (fileContents) { + this.fileContents = new JSUnzip.BigEndianBinaryStream(fileContents); + } + GLOBAL.JSUnzip = JSUnzip; + JSUnzip.MAGIC_NUMBER = 0x04034b50; + + JSUnzip.prototype = { + readEntries: function () { + if (!this.isZipFile()) { + throw new Error("File is not a Zip file."); + } + + this.entries = []; + var e = new JSUnzip.ZipEntry(this.fileContents); + while (typeof(e.data) === "string") { + this.entries.push(e); + e = new JSUnzip.ZipEntry(this.fileContents); + } + }, + + isZipFile: function () { + return this.fileContents.getByteRangeAsNumber(0, 4) === JSUnzip.MAGIC_NUMBER; + } + } + + JSUnzip.ZipEntry = function (binaryStream) { + this.signature = binaryStream.getNextBytesAsNumber(4); + if (this.signature !== JSUnzip.MAGIC_NUMBER) { + return; + } + + this.versionNeeded = binaryStream.getNextBytesAsNumber(2); + this.bitFlag = binaryStream.getNextBytesAsNumber(2); + this.compressionMethod = binaryStream.getNextBytesAsNumber(2); + this.timeBlob = binaryStream.getNextBytesAsNumber(4); + + if (this.isEncrypted()) { + throw "File contains encrypted entry. Not supported."; + } + + if (this.isUsingUtf8()) { + throw "File is using UTF8. Not supported."; + } + + this.crc32 = binaryStream.getNextBytesAsNumber(4); + this.compressedSize = binaryStream.getNextBytesAsNumber(4); + this.uncompressedSize = binaryStream.getNextBytesAsNumber(4); + + if (this.isUsingZip64()) { + throw "File is using Zip64 (4gb+ file size). Not supported"; + } + + this.fileNameLength = binaryStream.getNextBytesAsNumber(2); + this.extraFieldLength = binaryStream.getNextBytesAsNumber(2); + + this.fileName = binaryStream.getNextBytesAsString(this.fileNameLength); + this.extra = binaryStream.getNextBytesAsString(this.extraFieldLength); + this.data = binaryStream.getNextBytesAsString(this.compressedSize); + + if (this.isUsingBit3TrailingDataDescriptor()) { + if (typeof(console) !== "undefined") { + console.log( "File is using bit 3 trailing data descriptor. Not supported."); + } + binaryStream.getNextBytesAsNumber(16); //Skip the descriptor and move to beginning of next ZipEntry + } + } + + JSUnzip.ZipEntry.prototype = { + isEncrypted: function () { + return (this.bitFlag & 0x01) === 0x01; + }, + + isUsingUtf8: function () { + return (this.bitFlag & 0x0800) === 0x0800; + }, + + isUsingBit3TrailingDataDescriptor: function () { + return (this.bitFlag & 0x0008) === 0x0008; + }, + + isUsingZip64: function () { + this.compressedSize === 0xFFFFFFFF || + this.uncompressedSize === 0xFFFFFFFF; + } + } + + JSUnzip.BigEndianBinaryStream = function (stream) { + this.stream = stream; + this.resetByteIndex(); + } + + JSUnzip.BigEndianBinaryStream.prototype = { + // The index of the current byte, used when we step through the byte + // with getNextBytesAs*. + resetByteIndex: function () { + this.currentByteIndex = 0; + }, + + getByteAt: function (index) { + return this.stream.charCodeAt(index); + }, + + getNextBytesAsNumber: function (steps) { + var res = this.getByteRangeAsNumber(this.currentByteIndex, steps); + this.currentByteIndex += steps; + return res; + }, + + getNextBytesAsString: function (steps) { + var res = this.getByteRangeAsString(this.currentByteIndex, steps); + this.currentByteIndex += steps; + return res; + }, + + // Big endian, so we're going backwards. + getByteRangeAsNumber: function (index, steps) { + var result = 0; + var i = index + steps - 1; + while (i >= index) { + result = (result << 8) + this.getByteAt(i); + i--; + } + return result; + }, + + getByteRangeAsString: function (index, steps) { + var result = ""; + var max = index + steps; + var i = index; + while (i < max) { + var charCode = this.getByteAt(i); + result += String.fromCharCode(charCode); + // Accounting for multi-byte strings. + max -= Math.floor(charCode / 0x100); + i++; + } + return result; + } + } +}(this)); diff --git a/src/vendored/github.com/dankogai/js-deflate/rawinflate.js b/src/vendored/github.com/dankogai/js-deflate/rawinflate.js @@ -0,0 +1,755 @@ +/* + * $Id: rawinflate.js,v 0.4 2014/03/01 21:59:08 dankogai Exp dankogai $ + * + * GNU General Public License, version 2 (GPL-2.0) + * http://opensource.org/licenses/GPL-2.0 + * original: + * http://www.onicos.com/staff/iz/amuse/javascript/expert/inflate.txt + */ + +(function(ctx){ + +/* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp> + * Version: 1.0.0.1 + * LastModified: Dec 25 1999 + */ + +/* Interface: + * data = zip_inflate(src); + */ + +/* constant parameters */ +var zip_WSIZE = 32768; // Sliding Window size +var zip_STORED_BLOCK = 0; +var zip_STATIC_TREES = 1; +var zip_DYN_TREES = 2; + +/* for inflate */ +var zip_lbits = 9; // bits in base literal/length lookup table +var zip_dbits = 6; // bits in base distance lookup table +var zip_INBUFSIZ = 32768; // Input buffer size +var zip_INBUF_EXTRA = 64; // Extra buffer + +/* variables (inflate) */ +var zip_slide; +var zip_wp; // current position in slide +var zip_fixed_tl = null; // inflate static +var zip_fixed_td; // inflate static +var zip_fixed_bl, zip_fixed_bd; // inflate static +var zip_bit_buf; // bit buffer +var zip_bit_len; // bits in bit buffer +var zip_method; +var zip_eof; +var zip_copy_leng; +var zip_copy_dist; +var zip_tl, zip_td; // literal/length and distance decoder tables +var zip_bl, zip_bd; // number of bits decoded by tl and td + +var zip_inflate_data; +var zip_inflate_pos; + + +/* constant tables (inflate) */ +var zip_MASK_BITS = new Array( + 0x0000, + 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff, + 0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff); +// Tables for deflate from PKZIP's appnote.txt. +var zip_cplens = new Array( // Copy lengths for literal codes 257..285 + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0); +/* note: see note #13 above about the 258 in this list. */ +var zip_cplext = new Array( // Extra bits for literal codes 257..285 + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99); // 99==invalid +var zip_cpdist = new Array( // Copy offsets for distance codes 0..29 + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577); +var zip_cpdext = new Array( // Extra bits for distance codes + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13); +var zip_border = new Array( // Order of the bit length code lengths + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15); +/* objects (inflate) */ + +var zip_HuftList = function() { + this.next = null; + this.list = null; +} + +var zip_HuftNode = function() { + this.e = 0; // number of extra bits or operation + this.b = 0; // number of bits in this code or subcode + + // union + this.n = 0; // literal, length base, or distance base + this.t = null; // (zip_HuftNode) pointer to next level of table +} + +var zip_HuftBuild = function(b, // code lengths in bits (all assumed <= BMAX) + n, // number of codes (assumed <= N_MAX) + s, // number of simple-valued codes (0..s-1) + d, // list of base values for non-simple codes + e, // list of extra bits for non-simple codes + mm // maximum lookup bits + ) { + this.BMAX = 16; // maximum bit length of any code + this.N_MAX = 288; // maximum number of codes in any set + this.status = 0; // 0: success, 1: incomplete table, 2: bad input + this.root = null; // (zip_HuftList) starting table + this.m = 0; // maximum lookup bits, returns actual + +/* Given a list of code lengths and a maximum table size, make a set of + tables to decode that set of codes. Return zero on success, one if + the given code set is incomplete (the tables are still built in this + case), two if the input is invalid (all zero length codes or an + oversubscribed set of lengths), and three if not enough memory. + The code with value 256 is special, and the tables are constructed + so that no bits beyond that code are fetched when that code is + decoded. */ + { + var a; // counter for codes of length k + var c = new Array(this.BMAX+1); // bit length count table + var el; // length of EOB code (value 256) + var f; // i repeats in table every f entries + var g; // maximum code length + var h; // table level + var i; // counter, current code + var j; // counter + var k; // number of bits in current code + var lx = new Array(this.BMAX+1); // stack of bits per table + var p; // pointer into c[], b[], or v[] + var pidx; // index of p + var q; // (zip_HuftNode) points to current table + var r = new zip_HuftNode(); // table entry for structure assignment + var u = new Array(this.BMAX); // zip_HuftNode[BMAX][] table stack + var v = new Array(this.N_MAX); // values in order of bit length + var w; + var x = new Array(this.BMAX+1);// bit offsets, then code stack + var xp; // pointer into x or c + var y; // number of dummy codes added + var z; // number of entries in current table + var o; + var tail; // (zip_HuftList) + + tail = this.root = null; + for(i = 0; i < c.length; i++) + c[i] = 0; + for(i = 0; i < lx.length; i++) + lx[i] = 0; + for(i = 0; i < u.length; i++) + u[i] = null; + for(i = 0; i < v.length; i++) + v[i] = 0; + for(i = 0; i < x.length; i++) + x[i] = 0; + + // Generate counts for each bit length + el = n > 256 ? b[256] : this.BMAX; // set length of EOB code, if any + p = b; pidx = 0; + i = n; + do { + c[p[pidx]]++; // assume all entries <= BMAX + pidx++; + } while(--i > 0); + if(c[0] == n) { // null input--all zero length codes + this.root = null; + this.m = 0; + this.status = 0; + return; + } + + // Find minimum and maximum length, bound *m by those + for(j = 1; j <= this.BMAX; j++) + if(c[j] != 0) + break; + k = j; // minimum code length + if(mm < j) + mm = j; + for(i = this.BMAX; i != 0; i--) + if(c[i] != 0) + break; + g = i; // maximum code length + if(mm > i) + mm = i; + + // Adjust last length count to fill out codes, if needed + for(y = 1 << j; j < i; j++, y <<= 1) + if((y -= c[j]) < 0) { + this.status = 2; // bad input: more codes than bits + this.m = mm; + return; + } + if((y -= c[i]) < 0) { + this.status = 2; + this.m = mm; + return; + } + c[i] += y; + + // Generate starting offsets into the value table for each length + x[1] = j = 0; + p = c; + pidx = 1; + xp = 2; + while(--i > 0) // note that i == g from above + x[xp++] = (j += p[pidx++]); + + // Make a table of values in order of bit lengths + p = b; pidx = 0; + i = 0; + do { + if((j = p[pidx++]) != 0) + v[x[j]++] = i; + } while(++i < n); + n = x[g]; // set n to length of v + + // Generate the Huffman codes and for each, make the table entries + x[0] = i = 0; // first Huffman code is zero + p = v; pidx = 0; // grab values in bit order + h = -1; // no tables yet--level -1 + w = lx[0] = 0; // no bits decoded yet + q = null; // ditto + z = 0; // ditto + + // go through the bit lengths (k already is bits in shortest code) + for(; k <= g; k++) { + a = c[k]; + while(a-- > 0) { + // here i is the Huffman code of length k bits for value p[pidx] + // make tables up to required level + while(k > w + lx[1 + h]) { + w += lx[1 + h]; // add bits already decoded + h++; + + // compute minimum size table less than or equal to *m bits + z = (z = g - w) > mm ? mm : z; // upper limit + if((f = 1 << (j = k - w)) > a + 1) { // try a k-w bit table + // too few codes for k-w bit table + f -= a + 1; // deduct codes from patterns left + xp = k; + while(++j < z) { // try smaller tables up to z bits + if((f <<= 1) <= c[++xp]) + break; // enough codes to use up j bits + f -= c[xp]; // else deduct codes from patterns + } + } + if(w + j > el && w < el) + j = el - w; // make EOB code end at table + z = 1 << j; // table entries for j-bit table + lx[1 + h] = j; // set table size in stack + + // allocate and link in new table + q = new Array(z); + for(o = 0; o < z; o++) { + q[o] = new zip_HuftNode(); + } + + if(tail == null) + tail = this.root = new zip_HuftList(); + else + tail = tail.next = new zip_HuftList(); + tail.next = null; + tail.list = q; + u[h] = q; // table starts after link + + /* connect to last table, if there is one */ + if(h > 0) { + x[h] = i; // save pattern for backing up + r.b = lx[h]; // bits to dump before this table + r.e = 16 + j; // bits in this table + r.t = q; // pointer to this table + j = (i & ((1 << w) - 1)) >> (w - lx[h]); + u[h-1][j].e = r.e; + u[h-1][j].b = r.b; + u[h-1][j].n = r.n; + u[h-1][j].t = r.t; + } + } + + // set up table entry in r + r.b = k - w; + if(pidx >= n) + r.e = 99; // out of values--invalid code + else if(p[pidx] < s) { + r.e = (p[pidx] < 256 ? 16 : 15); // 256 is end-of-block code + r.n = p[pidx++]; // simple code is just the value + } else { + r.e = e[p[pidx] - s]; // non-simple--look up in lists + r.n = d[p[pidx++] - s]; + } + + // fill code-like entries with r // + f = 1 << (k - w); + for(j = i >> w; j < z; j += f) { + q[j].e = r.e; + q[j].b = r.b; + q[j].n = r.n; + q[j].t = r.t; + } + + // backwards increment the k-bit code i + for(j = 1 << (k - 1); (i & j) != 0; j >>= 1) + i ^= j; + i ^= j; + + // backup over finished tables + while((i & ((1 << w) - 1)) != x[h]) { + w -= lx[h]; // don't need to update q + h--; + } + } + } + + /* return actual size of base table */ + this.m = lx[1]; + + /* Return true (1) if we were given an incomplete table */ + this.status = ((y != 0 && g != 1) ? 1 : 0); + } /* end of constructor */ +} + + +/* routines (inflate) */ + +var zip_GET_BYTE = function() { + if(zip_inflate_data.length == zip_inflate_pos) + return -1; + return zip_inflate_data.charCodeAt(zip_inflate_pos++) & 0xff; +} + +var zip_NEEDBITS = function(n) { + while(zip_bit_len < n) { + zip_bit_buf |= zip_GET_BYTE() << zip_bit_len; + zip_bit_len += 8; + } +} + +var zip_GETBITS = function(n) { + return zip_bit_buf & zip_MASK_BITS[n]; +} + +var zip_DUMPBITS = function(n) { + zip_bit_buf >>= n; + zip_bit_len -= n; +} + +var zip_inflate_codes = function(buff, off, size) { + /* inflate (decompress) the codes in a deflated (compressed) block. + Return an error code or zero if it all goes ok. */ + var e; // table entry flag/number of extra bits + var t; // (zip_HuftNode) pointer to table entry + var n; + + if(size == 0) + return 0; + + // inflate the coded data + n = 0; + for(;;) { // do until end of block + zip_NEEDBITS(zip_bl); + t = zip_tl.list[zip_GETBITS(zip_bl)]; + e = t.e; + while(e > 16) { + if(e == 99) + return -1; + zip_DUMPBITS(t.b); + e -= 16; + zip_NEEDBITS(e); + t = t.t[zip_GETBITS(e)]; + e = t.e; + } + zip_DUMPBITS(t.b); + + if(e == 16) { // then it's a literal + zip_wp &= zip_WSIZE - 1; + buff[off + n++] = zip_slide[zip_wp++] = t.n; + if(n == size) + return size; + continue; + } + + // exit if end of block + if(e == 15) + break; + + // it's an EOB or a length + + // get length of block to copy + zip_NEEDBITS(e); + zip_copy_leng = t.n + zip_GETBITS(e); + zip_DUMPBITS(e); + + // decode distance of block to copy + zip_NEEDBITS(zip_bd); + t = zip_td.list[zip_GETBITS(zip_bd)]; + e = t.e; + + while(e > 16) { + if(e == 99) + return -1; + zip_DUMPBITS(t.b); + e -= 16; + zip_NEEDBITS(e); + t = t.t[zip_GETBITS(e)]; + e = t.e; + } + zip_DUMPBITS(t.b); + zip_NEEDBITS(e); + zip_copy_dist = zip_wp - t.n - zip_GETBITS(e); + zip_DUMPBITS(e); + + // do the copy + while(zip_copy_leng > 0 && n < size) { + zip_copy_leng--; + zip_copy_dist &= zip_WSIZE - 1; + zip_wp &= zip_WSIZE - 1; + buff[off + n++] = zip_slide[zip_wp++] + = zip_slide[zip_copy_dist++]; + } + + if(n == size) + return size; + } + + zip_method = -1; // done + return n; +} + +var zip_inflate_stored = function(buff, off, size) { + /* "decompress" an inflated type 0 (stored) block. */ + var n; + + // go to byte boundary + n = zip_bit_len & 7; + zip_DUMPBITS(n); + + // get the length and its complement + zip_NEEDBITS(16); + n = zip_GETBITS(16); + zip_DUMPBITS(16); + zip_NEEDBITS(16); + if(n != ((~zip_bit_buf) & 0xffff)) + return -1; // error in compressed data + zip_DUMPBITS(16); + + // read and output the compressed data + zip_copy_leng = n; + + n = 0; + while(zip_copy_leng > 0 && n < size) { + zip_copy_leng--; + zip_wp &= zip_WSIZE - 1; + zip_NEEDBITS(8); + buff[off + n++] = zip_slide[zip_wp++] = + zip_GETBITS(8); + zip_DUMPBITS(8); + } + + if(zip_copy_leng == 0) + zip_method = -1; // done + return n; +} + +var zip_inflate_fixed = function(buff, off, size) { + /* decompress an inflated type 1 (fixed Huffman codes) block. We should + either replace this with a custom decoder, or at least precompute the + Huffman tables. */ + + // if first time, set up tables for fixed blocks + if(zip_fixed_tl == null) { + var i; // temporary variable + var l = new Array(288); // length list for huft_build + var h; // zip_HuftBuild + + // literal table + for(i = 0; i < 144; i++) + l[i] = 8; + for(; i < 256; i++) + l[i] = 9; + for(; i < 280; i++) + l[i] = 7; + for(; i < 288; i++) // make a complete, but wrong code set + l[i] = 8; + zip_fixed_bl = 7; + + h = new zip_HuftBuild(l, 288, 257, zip_cplens, zip_cplext, + zip_fixed_bl); + if(h.status != 0) { + alert("HufBuild error: "+h.status); + return -1; + } + zip_fixed_tl = h.root; + zip_fixed_bl = h.m; + + // distance table + for(i = 0; i < 30; i++) // make an incomplete code set + l[i] = 5; + zip_fixed_bd = 5; + + h = new zip_HuftBuild(l, 30, 0, zip_cpdist, zip_cpdext, zip_fixed_bd); + if(h.status > 1) { + zip_fixed_tl = null; + alert("HufBuild error: "+h.status); + return -1; + } + zip_fixed_td = h.root; + zip_fixed_bd = h.m; + } + + zip_tl = zip_fixed_tl; + zip_td = zip_fixed_td; + zip_bl = zip_fixed_bl; + zip_bd = zip_fixed_bd; + return zip_inflate_codes(buff, off, size); +} + +var zip_inflate_dynamic = function(buff, off, size) { + // decompress an inflated type 2 (dynamic Huffman codes) block. + var i; // temporary variables + var j; + var l; // last length + var n; // number of lengths to get + var t; // (zip_HuftNode) literal/length code table + var nb; // number of bit length codes + var nl; // number of literal/length codes + var nd; // number of distance codes + var ll = new Array(286+30); // literal/length and distance code lengths + var h; // (zip_HuftBuild) + + for(i = 0; i < ll.length; i++) + ll[i] = 0; + + // read in table lengths + zip_NEEDBITS(5); + nl = 257 + zip_GETBITS(5); // number of literal/length codes + zip_DUMPBITS(5); + zip_NEEDBITS(5); + nd = 1 + zip_GETBITS(5); // number of distance codes + zip_DUMPBITS(5); + zip_NEEDBITS(4); + nb = 4 + zip_GETBITS(4); // number of bit length codes + zip_DUMPBITS(4); + if(nl > 286 || nd > 30) + return -1; // bad lengths + + // read in bit-length-code lengths + for(j = 0; j < nb; j++) + { + zip_NEEDBITS(3); + ll[zip_border[j]] = zip_GETBITS(3); + zip_DUMPBITS(3); + } + for(; j < 19; j++) + ll[zip_border[j]] = 0; + + // build decoding table for trees--single level, 7 bit lookup + zip_bl = 7; + h = new zip_HuftBuild(ll, 19, 19, null, null, zip_bl); + if(h.status != 0) + return -1; // incomplete code set + + zip_tl = h.root; + zip_bl = h.m; + + // read in literal and distance code lengths + n = nl + nd; + i = l = 0; + while(i < n) { + zip_NEEDBITS(zip_bl); + t = zip_tl.list[zip_GETBITS(zip_bl)]; + j = t.b; + zip_DUMPBITS(j); + j = t.n; + if(j < 16) // length of code in bits (0..15) + ll[i++] = l = j; // save last length in l + else if(j == 16) { // repeat last length 3 to 6 times + zip_NEEDBITS(2); + j = 3 + zip_GETBITS(2); + zip_DUMPBITS(2); + if(i + j > n) + return -1; + while(j-- > 0) + ll[i++] = l; + } else if(j == 17) { // 3 to 10 zero length codes + zip_NEEDBITS(3); + j = 3 + zip_GETBITS(3); + zip_DUMPBITS(3); + if(i + j > n) + return -1; + while(j-- > 0) + ll[i++] = 0; + l = 0; + } else { // j == 18: 11 to 138 zero length codes + zip_NEEDBITS(7); + j = 11 + zip_GETBITS(7); + zip_DUMPBITS(7); + if(i + j > n) + return -1; + while(j-- > 0) + ll[i++] = 0; + l = 0; + } + } + + // build the decoding tables for literal/length and distance codes + zip_bl = zip_lbits; + h = new zip_HuftBuild(ll, nl, 257, zip_cplens, zip_cplext, zip_bl); + if(zip_bl == 0) // no literals or lengths + h.status = 1; + if(h.status != 0) { + if(h.status == 1) + ;// **incomplete literal tree** + return -1; // incomplete code set + } + zip_tl = h.root; + zip_bl = h.m; + + for(i = 0; i < nd; i++) + ll[i] = ll[i + nl]; + zip_bd = zip_dbits; + h = new zip_HuftBuild(ll, nd, 0, zip_cpdist, zip_cpdext, zip_bd); + zip_td = h.root; + zip_bd = h.m; + + if(zip_bd == 0 && nl > 257) { // lengths but no distances + // **incomplete distance tree** + return -1; + } + + if(h.status == 1) { + ;// **incomplete distance tree** + } + if(h.status != 0) + return -1; + + // decompress until an end-of-block code + return zip_inflate_codes(buff, off, size); +} + +var zip_inflate_start = function() { + var i; + + if(zip_slide == null) + zip_slide = new Array(2 * zip_WSIZE); + zip_wp = 0; + zip_bit_buf = 0; + zip_bit_len = 0; + zip_method = -1; + zip_eof = false; + zip_copy_leng = zip_copy_dist = 0; + zip_tl = null; +} + +var zip_inflate_internal = function(buff, off, size) { + // decompress an inflated entry + var n, i; + + n = 0; + while(n < size) { + if(zip_eof && zip_method == -1) + return n; + + if(zip_copy_leng > 0) { + if(zip_method != zip_STORED_BLOCK) { + // STATIC_TREES or DYN_TREES + while(zip_copy_leng > 0 && n < size) { + zip_copy_leng--; + zip_copy_dist &= zip_WSIZE - 1; + zip_wp &= zip_WSIZE - 1; + buff[off + n++] = zip_slide[zip_wp++] = + zip_slide[zip_copy_dist++]; + } + } else { + while(zip_copy_leng > 0 && n < size) { + zip_copy_leng--; + zip_wp &= zip_WSIZE - 1; + zip_NEEDBITS(8); + buff[off + n++] = zip_slide[zip_wp++] = zip_GETBITS(8); + zip_DUMPBITS(8); + } + if(zip_copy_leng == 0) + zip_method = -1; // done + } + if(n == size) + return n; + } + + if(zip_method == -1) { + if(zip_eof) + break; + + // read in last block bit + zip_NEEDBITS(1); + if(zip_GETBITS(1) != 0) + zip_eof = true; + zip_DUMPBITS(1); + + // read in block type + zip_NEEDBITS(2); + zip_method = zip_GETBITS(2); + zip_DUMPBITS(2); + zip_tl = null; + zip_copy_leng = 0; + } + + switch(zip_method) { + case 0: // zip_STORED_BLOCK + i = zip_inflate_stored(buff, off + n, size - n); + break; + + case 1: // zip_STATIC_TREES + if(zip_tl != null) + i = zip_inflate_codes(buff, off + n, size - n); + else + i = zip_inflate_fixed(buff, off + n, size - n); + break; + + case 2: // zip_DYN_TREES + if(zip_tl != null) + i = zip_inflate_codes(buff, off + n, size - n); + else + i = zip_inflate_dynamic(buff, off + n, size - n); + break; + + default: // error + i = -1; + break; + } + + if(i == -1) { + if(zip_eof) + return 0; + return -1; + } + n += i; + } + return n; +} + +var zip_inflate = function(str) { + var i, j; + + zip_inflate_start(); + zip_inflate_data = str; + zip_inflate_pos = 0; + + var buff = new Array(1024); + var aout = []; + while((i = zip_inflate_internal(buff, 0, buff.length)) > 0) { + var cbuf = new Array(i); + for(j = 0; j < i; j++){ + cbuf[j] = String.fromCharCode(buff[j]); + } + aout[aout.length] = cbuf.join(""); + } + zip_inflate_data = null; // G.C. + return aout.join(""); +} + +if (! ctx.RawDeflate) ctx.RawDeflate = {}; +ctx.RawDeflate.inflate = zip_inflate; + +})(this);