map-render.js (10799B)
1 import L from 'leaflet'; 2 import {getTempMap, disposeMap} from '~/lib/leaflet.layer.rasterize'; 3 import {XHRQueue} from '~/lib/xhr-promise'; 4 5 function getLayersForPrint(map, xhrQueue) { 6 function getZIndex(el) { 7 return parseInt(window.getComputedStyle(el).zIndex, 10) || 0; 8 } 9 10 function compare(i1, i2) { 11 if (i1 < i2) { 12 return -1; 13 } else if (i1 > i2) { 14 return 1; 15 } 16 return 0; 17 } 18 19 function getLayerZOrder(layer) { 20 let el = layer._container || layer._path; 21 if (!el) { 22 throw new TypeError('Unsupported layer type'); 23 } 24 const order = []; 25 while (el !== layer._map._container) { 26 order.push(getZIndex(el)); 27 el = el.parentNode; 28 } 29 return order.reverse(); 30 } 31 32 function compareArrays(ar1, ar2) { 33 const len = Math.min(ar1.length, ar2.length); 34 for (let i = 0; i < len; i++) { 35 let c = compare(ar1[i], ar2[i]); 36 if (c) { 37 return c; 38 } 39 } 40 return compare(ar1.length, ar2.length); 41 } 42 43 function compareLayersOrder(layer1, layer2) { 44 return compareArrays(getLayerZOrder(layer1), getLayerZOrder(layer2)); 45 } 46 47 let layers = []; 48 map.eachLayer((layer) => { 49 if (layer.options.print) { 50 layers.push(layer); 51 } else { 52 if (layer.meta && layer.options.isOverlay === false && !layer.options.isWrapper) { 53 throw new Error(`Print disabled for layer ${layer.meta.title}`); 54 } 55 } 56 } 57 ); 58 layers.sort(compareLayersOrder); 59 layers = layers.map((l) => l.cloneForPrint({xhrQueue})); 60 return layers; 61 } 62 63 function blendMultiplyCanvas(src, dest) { 64 if (src.width !== dest.width || src.height !== dest.height) { 65 throw new Error('Canvas size mismatch'); 66 } 67 var s_data = src.getContext('2d').getImageData(0, 0, src.width, src.height).data; 68 src.width = 0; 69 src.height = 0; 70 var d_image_data = dest.getContext('2d').getImageData(0, 0, dest.width, dest.height); 71 var d_data = d_image_data.data; 72 var data_length = s_data.length, 73 sr, sg, sb, sa, sa1, 74 dr, dg, db, 75 l; 76 for (var i = 0; i < data_length; i += 4) { 77 sa = s_data[i + 3] / 255; 78 if (sa) { 79 sr = s_data[i]; 80 sg = s_data[i + 1]; 81 sb = s_data[i + 2]; 82 dr = d_data[i]; 83 dg = d_data[i + 1]; 84 db = d_data[i + 2]; 85 86 l = (dr + dg + db) / 3; 87 l = l / 255 * 0.5 + 0.5; 88 sa1 = 1 - sa; 89 dr = sr * l * sa + sa1 * dr; 90 dg = sg * l * sa + sa1 * dg; 91 db = sb * l * sa + sa1 * db; 92 93 d_data[i] = dr; 94 d_data[i + 1] = dg; 95 d_data[i + 2] = db; 96 } 97 } 98 s_data = null; 99 dest.getContext('2d').putImageData(d_image_data, 0, 0); 100 } 101 102 class PageComposer { 103 constructor(destSize, pixelBoundsAtZoom24) { 104 this.destSize = destSize; 105 this.projectedBounds = pixelBoundsAtZoom24; 106 this.currentCanvas = null; 107 this.currentZoom = null; 108 this.currentTileScale = null; 109 this.targetCanvas = this.createCanvas(destSize); 110 } 111 112 createCanvas(size) { 113 const canvas = L.DomUtil.create('canvas'); 114 canvas.width = size.x; 115 canvas.height = size.y; 116 return canvas; 117 } 118 119 putTile(tileInfo) { 120 if (!tileInfo.isOverlay && !tileInfo.image) { 121 return; 122 } 123 let zoom; 124 if (tileInfo.isOverlay) { 125 zoom = tileInfo.overlaySolid ? 'solidOverlay' : 'overlay'; 126 } else { 127 zoom = tileInfo.zoom; 128 } 129 if (zoom !== this.currentZoom || tileInfo.tileScale !== this.currentTileScale) { 130 this.mergeCurrentCanvas(); 131 this.setupCurrentCanvas(zoom, tileInfo.tileScale); 132 } 133 if (tileInfo.isOverlay) { 134 tileInfo.draw(this.currentCanvas); 135 } else { 136 const ctx = this.currentCanvas.getContext('2d'); 137 const {tilePos, tileSize} = tileInfo; 138 ctx.drawImage( 139 tileInfo.image, 140 tilePos.x * tileInfo.tileScale, 141 tilePos.y * tileInfo.tileScale, 142 tileSize.x * tileInfo.tileScale, 143 tileSize.y * tileInfo.tileScale 144 ); 145 } 146 } 147 148 setupCurrentCanvas(zoom, tileScale) { 149 let size; 150 if (zoom === 'overlay' || zoom === 'solidOverlay') { 151 size = this.destSize; 152 } else { 153 const q = 1 << (24 - zoom); 154 const 155 topLeft = this.projectedBounds.min.divideBy(q).round(), 156 bottomRight = this.projectedBounds.max.divideBy(q).round(); 157 size = bottomRight.subtract(topLeft).multiplyBy(tileScale); 158 } 159 this.currentCanvas = this.createCanvas(size); 160 this.currentZoom = zoom; 161 this.currentTileScale = tileScale; 162 } 163 164 mergeCurrentCanvas() { 165 if (!this.currentCanvas) { 166 return; 167 } 168 if (this.currentZoom === 'overlay') { 169 blendMultiplyCanvas(this.currentCanvas, this.targetCanvas); 170 } else { 171 this.targetCanvas.getContext('2d').drawImage(this.currentCanvas, 0, 0, 172 this.destSize.x, this.destSize.y 173 ); 174 } 175 this.currentCanvas.width = 0; 176 this.currentCanvas.height = 0; 177 this.currentCanvas = null; 178 } 179 180 getDataUrl() { 181 this.mergeCurrentCanvas(); 182 const dataUrl = this.targetCanvas.toDataURL("image/jpeg"); 183 this.targetCanvas.width = 0; 184 this.targetCanvas.height = 0; 185 this.targetCanvas = null; 186 return dataUrl; 187 } 188 } 189 190 async function* iterateLayersTiles( 191 layers, 192 latLngBounds, 193 destPixelSize, 194 resolution, 195 scale, 196 zooms, 197 pageLabel, 198 pagesCount 199 ) { 200 const defaultXHROptions = { 201 responseType: 'blob', 202 timeout: 20000, 203 isResponseSuccess: (xhr) => xhr.status === 200 || xhr.status === 404 204 }; 205 let doStop; 206 for (let layer of layers) { 207 let zoom; 208 if (layer.options && layer.options.scaleDependent) { 209 zoom = zooms.mapZoom; 210 } else { 211 zoom = zooms.satZoom; 212 } 213 let pixelBounds = L.bounds( 214 L.CRS.EPSG3857.latLngToPoint(latLngBounds.getNorthWest(), zoom).round(), 215 L.CRS.EPSG3857.latLngToPoint(latLngBounds.getSouthEast(), zoom).round() 216 ); 217 let map; 218 if (!layer._layerDummy) { 219 map = getTempMap(zoom, layer._rasterizeNeedsFullSizeMap, pixelBounds); 220 map.addLayer(layer); 221 } 222 let {iterateTilePromises, count, tileScale = 1} = await layer.getTilesInfo({ 223 xhrOptions: defaultXHROptions, 224 pixelBounds, 225 latLngBounds, 226 destPixelSize, 227 resolution, 228 scale, 229 zoom, 230 pageLabel, 231 pagesCount 232 } 233 ); 234 let layerPromises = []; 235 for (let tilePromise of iterateTilePromises()) { 236 layerPromises.push(tilePromise.tilePromise); 237 let progressInc = (layer._printProgressWeight || 1) / count; 238 tilePromise.tilePromise = tilePromise.tilePromise.then((tileInfo) => ({ 239 zoom, 240 progressInc, 241 layer, 242 tileScale, 243 ...tileInfo, 244 })); 245 doStop = yield tilePromise; 246 if (doStop) { 247 tilePromise.abortLoading(); 248 break; 249 } 250 } 251 if (!layer._layerDummy) { 252 if (doStop) { 253 disposeMap(map); 254 break; 255 } else { 256 Promise.all(layerPromises).then(() => disposeMap(map)); 257 } 258 } 259 } 260 } 261 262 async function* promiseQueueBuffer(source, maxActive) { 263 const queue = []; 264 while (queue.length < maxActive) { 265 let {value, done} = await source.next(); 266 if (done) { 267 break; 268 } 269 queue.push(value); 270 } 271 272 while (queue.length) { 273 let doStop = yield queue.shift(); 274 if (doStop) { 275 let {value, done} = await source.next(true); 276 if (!done) { 277 queue.push(value); 278 } 279 for (let {abortLoading} of queue) { 280 abortLoading(); 281 } 282 283 return; 284 } 285 let {value, done} = await source.next(); 286 if (!done) { 287 queue.push(value); 288 } 289 } 290 } 291 292 async function renderPages({map, pages, zooms, resolution, scale, progressCallback, decorationLayers}) { 293 const xhrQueue = new XHRQueue(); 294 const layers = getLayersForPrint(map, xhrQueue); 295 layers.push(...decorationLayers); 296 let progressRange = 0; 297 for (let layer of layers) { 298 progressRange += layer._printProgressWeight || 1; 299 } 300 progressRange *= pages.length; 301 const pageImagesInfo = []; 302 const renderedLayers = new Set(); 303 for (let page of pages) { 304 let destPixelSize = page.printSize.multiplyBy(resolution / 25.4).round(); 305 let pixelBounds = L.bounds( 306 map.project(page.latLngBounds.getNorthWest(), 24).round(), 307 map.project(page.latLngBounds.getSouthEast(), 24).round() 308 ); 309 310 const composer = new PageComposer(destPixelSize, pixelBounds); 311 let tilesIterator = await iterateLayersTiles(layers, page.latLngBounds, destPixelSize, resolution, scale, zooms, 312 page.label, pages.length); 313 let queuedTilesIterator = promiseQueueBuffer(tilesIterator, 20); 314 while (true) { 315 let {value: tilePromise, done} = await queuedTilesIterator.next(); 316 if (done) { 317 break; 318 } 319 let tileInfo; 320 try { 321 tileInfo = await tilePromise.tilePromise; 322 } catch (e) { 323 queuedTilesIterator.next(true); 324 throw e; 325 } 326 progressCallback(tileInfo.progressInc, progressRange); 327 composer.putTile(tileInfo); 328 const {image, draw, layer} = tileInfo; 329 if ((image || draw) && !layer._layerDummy) { 330 renderedLayers.add(layer); 331 } 332 } 333 const dataUrl = composer.getDataUrl(); 334 let data = dataUrl.substring(dataUrl.indexOf(',') + 1); 335 data = atob(data); 336 pageImagesInfo.push({ 337 data, 338 width: destPixelSize.x, 339 height: destPixelSize.y 340 } 341 ); 342 } 343 return {images: pageImagesInfo, renderedLayers}; 344 } 345 346 export {renderPages};