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