nakarte

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

index.js (8035B)


      1 import ko from 'knockout';
      2 import L from 'leaflet';
      3 
      4 import '~/lib/leaflet.control.commons';
      5 import '~/lib/controls-styles/controls-styles.css';
      6 import * as logging from '~/lib/logging';
      7 import {notify} from '~/lib/notifications';
      8 import {saveAs} from '~/vendored/github.com/eligrey/FileSaver';
      9 
     10 import controlHtml from './control.html';
     11 import './control.css';
     12 import {makeJnxFromLayer, minZoom} from './jnx-maker';
     13 import {RectangleSelect} from './selector';
     14 
     15 L.Control.JNX = L.Control.extend({
     16     includes: L.Mixin.Events,
     17 
     18     initialize: function (layersControl, options) {
     19         L.Control.prototype.initialize.call(this, options);
     20         this._layersControl = layersControl;
     21         this.makingJnx = ko.observable(false);
     22         this.downloadProgressRange = ko.observable(1);
     23         this.downloadProgressDone = ko.observable(0);
     24         this.layerForExport = ko.observable(null);
     25         this.areaSelectorVisible = ko.observable(false);
     26         this.zoomLevel = ko.observable(null);
     27         this.zoomChoices = ko.observable(null);
     28         this.fixZoom = ko.observable(false);
     29 
     30         this.zoomLevel.subscribe(() => this.fireChangeEvent());
     31         this.fixZoom.subscribe(() => this.fireChangeEvent());
     32     },
     33 
     34     getLayerForJnx: function () {
     35         for (const layerRec of this._layersControl._layers.slice().reverse()) {
     36             const layer = layerRec.layer;
     37             if (!this._map.hasLayer(layer) || !layer.options) {
     38                 continue;
     39             }
     40             if (layer.options.isWrapper) {
     41                 const layerName = layerRec.name;
     42                 for (const subLayer of layer.getLayers().slice().reverse()) {
     43                     if (subLayer.options?.jnx) {
     44                         return {layer: subLayer, name: layerName};
     45                     }
     46                 }
     47             } else if (layer.options.jnx) {
     48                 return {layer, name: layerRec.name};
     49             }
     50         }
     51         return null;
     52     },
     53 
     54     estimateTilesCount: function (maxZoom) {
     55         const bounds = this._areaSelector.getBounds();
     56         let maxTilesPerLevel = 0;
     57         let totalTiles = 0;
     58         for (let zoom = minZoom(maxZoom); zoom <= maxZoom; zoom++) {
     59             const topLeftTile = this._map.project(bounds.getNorthWest(), zoom).divideBy(256).floor();
     60             const bottomRightTile = this._map.project(bounds.getSouthEast(), zoom).divideBy(256).ceil();
     61             const tilesCount = Math.ceil((bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y));
     62             totalTiles += tilesCount;
     63             maxTilesPerLevel = Math.max(maxTilesPerLevel, tilesCount);
     64         }
     65         return {total: totalTiles, maxPerLevel: maxTilesPerLevel};
     66     },
     67 
     68     updateZoomChoices: function () {
     69         if (!this.layerForExport() || !this._areaSelector) {
     70             this.zoomChoices(null);
     71             this.zoomLevel(null);
     72             return;
     73         }
     74         const layer = this.layerForExport().layer;
     75         const choices = {};
     76         const maxLevel = layer.options.maxNativeZoom || layer.options.maxZoom || 18;
     77         let minLevel = Math.max(0, maxLevel - 6);
     78         if (layer.options.minZoom) {
     79             minLevel = Math.max(minLevel, layer.options.minZoom);
     80         }
     81 
     82         const equatorLength = 40075016;
     83         const lat = this._areaSelector.getBounds().getCenter().lat;
     84         let metersPerPixel = (equatorLength / 2 ** maxLevel / 256) * Math.cos((lat / 180) * Math.PI);
     85 
     86         for (let zoom = maxLevel; zoom >= minLevel; zoom -= 1) {
     87             const tilesCount = this.estimateTilesCount(zoom);
     88             const fileSizeMb = tilesCount.total * 0.02;
     89             choices[zoom] = {
     90                 zoom,
     91                 metersPerPixel,
     92                 maxLevelTiles: tilesCount.maxPerLevel,
     93                 fileSizeMb,
     94                 warning: tilesCount.maxPerLevel > 50000,
     95             };
     96             metersPerPixel *= 2;
     97         }
     98         if (!choices[this.zoomLevel()]) {
     99             this.zoomLevel(null);
    100         }
    101         this.zoomChoices(choices);
    102     },
    103 
    104     notifyProgress: function (value, maxValue) {
    105         this.downloadProgressDone(this.downloadProgressDone() + value);
    106         this.downloadProgressRange(maxValue);
    107     },
    108 
    109     errorMessage: function () {
    110         if (!this.layerForExport()) {
    111             return 'Select layer for export';
    112         }
    113         if (!this.areaSelectorVisible()) {
    114             return 'Set area for export';
    115         }
    116         if (this.zoomLevel() === null) {
    117             return 'Select zoom level';
    118         }
    119         return null;
    120     },
    121 
    122     makeJnx: function () {
    123         if (!this.zoomLevel() || !this._areaSelector || !this.layerForExport()) {
    124             return;
    125         }
    126         logging.captureBreadcrumb('start making jnx');
    127         this.makingJnx(true);
    128         this.downloadProgressDone(0);
    129 
    130         const bounds = this._areaSelector.getBounds();
    131         const layer = this.layerForExport().layer;
    132         const layerName = this.layerForExport().name;
    133         const zoom = this.zoomLevel();
    134         const sanitizedLayerName = layerName.toLowerCase().replace(/[ ()]+/u, '_');
    135         const fileName = `nakarte.me_${sanitizedLayerName}_z${zoom}.jnx`;
    136         const eventId = logging.randId();
    137         this.fire('tileExportStart', {
    138             eventId,
    139             layer,
    140             zoom,
    141             bounds,
    142         });
    143         makeJnxFromLayer(layer, layerName, zoom, bounds, this.fixZoom(), this.notifyProgress.bind(this))
    144             .then((fileData) => {
    145                 saveAs(fileData, fileName, true);
    146                 this.fire('tileExportEnd', {eventId, success: true});
    147             })
    148             .catch((e) => {
    149                 logging.captureException(e, 'Failed to create JNX');
    150                 this.fire('tileExportEnd', {
    151                     eventId,
    152                     success: false,
    153                     error: e,
    154                 });
    155                 notify(`Failed to create JNX: ${e.message}`);
    156             })
    157             .then(() => this.makingJnx(false));
    158     },
    159 
    160     onAdd: function (map) {
    161         this._map = map;
    162         const container = L.DomUtil.create('div', 'leaflet-control control-form leaflet-control-jnx');
    163         this._container = container;
    164         container.innerHTML = controlHtml;
    165         map.on('baselayerchange overlayadd overlayremove', this.updateLayer, this);
    166         this.updateLayer();
    167         ko.applyBindings(this, container);
    168         this._stopContainerEvents();
    169         return container;
    170     },
    171 
    172     removeAreaSelector: function () {
    173         if (this._areaSelector) {
    174             this._map.removeLayer(this._areaSelector);
    175             this._areaSelector = null;
    176             this.areaSelectorVisible(false);
    177             this.onSelectorChange();
    178         }
    179     },
    180 
    181     setAreaSelector: function (bounds = this._map.getBounds().pad(-0.25)) {
    182         if (this._areaSelector) {
    183             this._map.removeLayer(this._areaSelector);
    184         }
    185         this._areaSelector = new RectangleSelect(bounds)
    186             .addTo(this._map)
    187             .on('change', this.onSelectorChange, this)
    188             .on('click', this.setExpanded, this);
    189         this.areaSelectorVisible(true);
    190         this.onSelectorChange();
    191     },
    192 
    193     fireChangeEvent: function () {
    194         this.fire('settingschange');
    195     },
    196 
    197     onSelectorChange: function () {
    198         this.updateZoomChoices();
    199         this.fireChangeEvent();
    200     },
    201 
    202     moveMapToAreaSelector: function () {
    203         if (this._areaSelector) {
    204             this._map.fitBounds(this._areaSelector.getBounds().pad(0.25));
    205         }
    206     },
    207 
    208     onMinimizedDialogButtonClick: function () {
    209         if (!this._areaSelector) {
    210             this.setAreaSelector();
    211         }
    212         this.setExpanded();
    213     },
    214 
    215     setExpanded: function () {
    216         L.DomUtil.removeClass(this._container, 'minimized');
    217     },
    218 
    219     setMinimized: function () {
    220         L.DomUtil.addClass(this._container, 'minimized');
    221     },
    222 
    223     updateLayer: function () {
    224         this.layerForExport(this.getLayerForJnx());
    225         this.updateZoomChoices();
    226     },
    227 });