nakarte

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

control.js (21544B)


      1 import L from 'leaflet';
      2 import ko from 'knockout';
      3 import '~/lib/knockout.component.progress/progress';
      4 import '~/lib/controls-styles/controls-styles.css';
      5 import './control.css';
      6 import PageFeature from './pageFeature';
      7 import Contextmenu from '~/lib/contextmenu';
      8 import {renderPages} from './map-render';
      9 import formHtml from './form.html';
     10 import {notify} from '~/lib/notifications';
     11 import {makePdf} from './pdf';
     12 import {saveAs} from '~/vendored/github.com/eligrey/FileSaver';
     13 import {blobFromString} from '~/lib/binary-strings';
     14 import '~/lib/leaflet.hashState/leaflet.hashState';
     15 import '~/lib/leaflet.control.commons';
     16 import * as logging from '~/lib/logging';
     17 import {MagneticMeridians} from './decoration.magnetic-meridians';
     18 import {OverlayScale} from './decoration.scale';
     19 import {Grid} from './decoration.grid';
     20 
     21 ko.extenders.checkNumberRange = function(target, range) {
     22     return ko.pureComputed({
     23             read: target,  // always return the original observables value
     24             write: function(newValue) {
     25                 newValue = parseFloat(newValue);
     26                 if (newValue >= range[0] && newValue <= range[1]) {
     27                     target(newValue);
     28                 } else {
     29                     target.notifySubscribers(target());
     30                 }
     31             }
     32         }
     33     ).extend({notify: 'always'});
     34 };
     35 
     36 function savePagesPdf(imagesInfo, resolution, fileName) {
     37     let pdf = makePdf(imagesInfo, resolution);
     38     pdf = blobFromString(pdf);
     39     saveAs(pdf, fileName, true);
     40 }
     41 
     42 function savePageJpg(page, fileName) {
     43     saveAs(blobFromString(page.data), fileName, true);
     44 }
     45 
     46 L.Control.PrintPages = L.Control.extend({
     47         options: {
     48             position: 'bottomleft',
     49             defaultMargin: 7,
     50         },
     51 
     52         includes: [L.Mixin.Events, L.Mixin.HashState],
     53 
     54         stateChangeEvents: ['change'],
     55 
     56         pageSizes: [
     57             {name: 'A1', width: 594, height: 841},
     58             {name: 'A2', width: 420, height: 594},
     59             {name: 'A3', width: 297, height: 420},
     60             {name: 'A4', width: 210, height: 297},
     61             {name: 'A5', width: 148, height: 210}
     62         ],
     63 
     64         zoomLevels: ['auto', 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
     65 
     66         initialize: function(options) {
     67             L.Control.prototype.initialize.call(this, options);
     68             this.pages = [];
     69             this.scale = ko.observable(500).extend({checkNumberRange: [1, 1000000]});
     70             this.resolution = ko.observable(300).extend({checkNumberRange: [10, 9999]});
     71             this.zoomLevel = ko.observable('auto');
     72             this.pageWidth = ko.observable(210).extend({checkNumberRange: [10, 9999]});
     73             this.pageHeight = ko.observable(297).extend({checkNumberRange: [10, 9999]});
     74             this.settingsExpanded = ko.observable(false);
     75             this.makingPdf = ko.observable(false);
     76             this.downloadProgressRange = ko.observable();
     77             this.downloadProgressDone = ko.observable();
     78             this.marginLeft = ko.observable(this.options.defaultMargin).extend({checkNumberRange: [0, 99]});
     79             this.marginRight = ko.observable(this.options.defaultMargin).extend({checkNumberRange: [0, 99]});
     80             this.marginTop = ko.observable(this.options.defaultMargin).extend({checkNumberRange: [0, 99]});
     81             this.marginBottom = ko.observable(this.options.defaultMargin).extend({checkNumberRange: [0, 99]});
     82             this.autoZoomLevels = ko.observable({});
     83             this.printSize = ko.pureComputed(this._printSize, this);
     84             this.printSize.subscribe(this.onPageSizeChanged, this);
     85             this.scale.subscribe(this.onPageSizeChanged, this);
     86             this.resolution.subscribe(this.onPageSizeChanged, this);
     87             this.pageSizeDescription = ko.pureComputed(this._displayPageSize, this);
     88             this.pagesNum = ko.observable(0);
     89             this.pagesNumLabel = ko.pureComputed(this._pagesNumLabel, this);
     90             this.gridOn = ko.observable(false);
     91             this.magneticMeridiansOn = ko.observable(false);
     92 
     93             // hash state notifications
     94             this.scale.subscribe(this.notifyChange, this);
     95             this.printSize.subscribe(this.notifyChange, this);
     96             this.resolution.subscribe(this.notifyChange, this);
     97             this.zoomLevel.subscribe(this.notifyChange, this);
     98             this.gridOn.subscribe(this.notifyChange, this);
     99             this.magneticMeridiansOn.subscribe(this.notifyChange, this);
    100         },
    101 
    102         onAdd: function(map) {
    103             this._map = map;
    104             const container = this._container =
    105                 L.DomUtil.create('div', 'leaflet-control control-form control-print-pages');
    106             this._stopContainerEvents();
    107 
    108             map.on('move', this.updateFormZooms, this);
    109             container.innerHTML = formHtml;
    110             ko.applyBindings(this, container);
    111             this.updateFormZooms();
    112             return container;
    113         },
    114 
    115         setExpanded: function() {
    116             L.DomUtil.removeClass(this._container, 'minimized');
    117         },
    118 
    119         setMinimized: function() {
    120             L.DomUtil.addClass(this._container, 'minimized');
    121         },
    122 
    123         addPage: function(isLandscape, center) {
    124             let [pageWidth, pageHeight] = this.printSize();
    125             if (isLandscape) {
    126                 [pageWidth, pageHeight] = [pageHeight, pageWidth];
    127             }
    128             if (!center) {
    129                 center = this._map.getCenter();
    130             }
    131             const page = new PageFeature(center, [pageWidth, pageHeight],
    132                 this.scale(), (this.pages.length + 1).toString()
    133             );
    134             page._rotated = isLandscape;
    135             page.addTo(this._map);
    136             this.pages.push(page);
    137             this.pagesNum(this.pages.length);
    138             let cm = new Contextmenu(this.makePageContexmenuItems.bind(this, page));
    139             page.on('contextmenu', cm.show, cm);
    140             page.on('click', this.rotatePage.bind(this, page));
    141             page.on('move', this.updateFormZooms, this);
    142             page.on('moveend', this.notifyChange, this);
    143             this.updateFormZooms();
    144             this.notifyChange();
    145             return page;
    146         },
    147 
    148         addLandscapePage: function() {
    149             this.addPage(true);
    150         },
    151 
    152         addPortraitPage: function() {
    153             this.addPage(false);
    154         },
    155 
    156         removePage: function(page) {
    157             let i = this.pages.indexOf(page);
    158             this.pages.splice(i, 1);
    159             this.pagesNum(this.pages.length);
    160             this._map.removeLayer(page);
    161             for (; i < this.pages.length; i++) {
    162                 this.pages[i].setLabel((i + 1).toString());
    163             }
    164             this.notifyChange();
    165             this.updateFormZooms();
    166         },
    167 
    168         removePages: function() {
    169             this.pages.forEach((page) => page.removeFrom(this._map));
    170             this.pages = [];
    171             this.pagesNum(this.pages.length);
    172             this.notifyChange();
    173             this.updateFormZooms();
    174         },
    175 
    176         onSavePdfClicked: function() {
    177             if (!this.pages.length) {
    178                 notify('Add some pages to print');
    179                 return;
    180             }
    181             this.savePdf();
    182         },
    183 
    184         zoomForPrint: function() {
    185             let zoom = this.zoomLevel();
    186             if (zoom === 'auto') {
    187                 zoom = this.suggestZooms();
    188             } else {
    189                 zoom = {mapZoom: zoom, satZoom: zoom};
    190             }
    191             return zoom;
    192         },
    193 
    194         incrementProgress: function(inc, range) {
    195             this.downloadProgressRange(range);
    196             this.downloadProgressDone((this.downloadProgressDone() || 0) + inc);
    197         },
    198 
    199         savePdf: function() {
    200             logging.captureBreadcrumb('start save pdf');
    201             if (!this._map) {
    202                 return;
    203             }
    204             this.downloadProgressRange(1000);
    205             this.downloadProgressDone(undefined);
    206             this.makingPdf(true);
    207             const pages = this.pages.map((page) => ({
    208                         latLngBounds: page.getLatLngBounds(),
    209                         printSize: page.getPrintSize(),
    210                         label: page.getLabel()
    211             }));
    212             const resolution = this.resolution();
    213             const decorationLayers = [];
    214             if (this.gridOn()) {
    215                 decorationLayers.push(new Grid());
    216             }
    217             if (this.magneticMeridiansOn()) {
    218                 decorationLayers.push(new MagneticMeridians());
    219             }
    220             decorationLayers.push(new OverlayScale());
    221             const scale = this.scale();
    222             const width = this.pageWidth();
    223             const height = this.pageHeight();
    224             const eventId = logging.randId();
    225             const zooms = this.zoomForPrint();
    226             this.fire('mapRenderStart', {
    227                 action: 'pdf',
    228                 eventId,
    229                 scale,
    230                 resolution,
    231                 pages,
    232                 zooms
    233             });
    234             renderPages({
    235                     map: this._map,
    236                     pages,
    237                     zooms,
    238                     resolution,
    239                     scale,
    240                     decorationLayers,
    241                     progressCallback: this.incrementProgress.bind(this)
    242                 }
    243             ).then(({images, renderedLayers}) => {
    244                     if (images) {
    245                         const fileName = this.getFileName({
    246                             renderedLayers,
    247                             scale,
    248                             width,
    249                             height,
    250                             extension: 'pdf'
    251                         });
    252                         savePagesPdf(images, resolution, fileName);
    253                         this.fire('mapRenderEnd', {eventId, success: true});
    254                     }
    255                 }
    256             ).catch((e) => {
    257                     logging.captureException(e, 'raster creation failed');
    258                     this.fire('mapRenderEnd', {eventId, success: false, error: e});
    259                     notify(`Failed to create PDF: ${e.message}`);
    260                 }
    261             ).then(() => this.makingPdf(false));
    262         },
    263 
    264         savePageJpg: function(page) {
    265             logging.captureBreadcrumb('start save page jpg', {pageNumber: page.getLabel()});
    266             const pages = [{
    267                 latLngBounds: page.getLatLngBounds(),
    268                 printSize: page.getPrintSize(),
    269                 label: page.getLabel()
    270             }];
    271             const decorationLayers = [];
    272             if (this.gridOn()) {
    273                 decorationLayers.push(new Grid());
    274             }
    275             if (this.magneticMeridiansOn()) {
    276                 decorationLayers.push(new MagneticMeridians());
    277             }
    278             decorationLayers.push(new OverlayScale());
    279             this.downloadProgressRange(1000);
    280             this.downloadProgressDone(undefined);
    281             this.makingPdf(true);
    282             const resolution = this.resolution();
    283             const scale = this.scale();
    284             const width = this.pageWidth();
    285             const height = this.pageHeight();
    286             const eventId = logging.randId();
    287             const zooms = this.zoomForPrint();
    288             this.fire('mapRenderStart', {
    289                 action: 'jpg',
    290                 eventId,
    291                 scale,
    292                 resolution,
    293                 pages,
    294                 zooms
    295             });
    296             renderPages({
    297                     map: this._map,
    298                     pages,
    299                     zooms,
    300                     resolution,
    301                     scale,
    302                     decorationLayers,
    303                     progressCallback: this.incrementProgress.bind(this)
    304                 }
    305             )
    306                 .then(({images, renderedLayers}) => {
    307                     const fileName = this.getFileName({
    308                         renderedLayers,
    309                         scale,
    310                         width,
    311                         height,
    312                         extension: 'jpg'
    313                     });
    314                     savePageJpg(images[0], fileName);
    315                     this.fire('mapRenderEnd', {eventId, success: true});
    316                 })
    317                 .catch((e) => {
    318                         logging.captureException(e, 'raster creation failed');
    319                         this.fire('mapRenderEnd', {eventId, success: false, error: e});
    320                         notify(`Failed to create JPEG from page: ${e.message}`);
    321                     }
    322                 ).then(() => this.makingPdf(false));
    323         },
    324 
    325         onPageSizeChanged: function() {
    326             let [pageWidth, pageHeight] = this.printSize();
    327             this.pages.forEach((page) => {
    328                     let [w, h] = [pageWidth, pageHeight];
    329                     if (page._rotated) {
    330                         [w, h] = [h, w];
    331                     }
    332                     page.setSize([w, h], this.scale());
    333                 }
    334             );
    335             this.updateFormZooms();
    336         },
    337 
    338         onPagesNumLabelClick: function() {
    339             if (this.pages.length > 0) {
    340                 const bounds = L.latLngBounds([]);
    341                 for (let page of this.pages) {
    342                     bounds.extend(page.latLngBounds);
    343                 }
    344                 this._map.fitBounds(bounds.pad(0.2));
    345             }
    346         },
    347 
    348         makePageContexmenuItems: function(page) {
    349             const items = [
    350                 {text: 'Rotate', callback: this.rotatePage.bind(this, page)},
    351                 '-',
    352                 {text: 'Delete', callback: this.removePage.bind(this, page)},
    353                 '-',
    354                 {text: 'Save image', callback: this.savePageJpg.bind(this, page), disabled: this.makingPdf()}
    355             ];
    356             if (this.pages.length > 1) {
    357                 items.push({text: 'Change order', separator: true});
    358                 this.pages.forEach((p, i) => {
    359                         if (p !== page) {
    360                             items.push({
    361                                     text: (i + 1).toString(),
    362                                     callback: this.renumberPage.bind(this, page, i)
    363                                 }
    364                             );
    365                         }
    366                     }
    367                 );
    368             }
    369             return items;
    370         },
    371 
    372         rotatePage: function(page) {
    373             page._rotated = !page._rotated;
    374             page.rotate();
    375             this.notifyChange();
    376         },
    377 
    378         renumberPage: function(page, newIndex) {
    379             const oldIndex = this.pages.indexOf(page);
    380             this.pages.splice(oldIndex, 1);
    381             this.pages.splice(newIndex, 0, page);
    382             for (let i = Math.min(oldIndex, newIndex); i < this.pages.length; i++) {
    383                 this.pages[i].setLabel((i + 1).toString());
    384             }
    385             this.notifyChange();
    386         },
    387 
    388         _printSize: function() {
    389             return [this.pageWidth() - this.marginLeft() - this.marginRight(),
    390                 this.pageHeight() - this.marginTop() - this.marginBottom()];
    391         },
    392 
    393         suggestZooms: function() {
    394             const scale = this.scale(),
    395                 resolution = this.resolution();
    396             let referenceLat;
    397             if (this.pages.length > 0) {
    398                 let absLats = this.pages.map((page) => Math.abs(page.getLatLngBounds().getCenter().lat));
    399                 referenceLat = Math.min(...absLats);
    400             } else {
    401                 if (!this._map) {
    402                     return [null, null];
    403                 }
    404                 referenceLat = this._map.getCenter().lat;
    405             }
    406             let targetMetersPerPixel = scale / (resolution / 2.54);
    407             let mapUnitsPerPixel = targetMetersPerPixel / Math.cos(referenceLat * Math.PI / 180);
    408             let satZoom = Math.ceil(Math.log(40075016.4 / 256 / mapUnitsPerPixel) / Math.LN2);
    409 
    410             targetMetersPerPixel = scale / (90 / 2.54) / 1.5;
    411             mapUnitsPerPixel = targetMetersPerPixel / Math.cos(referenceLat * Math.PI / 180);
    412             let mapZoom = Math.round(Math.log(40075016.4 / 256 / mapUnitsPerPixel) / Math.LN2);
    413             mapZoom = Math.min(mapZoom, 18);
    414             satZoom = Math.min(satZoom, 18);
    415             mapZoom = Math.max(mapZoom, 0);
    416             satZoom = Math.max(satZoom, 0);
    417             return {mapZoom, satZoom};
    418         },
    419 
    420         updateFormZooms: function() {
    421             this.autoZoomLevels(this.suggestZooms());
    422         },
    423 
    424         _displayPageSize: function() {
    425             const width = this.pageWidth(),
    426                 height = this.pageHeight();
    427             for (let size of this.pageSizes) {
    428                 if (size.width === width && size.height === height) {
    429                     return size.name;
    430                 }
    431             }
    432             return `${width} x ${height} mm`;
    433         },
    434 
    435         notifyChange: function() {
    436             this.fire('change');
    437         },
    438 
    439         hasPages: function() {
    440             return this.pages.length > 0;
    441         },
    442 
    443         _pagesNumLabel: function() {
    444             const n = this.pagesNum();
    445             let label = '';
    446             if (n) {
    447                 label += n;
    448             } else {
    449                 label = 'No';
    450             }
    451             label += ' page';
    452             if (n === 0 || n > 1) {
    453                 label += 's';
    454             }
    455             return label;
    456         },
    457 
    458         serializeState: function() {
    459             const pages = this.pages;
    460             let state = null;
    461             if (pages.length) {
    462                 state = [];
    463                 state.push(this.scale().toString());
    464                 state.push(this.resolution().toString());
    465                 state.push(this.zoomLevel().toString());
    466                 state.push(this.pageWidth().toString());
    467                 state.push(this.pageHeight().toString());
    468                 state.push(this.marginLeft().toString());
    469                 state.push(this.marginRight().toString());
    470                 state.push(this.marginTop().toString());
    471                 state.push(this.marginBottom().toString());
    472                 for (let page of pages) {
    473                     let latLng = page.getLatLng().wrap();
    474                     state.push(latLng.lat.toFixed(5));
    475                     state.push(latLng.lng.toFixed(5));
    476                     state.push(page._rotated ? '1' : '0');
    477                 }
    478                 let flags =
    479                     (this.magneticMeridiansOn() ? 1 : 0) |
    480                     (this.gridOn() ? 2 : 0);
    481                 state.push(flags.toString());
    482             }
    483             return state;
    484         },
    485 
    486         unserializeState: function(state) {
    487             if (!state || !state.length) {
    488                 return false;
    489             }
    490             this.removePages();
    491             state = [...state];
    492             this.scale(state.shift());
    493             this.resolution(state.shift());
    494             this.zoomLevel(state.shift());
    495             this.pageWidth(state.shift());
    496             this.pageHeight(state.shift());
    497             this.marginLeft(state.shift());
    498             this.marginRight(state.shift());
    499             this.marginTop(state.shift());
    500             this.marginBottom(state.shift());
    501             let lat, lng, rotated;
    502             while (state.length >= 3) {
    503                 lat = parseFloat(state.shift());
    504                 lng = parseFloat(state.shift());
    505                 rotated = parseInt(state.shift(), 10);
    506                 if (isNaN(lat) || isNaN(lng) || lat < -85 || lat > 85 || lng < -180 || lng > 180) {
    507                     break;
    508                 }
    509                 this.addPage(Boolean(rotated), L.latLng(lat, lng));
    510             }
    511             if (state.length) {
    512                 const flags = parseInt(state.shift(), 10);
    513                 if (flags >= 0 && flags <= 3) {
    514                     this.magneticMeridiansOn(Boolean(flags & 1));
    515                     this.gridOn(Boolean(flags & 2));
    516                 }
    517             }
    518             return true;
    519         },
    520 
    521         getFileName: function({renderedLayers, scale, width, height, extension}) {
    522             let fileName = '';
    523 
    524             let opaqueLayer;
    525             const transparentOverlayLayers = [];
    526 
    527             renderedLayers.forEach((layer) => {
    528                 const {
    529                     options: {
    530                         isOverlay,
    531                         isOverlayTransparent,
    532                         shortName
    533                     }
    534                 } = layer;
    535 
    536                 if (!shortName) {
    537                     return;
    538                 }
    539 
    540                 if (isOverlay) {
    541                     if (isOverlayTransparent) {
    542                         transparentOverlayLayers.push(layer);
    543                     } else {
    544                         opaqueLayer = layer;
    545                     }
    546                 } else if (!opaqueLayer) {
    547                     opaqueLayer = layer;
    548                 }
    549             });
    550 
    551             function appendLayerShortName(layer) {
    552                 fileName += `${layer.options.shortName}_`;
    553             }
    554             if (opaqueLayer) {
    555                 appendLayerShortName(opaqueLayer);
    556             }
    557             transparentOverlayLayers.forEach(appendLayerShortName);
    558 
    559             fileName += `${scale}m`;
    560 
    561             const currentPageSize = this.pageSizes.find(
    562                 (pageSize) => width === pageSize.width && height === pageSize.height
    563             );
    564 
    565             if (currentPageSize) {
    566                 fileName += `_${currentPageSize.name}`;
    567             }
    568 
    569             return `${fileName}.${extension}`;
    570         }
    571     }
    572 );