commit 42bca19639bcaaa7c2fb7750558f20485475c040
parent 74477bcb8a1e0330441a62326ac4d3d2ebd5e189
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Sat, 25 Feb 2017 00:29:04 +0300
added jnx control
Diffstat:
12 files changed, 751 insertions(+), 45 deletions(-)
diff --git a/src/App.js b/src/App.js
@@ -23,7 +23,8 @@ import hashState from 'lib/leaflet.hashState/hashState';
import raiseControlsOnFocus from 'lib/leaflet.controls.raise-on-focus';
import getLayers from 'layers';
import 'lib/leaflet.control.layers.events';
-
+import 'lib/leaflet.control.jnx';
+import 'lib/leaflet.control.jnx/hash-state';
function setUp() {
@@ -77,6 +78,10 @@ function setUp() {
printControl.setMinimized();
}
+ new L.Control.JNX(layersControl, {position: 'bottomleft'})
+ .addTo(map)
+ .enableHashState('j');
+
/////////// controls bottom-right corner
tracklist.addTo(map);
diff --git a/src/lib/leaflet.control.jnx/binary-stream.js b/src/lib/leaflet.control.jnx/binary-stream.js
@@ -0,0 +1,90 @@
+import utf8 from 'utf8'
+
+class BinStream {
+ constructor(size, littlEndian) {
+ this.maxSize = 1024;
+ this.dv = new DataView(new ArrayBuffer(this.maxSize));
+ this._pos = 0;
+ this.size = 0;
+ this.littlEndian = littlEndian;
+ }
+
+ grow() {
+ this.maxSize *= 2;
+ const old_buffer = this.dv.buffer;
+ this.dv = new DataView(new ArrayBuffer(this.maxSize));
+ const newAr = new Uint8Array(this.dv.buffer);
+ const oldAr = new Uint8Array(old_buffer);
+ for (let i = 0; i < oldAr.length; i++) {
+ newAr[i] = oldAr[i];
+ }
+ };
+
+ checkSize(size) {
+ if (this._pos + size >= this.maxSize) {
+ this.grow();
+ }
+ const newPos = this._pos + size;
+ this.size = (newPos > this.size) ? newPos : this.size;
+ };
+
+ writeUint8(value) {
+ this.checkSize(1);
+ this.dv.setUint8(this._pos, value);
+ this._pos += 1;
+ };
+
+ writeInt8(value) {
+ this.checkSize(1);
+ this.dv.setInt8(this._pos, value);
+ this._pos += 1;
+ };
+
+ writeInt16(value) {
+ this.checkSize(2);
+ this.dv.setInt16(this._pos, value, this.littlEndian);
+ this._pos += 2;
+ };
+
+ writeUint16(value) {
+ this.checkSize(2);
+ this.dv.setUint16(this._pos, value, this.littlEndian);
+ this._pos += 2;
+ };
+
+ writeInt32(value) {
+ this.checkSize(4);
+ this.dv.setInt32(this._pos, value, this.littlEndian);
+ this._pos += 4;
+ };
+
+ writeUint32(value) {
+ this.checkSize(4);
+ this.dv.setUint32(this._pos, value, this.littlEndian);
+ this._pos += 4;
+ };
+
+ writeString(s, zeroTerminated) {
+ s = utf8.encode(s);
+ for (let i = 0; i < s.length; i++) {
+ this.writeUint8(s.charCodeAt(i));
+ }
+ if (zeroTerminated) {
+ this.writeUint8(0);
+ }
+ };
+
+ tell() {
+ return this._pos;
+ };
+
+ seek(pos) {
+ this._pos = pos;
+ };
+
+ getBuffer() {
+ return this.dv.buffer.slice(0, this.size);
+ };
+}
+
+export {BinStream}
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.jnx/hash-state.js b/src/lib/leaflet.control.jnx/hash-state.js
@@ -0,0 +1,51 @@
+import L from 'leaflet';
+import '../leaflet.control.layers.hotkeys/index';
+import 'lib/leaflet.hashState/leaflet.hashState';
+
+L.Control.JNX.include(L.Mixin.HashState);
+L.Control.JNX.include({
+ stateChangeEvents: ['selectionchange'],
+
+ serializeState: function() {
+ let state;
+ if (this._selector) {
+ const bounds = this._selector.getBounds();
+ state = [
+ bounds.getSouth().toFixed(5),
+ bounds.getWest().toFixed(5),
+ bounds.getNorth().toFixed(5),
+ bounds.getEast().toFixed(5)];
+ }
+ return state;
+ },
+
+ unserializeState: function(values) {
+ function validateFloat(value, min, max) {
+ value = parseFloat(value);
+ if (isNaN(value) || value < min || value > max) {
+ throw new Error('INVALID VALUE');
+ }
+ return value;
+ }
+
+ if (values && values.length === 4) {
+ try {
+ var south = validateFloat(values[0], -86, 86),
+ west = validateFloat(values[1], -180, 180),
+ north = validateFloat(values[2], -86, 86),
+ east = validateFloat(values[3], -180, 180);
+ } catch (e) {
+ if (e.message === 'INVALID VALUE') {
+ return false;
+ }
+ throw e;
+ }
+ this.removeSelector();
+ this.addSelector([[south, west], [north, east]]);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+);
diff --git a/src/lib/leaflet.control.jnx/index.js b/src/lib/leaflet.control.jnx/index.js
@@ -0,0 +1,147 @@
+import L from 'leaflet';
+import ko from 'knockout';
+import './style.css';
+import 'lib/leaflet.control.commons';
+import {RectangleSelect} from './selector';
+import Contextmenu from 'lib/contextmenu';
+import {makeJnxFromLayer} from './jnx-maker';
+import {saveAs} from 'browser-filesaver';
+import {notify} from 'lib/notifications';
+
+L.Control.JNX = L.Control.extend({
+ includes: L.Mixin.Events,
+
+ initialize: function(layersControl, options) {
+ L.Control.prototype.initialize.call(this, options);
+ this._layersControl = layersControl;
+ this.makingJnx = ko.observable(false);
+ this.downloadProgressRange = ko.observable(1);
+ this.downloadProgressDone = ko.observable(0);
+ this.contextMenu = new Contextmenu(() => this.makeMenuItems());
+ },
+
+ getLayerForJnx: function() {
+ let selectedLayer = null;
+ this._map.eachLayer((layer) => {
+ if (layer.options && layer.options.jnx) {
+ selectedLayer = layer;
+ }
+ }
+ );
+ return selectedLayer;
+ },
+
+ getLayerName: function(layer) {
+ const layerRec = this._layersControl._getLayer(L.stamp(layer));
+ if (layerRec) {
+ return layerRec.name;
+ } else {
+ return 'Unknown layer';
+ }
+
+ },
+
+ estimateTilesNumber: function(zoom) {
+ const bounds = this._selector.getBounds();
+ const topLeftTile = this._map.project(bounds.getNorthWest(), zoom).divideBy(256).floor();
+ const bottomRightTile = this._map.project(bounds.getSouthEast(), zoom).divideBy(256).ceil();
+ return Math.ceil((bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y) * 4 / 3);
+ },
+
+ makeMenuItems: function() {
+ const layer = this.getLayerForJnx();
+ if (!layer) {
+ return [{text: 'No supported layer'}];
+ }
+ const layerName = this.getLayerName(layer);
+ const maxLevel = layer.options.maxNativeZoom || layer.options.maxZoom || 18;
+ const minLevel = Math.max(0, maxLevel - 6);
+
+ const equatorLength = 40075016;
+ const lat = this._selector.getBounds().getCenter().lat;
+ let metersPerPixel = equatorLength / Math.pow(2, maxLevel) / 256 * Math.cos(lat / 180 * Math.PI);
+
+ const items = [{text: `<b>${layerName}</b>`}];
+ for (let zoom = maxLevel; zoom >= minLevel; zoom -= 1) {
+ let tilesCount = this.estimateTilesNumber(zoom);
+ let fileSizeMb = tilesCount * 0.02;
+ items.push({
+ text: `Zoom ${zoom} (${metersPerPixel.toFixed(2)} m/pixel) — ${tilesCount} (~${fileSizeMb.toFixed(1)} Mb)`,
+ callback: () => this.makeJnx(layer, layerName, zoom)
+ }
+ );
+ metersPerPixel *= 2;
+ }
+ return items;
+ },
+
+ notifyProgress: function(value, maxValue) {
+ this.downloadProgressDone(this.downloadProgressDone() + value);
+ this.downloadProgressRange(maxValue);
+ },
+
+ makeJnx: function(layer, layerName, zoom) {
+ this.makingJnx(true);
+ this.downloadProgressDone(0);
+
+ const bounds = this._selector.getBounds();
+ const sanitizedLayerName = layerName.toLowerCase().replace(/[ ()]+/, '_');
+ const fileName = `nakarte.tk_${sanitizedLayerName}_z${zoom}.jnx`;
+ makeJnxFromLayer(layer, layerName, zoom, bounds, this.notifyProgress.bind(this))
+ .then((fileData) => saveAs(fileData, fileName, true))
+ .catch((e) => notify(e.message))
+ .then(() => this.makingJnx(false));
+ },
+
+ onAdd: function(map) {
+ this._map = map;
+ const container = this._container = L.DomUtil.create('div', 'leaflet-control leaflet-control-jnx');
+ container.innerHTML = `
+ <a class="button" data-bind="visible: !makingJnx(), click: onButtonClicked">JNX</a>
+ <div data-bind="
+ component:{
+ name: 'progress-indicator',
+ params: {progressRange: downloadProgressRange, progressDone: downloadProgressDone}
+ },
+ visible: makingJnx()"></div>`;
+ ko.applyBindings(this, container);
+ this._stopContainerEvents();
+ return container;
+ },
+
+ removeSelector: function() {
+ if (this._selector) {
+ this._map.removeLayer(this._selector);
+ this._selector = null;
+ this.fire('selectionchange');
+ }
+ },
+
+ addSelector: function(bounds) {
+ if (!bounds) {
+ bounds = this._map.getBounds().pad(-0.25);
+ }
+ this._selector = new RectangleSelect(bounds)
+ .addTo(this._map)
+ .on('change', () => this.fire('selectionchange'))
+ .on('click contextmenu', (e) => this.contextMenu.show(e));
+ this.fire('selectionchange');
+ },
+
+
+ onButtonClicked: function() {
+ if (this._selector) {
+ if (this._selector.getBounds().intersects(this._map.getBounds().pad(-0.05))) {
+ this.removeSelector();
+ } else {
+ this.removeSelector();
+ this.addSelector();
+ }
+
+ } else {
+ this.addSelector();
+ }
+ },
+ }
+);
+
diff --git a/src/lib/leaflet.control.jnx/jnx-encoder.js b/src/lib/leaflet.control.jnx/jnx-encoder.js
@@ -0,0 +1,136 @@
+import L from 'leaflet';
+import {BinStream} from './binary-stream';
+
+function jnxCoordinates(extents) {
+ function toJnx(x) {
+ return Math.round(x / 180.0 * 0x7fffffff);
+ }
+
+ return [toJnx(extents.north), toJnx(extents.east), toJnx(extents.south), toJnx(extents.west)];
+}
+
+const JnxWriter = L.Class.extend({
+ initialize: function(productName, productId, zOrder) {
+ this.tiles = {};
+ this.productName = productName || 'Raster map';
+ this.productId = productId || 0;
+ this.zOrder = zOrder;
+ },
+
+ addTile: function(tileData, level, latLngBounds) {
+ this.tiles[level] = this.tiles[level] || [];
+ tileData = new Blob([tileData]).slice(2);
+ const extents = {
+ west: latLngBounds.getWest(),
+ north: latLngBounds.getNorth(),
+ south: latLngBounds.getSouth(),
+ east: latLngBounds.getEast(),
+
+ };
+ this.tiles[level].push({data: tileData, extents: extents});
+ },
+
+ getJnx: function() {
+ const HEADER_SIZE = 52,
+ LEVEL_INFO_SIZE = 17,
+ TILE_INFO_SIZE = 28;
+
+ let west = 1e10,
+ east = -1e10,
+ north = -1e10,
+ south = 1e10,
+ levels_n = Object.keys(this.tiles).length,
+ level, tiles, extents,
+ i, tile;
+ for (let levelTiles of Object.values(this.tiles)) {
+ for (let tile of levelTiles) {
+ west = (west < tile.extents.west) ? west : tile.extents.west;
+ east = (east > tile.extents.east) ? east : tile.extents.east;
+ north = (north > tile.extents.north) ? north : tile.extents.north;
+ south = (south < tile.extents.south) ? south : tile.extents.south;
+ }
+ }
+ const stream = new BinStream(1024, true);
+ // header
+ stream.writeInt32(4); // version
+ stream.writeInt32(0); // device id
+ extents = jnxCoordinates({south: south, north: north, west: west, east: east});
+ stream.writeInt32(extents[0]); // north
+ stream.writeInt32(extents[1]); // west
+ stream.writeInt32(extents[2]); // south
+ stream.writeInt32(extents[3]); // east
+ stream.writeInt32(levels_n); // number of zoom levels
+ stream.writeInt32(0); //expiration date
+ stream.writeInt32(this.productId);
+ stream.writeInt32(0); // tiles CRC32
+ stream.writeInt32(0); // signature version
+ stream.writeUint32(0); // signature offset
+ stream.writeInt32(this.zOrder);
+ stream.seek(HEADER_SIZE + LEVEL_INFO_SIZE * levels_n);
+ // map description
+ stream.writeInt32(9); // section version
+ stream.writeString('12345678-1234-1234-1234-123456789ABC', true); // GUID
+ stream.writeString(this.productName, true);
+ stream.writeString('', true);
+ stream.writeInt16(this.productId);
+ stream.writeString(this.productName, true);
+ stream.writeInt32(levels_n);
+ // levels descriptions
+ for (level of Object.keys(this.tiles)) {
+ stream.writeString('', true);
+ stream.writeString('', true);
+ stream.writeString('', true);
+ stream.writeInt32(level);
+ }
+ let tileDescriptorOffset = stream.tell();
+ // level info
+ let jnxScale;
+ stream.seek(HEADER_SIZE);
+ for (level of Object.keys(this.tiles)) {
+ level = parseInt(level, 10);
+ stream.writeInt32(this.tiles[level].length);
+ stream.writeUint32(tileDescriptorOffset);
+ //jnxScale = JnxScales[level + 3];
+ jnxScale = 34115555 / (Math.pow(2, level)) * Math.cos((north + south) / 2 / 180 * Math.PI) / 1.1;
+ stream.writeInt32(jnxScale);
+ stream.writeInt32(2);
+ stream.writeUint8(0);
+ tileDescriptorOffset += TILE_INFO_SIZE * this.tiles[level].length;
+ }
+ // tiles descriptors
+ stream.seek(stream.size);
+ let tileDataOffset = tileDescriptorOffset;
+ for (level of Object.keys(this.tiles)) {
+ tiles = this.tiles[level];
+ for (i = 0; i < tiles.length; i++) {
+ tile = tiles[i];
+ extents = jnxCoordinates(tile.extents);
+ stream.writeInt32(extents[0]); // north
+ stream.writeInt32(extents[1]); // west
+ stream.writeInt32(extents[2]); // south
+ stream.writeInt32(extents[3]); // east
+ stream.writeInt16(256); // width
+ stream.writeInt16(256); // height
+ stream.writeInt32(tile.data.size);
+ stream.writeUint32(tileDataOffset);
+ tileDataOffset += tile.data.size;
+ }
+ }
+
+ const blob = [];
+ blob.push(stream.getBuffer());
+ for (level of Object.keys(this.tiles)) {
+ tiles = this.tiles[level];
+ for (i = 0; i < tiles.length; i++) {
+ tile = tiles[i];
+ blob.push(tile.data);
+ }
+ }
+
+ blob.push('BirdsEye');
+ return new Blob(blob);
+ }
+ }
+);
+
+export {JnxWriter};
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.jnx/jnx-maker.js b/src/lib/leaflet.control.jnx/jnx-maker.js
@@ -0,0 +1,118 @@
+import L from 'leaflet';
+import {JnxWriter} from './jnx-encoder';
+import {getTempMap, disposeMap} from 'lib/leaflet.layer.rasterize';
+import {XHRQueue} from 'lib/xhr-promise';
+import {arrayBufferToString, stringToArrayBuffer} from 'lib/binary-strings';
+import {formatXhrError} from 'lib/notifications';
+
+const defaultXHROptions = {
+ responseType: 'arraybuffer',
+ timeout: 10000,
+ isResponseSuccess: (xhr) => xhr.status === 200 || xhr.status === 404
+};
+
+function imageFromarrayBuffer(arr) {
+ const dataUrl = 'data:image/png;base64,' + btoa(arrayBufferToString(arr));
+ const image = new Image();
+ return new Promise(function(resolve, reject) {
+ image.onload = () => resolve(image);
+ image.onerror = () => reject(new Error('Tile image corrupt'));
+ image.src = dataUrl;
+ }
+ )
+}
+
+async function convertToJpeg(image) {
+ try {
+ image = await imageFromarrayBuffer(image);
+ } catch (e) {
+ return null;
+ }
+ const canvas = document.createElement("canvas");
+ canvas.width = image.width;
+ canvas.height = image.height;
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(image, 0, 0);
+ const dataURL = canvas.toDataURL("image/jpeg");
+ const s = atob(dataURL.replace(/^data:image\/jpeg;base64,/, ""));
+ return stringToArrayBuffer(s);
+}
+
+function ensureImageJpg(image) {
+ if (!image) {
+ return null;
+ }
+ if (arrayBufferToString(image.slice(0, 4)) === '\x89PNG' &&
+ arrayBufferToString(image.slice(-8)) === 'IEND\xae\x42\x60\x82') {
+ return convertToJpeg(image)
+ } else if (arrayBufferToString(image.slice(0, 2)) === '\xff\xd8' &&
+ arrayBufferToString(image.slice(-2)) === '\xff\xd9') {
+ return Promise.resolve(image);
+ } else {
+ return null;
+ }
+}
+
+async function makeJnxFromLayer(srcLayer, layerName, maxZoomLevel, latLngBounds, progress) {
+ const jnxProductId = L.stamp(srcLayer);
+ const jnxZOrder = Math.min(jnxProductId, 100);
+ const writer = new JnxWriter(layerName, jnxProductId, jnxZOrder);
+ const xhrQueue = new XHRQueue();
+ let doStop = false;
+ let error;
+ const minZoomLevel = Math.max(maxZoomLevel - 4, 0);
+ let progressWeight = 1;
+ for (let zoom = maxZoomLevel; zoom >= minZoomLevel; zoom--) {
+ let pixelBounds = L.bounds(
+ L.CRS.EPSG3857.latLngToPoint(latLngBounds.getNorthWest(), zoom).round(),
+ L.CRS.EPSG3857.latLngToPoint(latLngBounds.getSouthEast(), zoom).round()
+ );
+
+ let promises = [];
+ let layer = srcLayer.cloneForPrint({xhrQueue});
+ let tempMap = getTempMap(zoom, layer._rasterizeNeedsFullSizeMap, pixelBounds);
+ tempMap.addLayer(layer);
+ let {iterateTilePromises, count: tilesCount} = await layer.getTilesInfo({
+ xhrOptions: defaultXHROptions,
+ pixelBounds,
+ rawData: true
+ }
+ );
+ for (let tilePromiseRec of iterateTilePromises()) {
+ promises.push(tilePromiseRec);
+ }
+ for (let {tilePromise} of promises) {
+ let imageRec;
+ try {
+ imageRec = await tilePromise;
+ } catch (xhr) {
+ error = new Error(formatXhrError(xhr, 'map tile'));
+ doStop = true;
+ break;
+ }
+ let xhr = imageRec.image;
+ if (xhr.status !== 200) {
+ continue;
+ }
+ let image = await ensureImageJpg(xhr.response);
+ if (!image) {
+ error = new Error('Tile image invalid');
+ doStop = true;
+ break;
+ }
+ writer.addTile(image, zoom, imageRec.latLngBounds);
+ progress(progressWeight / tilesCount, 4 / 3);
+ }
+ disposeMap(tempMap);
+ if (doStop) {
+ promises.forEach((promiseRec) => promiseRec.abortLoading());
+ throw error;
+ }
+ progressWeight /= 4;
+ }
+
+ return writer.getJnx();
+}
+
+
+export {makeJnxFromLayer};
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.jnx/selector.js b/src/lib/leaflet.control.jnx/selector.js
@@ -0,0 +1,87 @@
+import L from 'leaflet';
+
+const RectangleSelect = L.Rectangle.extend({
+ includes: L.Mixin.Events,
+
+ options: {
+ opacity: 1,
+ weight: 0.5,
+ fillOpacity: 0.2,
+ color: '#3388ff',
+ fillColor: '#3388ff',
+
+ },
+
+ onAdd: function(map) {
+ L.Rectangle.prototype.onAdd.call(this, map);
+ this.markers = [];
+ for (let position of ['top', 'right', 'bottom', 'left']) {
+ let marker = L.marker([0, 0], {
+ icon: L.divIcon({className: `leaflet-rectangle-select-edge edge-${position}`}),
+ draggable: true
+ }
+ )
+ .addTo(map);
+ marker.on({
+ 'drag': this.onMarkerDrag,
+ 'dragend': this.onMarkerDragEnd
+ }, this
+ );
+ this.markers[position] = marker;
+ }
+ this.placeMarkers();
+ map.on('zoomend', this.placeMarkers, this);
+ },
+
+ placeMarkers: function() {
+ const bounds = this.getBounds();
+ const topLeftPixel = this._map.project(bounds.getNorthWest());
+ const bottomRightPixel = this._map.project(bounds.getSouthEast());
+ const size = bottomRightPixel.subtract(topLeftPixel);
+ let center = topLeftPixel.add(size.divideBy(2));
+ center = this._map.unproject(center);
+ this.markers['top'].setLatLng([bounds.getNorth(), center.lng]);
+ this.markers['top']._icon.style.width = `${size.x}px`;
+ this.markers['top']._icon.style.marginLeft = `-${size.x / 2}px`;
+ this.markers['right'].setLatLng([center.lat, bounds.getEast()]);
+ this.markers['right']._icon.style.height = `${size.y}px`;
+ this.markers['right']._icon.style.marginTop = `-${size.y / 2}px`;
+ this.markers['bottom'].setLatLng([bounds.getSouth(), center.lng]);
+ this.markers['bottom']._icon.style.width = `${size.x}px`;
+ this.markers['bottom']._icon.style.marginLeft = `-${size.x / 2}px`;
+ this.markers['left'].setLatLng([center.lat, bounds.getWest()]);
+ this.markers['left']._icon.style.height = `${size.y}px`;
+ this.markers['left']._icon.style.marginTop = `-${size.y / 2}px`;
+ },
+
+ onRemove: function(map) {
+ for (let marker of Object.values(this.markers)) {
+ this._map.removeLayer(marker);
+ }
+ this.markers = null;
+ map.off('zoomend', this.placeMarkers, this);
+ L.Rectangle.prototype.onRemove.call(this, map);
+ },
+
+ setBoundsFromMarkers: function() {
+ this.setBounds(
+ [
+ [this.markers['top'].getLatLng().lat, this.markers['left'].getLatLng().lng],
+ [this.markers['bottom'].getLatLng().lat, this.markers['right'].getLatLng().lng]
+ ]
+ );
+
+ },
+ onMarkerDrag: function() {
+ this.setBoundsFromMarkers();
+ },
+
+ onMarkerDragEnd: function() {
+ this.setBoundsFromMarkers();
+ this.placeMarkers();
+ this.fire('change');
+ }
+ }
+);
+
+export {RectangleSelect};
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.jnx/style.css b/src/lib/leaflet.control.jnx/style.css
@@ -0,0 +1,55 @@
+.leaflet-control-jnx .ko-progress{
+ width: 200px;
+ background-color: white;
+}
+
+.leaflet-control-jnx .button {
+ display: inline-block;
+ background-color: white;
+ height: 26px;
+ border-radius: 4px 4px 4px 4px;
+ border: 1px solid #ccc;
+ cursor: pointer;
+ margin-right: 6px;
+ padding: 0 1em;
+ line-height: 26px;
+ font-weight: bold;
+ color: #333;
+}
+
+.leaflet-control-jnx .button:hover {
+ background-color: #f4f4f4;
+}
+
+/*.leaflet-rectangle-select-edge {*/
+ /*border: 1px solid black;*/
+ /*background-color: red;*/
+ /*opacity: 0.5;*/
+/*}*/
+
+.leaflet-rectangle-select-edge.edge-top, .leaflet-rectangle-select-edge.edge-bottom {
+ height: 30px !important;
+ margin-top: -15px !important;
+}
+
+.leaflet-rectangle-select-edge.edge-left, .leaflet-rectangle-select-edge.edge-right {
+ width: 30px !important;
+ margin-left: -15px !important;
+}
+
+.leaflet-rectangle-select-edge.edge-top {
+ cursor: n-resize !important;
+}
+
+.leaflet-rectangle-select-edge.edge-bottom {
+ cursor: s-resize !important;
+}
+
+.leaflet-rectangle-select-edge.edge-left {
+ cursor: w-resize !important;
+}
+
+.leaflet-rectangle-select-edge.edge-right {
+ cursor: e-resize !important;
+}
+
diff --git a/src/lib/leaflet.control.printPages/map-render.js b/src/lib/leaflet.control.printPages/map-render.js
@@ -1,5 +1,5 @@
import L from 'leaflet';
-import 'lib/leaflet.layer.rasterize';
+import {getTempMap, disposeMap} from 'lib/leaflet.layer.rasterize';
import {XHRQueue} from 'lib/xhr-promise';
@@ -167,42 +167,6 @@ class PageComposer {
}
}
-function getTempMap(zoom, fullSize, pixelBounds) {
- const container = L.DomUtil.create('div', '', document.body);
- let width, height, center;
- if (fullSize) {
- const size = pixelBounds.getSize();
- width = size.x;
- height = size.y;
- center = pixelBounds.min.add(size.divideBy(2));
- center = L.CRS.EPSG3857.pointToLatLng(center, zoom);
- } else {
- width = 100;
- height = 100;
- center = L.latLng(0, 0);
- }
-
- Object.assign(container.style, {
- width: `${width}px`,
- height: `${height}px`,
- position: 'absolute',
- left: '0',
- top: '0',
- visibility: 'hidden',
- }
- );
-
- const map = L.map(container, {fadeAnimation: false, zoomAnimation: false, inertia: false});
- map.setView(center, zoom);
- return map;
-}
-
-function disposeMap(map) {
- const container = map._container;
- map.remove();
- L.DomUtil.remove(container);
-}
-
async function* iterateLayersTiles(layers, latLngBounds, destPixelSize, resolution, scale, zooms) {
const defaultXHROptions = {
responseType: 'blob',
diff --git a/src/lib/leaflet.layer.rasterize/TileLayer.js b/src/lib/leaflet.layer.rasterize/TileLayer.js
@@ -27,12 +27,17 @@ const GridLayerGrabMixin = {
let tilePos = this._getTilePos(coords);
const coordsPlusOne = coords.add(L.point(1, 1));
coordsPlusOne.z = coords.z;
- const tileSize = this._getTilePos(coordsPlusOne).subtract(tilePos);
+ const tilePlusOne = this._getTilePos(coordsPlusOne)
+ const tileSize = tilePlusOne.subtract(tilePos);
+ const latLngBounds = L.latLngBounds(
+ this._map.unproject(tilePos.add(this._level.origin)),
+ this._map.unproject(tilePlusOne.add(this._level.origin)));
tilePos = tilePos.add(this._level.origin).subtract(topLeft);
- let {tilePromise, abortLoading} = this.tileImagePromiseFromCoords(this._wrapCoords(coords), printOptions);
+ let {tilePromise, abortLoading} = this.tileImagePromiseFromCoords(
+ this._wrapCoords(coords), printOptions);
yield {
tilePromise: tilePromise.then((image) => {
- return {image, tilePos, tileSize};
+ return {image, tilePos, tileSize, latLngBounds};
}
),
abortLoading
@@ -59,8 +64,9 @@ const TileLayerGrabMixin = L.Util.extend({}, GridLayerGrabMixin, {
url = urlViaCorsProxy(url);
}
let promise = this.options.xhrQueue.put(url, xhrOptions);
+
return {
- tilePromise: promise.then(imgFromDataString),
+ tilePromise: printOptions.rawData ? promise : promise.then(imgFromDataString),
abortLoading: () => promise.abort()
}
}
diff --git a/src/lib/leaflet.layer.rasterize/imgFromDataString.js b/src/lib/leaflet.layer.rasterize/imgFromDataString.js
@@ -2,11 +2,14 @@ function imgFromDataString(xhr) {
if (xhr.status === 200 && xhr.response.size) {
const image = new Image();
let blobUrl = window.URL.createObjectURL(xhr.response);
- const promise = new Promise((resolve) => {
+ const promise = new Promise((resolve, reject) => {
image.onload = () => {
resolve(image);
window.URL.revokeObjectURL(blobUrl);
};
+ image.onerror = () => {
+ reject(new Error('Image corrupt'));
+ }
}
);
image.src = blobUrl;
diff --git a/src/lib/leaflet.layer.rasterize/index.js b/src/lib/leaflet.layer.rasterize/index.js
@@ -1,7 +1,47 @@
+import L from 'leaflet';
import './TileLayer'
import './Bing'
import './Yandex'
import './Google'
import './WestraPasses'
import './CanvasMarkers'
-import './MeasuredLine'
-\ No newline at end of file
+import './MeasuredLine'
+
+function getTempMap(zoom, fullSize, pixelBounds) {
+ const container = L.DomUtil.create('div', '', document.body);
+ let width, height, center;
+ if (fullSize) {
+ const size = pixelBounds.getSize();
+ width = size.x;
+ height = size.y;
+ center = pixelBounds.min.add(size.divideBy(2));
+ center = L.CRS.EPSG3857.pointToLatLng(center, zoom);
+ } else {
+ width = 100;
+ height = 100;
+ center = L.latLng(0, 0);
+ }
+
+ Object.assign(container.style, {
+ width: `${width}px`,
+ height: `${height}px`,
+ position: 'absolute',
+ left: '0',
+ top: '0',
+ visibility: 'hidden',
+ }
+ );
+
+ const map = L.map(container, {fadeAnimation: false, zoomAnimation: false, inertia: false});
+ map.setView(center, zoom);
+ return map;
+}
+
+function disposeMap(map) {
+ const container = map._container;
+ map.remove();
+ L.DomUtil.remove(container);
+}
+
+
+export {getTempMap, disposeMap}
+\ No newline at end of file