nakarte

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

map-render.js (10771B)


      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) {
     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};