nakarte

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

commit 42bca19639bcaaa7c2fb7750558f20485475c040
parent 74477bcb8a1e0330441a62326ac4d3d2ebd5e189
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Sat, 25 Feb 2017 00:29:04 +0300

added jnx control

Diffstat:
Msrc/App.js | 7++++++-
Asrc/lib/leaflet.control.jnx/binary-stream.js | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.jnx/hash-state.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.jnx/index.js | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.jnx/jnx-encoder.js | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.jnx/jnx-maker.js | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.jnx/selector.js | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.control.jnx/style.css | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/leaflet.control.printPages/map-render.js | 38+-------------------------------------
Msrc/lib/leaflet.layer.rasterize/TileLayer.js | 14++++++++++----
Msrc/lib/leaflet.layer.rasterize/imgFromDataString.js | 5++++-
Msrc/lib/leaflet.layer.rasterize/index.js | 44++++++++++++++++++++++++++++++++++++++++++--
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) &mdash; ${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