nakarte

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

index.js (7397B)


      1 import L from 'leaflet';
      2 import ko from 'knockout';
      3 import './style.css';
      4 import '~/lib/leaflet.control.commons';
      5 import {RectangleSelect} from './selector';
      6 import Contextmenu from '~/lib/contextmenu';
      7 import {makeJnxFromLayer, minZoom} from './jnx-maker';
      8 import {saveAs} from '~/vendored/github.com/eligrey/FileSaver';
      9 import {notify} from '~/lib/notifications';
     10 import * as logging from '~/lib/logging';
     11 
     12 L.Control.JNX = L.Control.extend({
     13         includes: L.Mixin.Events,
     14 
     15         initialize: function(layersControl, options) {
     16             L.Control.prototype.initialize.call(this, options);
     17             this._layersControl = layersControl;
     18             this.makingJnx = ko.observable(false);
     19             this.downloadProgressRange = ko.observable(1);
     20             this.downloadProgressDone = ko.observable(0);
     21             this.contextMenu = new Contextmenu(() => this.makeMenuItems());
     22         },
     23 
     24         getLayerForJnx: function() {
     25             for (let layerRec of this._layersControl._layers.slice().reverse()) {
     26                 let layer = layerRec.layer;
     27                 if (!this._map.hasLayer(layer) || !layer.options) {
     28                     continue;
     29                 }
     30                 if (layer.options.isWrapper) {
     31                     const layerName = layerRec.name;
     32                     for (const subLayer of layer.getLayers().slice().reverse()) {
     33                         if (subLayer.options?.jnx) {
     34                             return {layer: subLayer, layerName};
     35                         }
     36                     }
     37                 } else if (layer.options.jnx) {
     38                     return {layer, layerName: layerRec.name};
     39                 }
     40             }
     41             return {};
     42         },
     43 
     44         estimateTilesCount: function(maxZoom) {
     45             let tilesCount = 0;
     46             const bounds = this._selector.getBounds();
     47             for (let zoom = minZoom(maxZoom); zoom <= maxZoom; zoom++) {
     48                 const topLeftTile = this._map.project(bounds.getNorthWest(), zoom).divideBy(256).floor();
     49                 const bottomRightTile = this._map.project(bounds.getSouthEast(), zoom).divideBy(256).ceil();
     50                 tilesCount += Math.ceil((bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y));
     51             }
     52             return tilesCount;
     53         },
     54 
     55         makeMenuItems: function() {
     56             const {layer, layerName} = this.getLayerForJnx();
     57             if (!layer) {
     58                 return [{text: 'No supported layers'}];
     59             }
     60             const maxLevel = layer.options.maxNativeZoom || layer.options.maxZoom || 18;
     61             let minLevel = Math.max(0, maxLevel - 6);
     62             if (layer.options.minZoom) {
     63                 minLevel = Math.max(minLevel, layer.options.minZoom);
     64             }
     65 
     66             const equatorLength = 40075016;
     67             const lat = this._selector.getBounds().getCenter().lat;
     68             let metersPerPixel = equatorLength / 2 ** maxLevel / 256 * Math.cos(lat / 180 * Math.PI);
     69 
     70             const items = [{text: layerName, header: true}];
     71             for (let zoom = maxLevel; zoom >= minLevel; zoom -= 1) {
     72                 let tilesCount = this.estimateTilesCount(zoom);
     73                 let fileSizeMb = tilesCount * 0.02;
     74                 let itemClass = tilesCount > 50000 ? 'jnx-menu-warning' : '';
     75                 let resolutionString = metersPerPixel.toFixed(2);
     76                 let sizeString = fileSizeMb.toFixed(fileSizeMb > 1 ? 0 : 1);
     77                 let item = {
     78                     text: `<span class="${itemClass}">Zoom ${zoom} (${resolutionString} m/pixel) &mdash; ${tilesCount} tiles (~${sizeString} Mb)</span>`, // eslint-disable-line max-len
     79                     callback: () => this.makeJnx(layer, layerName, zoom),
     80                     disabled: this.makingJnx()
     81                 };
     82                 items.push(item);
     83                 metersPerPixel *= 2;
     84             }
     85             return items;
     86         },
     87 
     88         notifyProgress: function(value, maxValue) {
     89             this.downloadProgressDone(this.downloadProgressDone() + value);
     90             this.downloadProgressRange(maxValue);
     91         },
     92 
     93         makeJnx: function(layer, layerName, zoom) {
     94             logging.captureBreadcrumb('start making jnx');
     95             this.makingJnx(true);
     96             this.downloadProgressDone(0);
     97 
     98             const bounds = this._selector.getBounds();
     99             const sanitizedLayerName = layerName.toLowerCase().replace(/[ ()]+/u, '_');
    100             const fileName = `nakarte.me_${sanitizedLayerName}_z${zoom}.jnx`;
    101             const eventId = logging.randId();
    102             this.fire('tileExportStart', {
    103                 eventId,
    104                 layer,
    105                 zoom,
    106                 bounds,
    107             });
    108             makeJnxFromLayer(layer, layerName, zoom, bounds, this.notifyProgress.bind(this))
    109                 .then((fileData) => {
    110                     saveAs(fileData, fileName, true);
    111                     this.fire('tileExportEnd', {eventId, success: true});
    112                 })
    113                 .catch((e) => {
    114                     logging.captureException(e, 'Failed to create JNX');
    115                     this.fire('tileExportEnd', {
    116                         eventId,
    117                         success: false,
    118                         error: e
    119                     });
    120                     notify(`Failed to create JNX: ${e.message}`);
    121                 })
    122                 .then(() => this.makingJnx(false));
    123         },
    124 
    125         onAdd: function(map) {
    126             this._map = map;
    127             const container = this._container = L.DomUtil.create('div', 'leaflet-control leaflet-control-jnx');
    128             container.innerHTML = `
    129                 <a class="button" data-bind="visible: !makingJnx(), click: onButtonClicked"
    130                  title="Make JNX for Garmin receivers">JNX</a>
    131                 <div data-bind="
    132                     component:{
    133                         name: 'progress-indicator',
    134                         params: {progressRange: downloadProgressRange, progressDone: downloadProgressDone}
    135                     },
    136                     visible: makingJnx()"></div>`;
    137             ko.applyBindings(this, container);
    138             this._stopContainerEvents();
    139             return container;
    140         },
    141 
    142         removeSelector: function() {
    143             if (this._selector) {
    144                 this._map.removeLayer(this._selector);
    145                 this._selector = null;
    146                 this.fire('selectionchange');
    147             }
    148         },
    149 
    150         addSelector: function(bounds) {
    151             if (!bounds) {
    152                 bounds = this._map.getBounds().pad(-0.25);
    153             }
    154             this._selector = new RectangleSelect(bounds)
    155                 .addTo(this._map)
    156                 .on('change', () => this.fire('selectionchange'))
    157                 .on('click contextmenu', (e) => {
    158                     L.DomEvent.stop(e);
    159                     this.contextMenu.show(e);
    160                 });
    161             this.fire('selectionchange');
    162         },
    163 
    164         onButtonClicked: function() {
    165             if (this._selector) {
    166                 if (this._selector.getBounds().intersects(this._map.getBounds().pad(-0.05))) {
    167                     this.removeSelector();
    168                 } else {
    169                     this.removeSelector();
    170                     this.addSelector();
    171                 }
    172             } else {
    173                 this.addSelector();
    174             }
    175         },
    176     }
    177 );
    178