commit 49e54d397869d441fa628d99db906cea09862dc0
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Sat, 5 Nov 2016 16:50:17 +0300
initial commit
Diffstat:
30 files changed, 1469 insertions(+), 0 deletions(-)
diff --git a/src/App.css b/src/App.css
@@ -0,0 +1,3 @@
+#map {
+ height: 100%;
+}
+\ No newline at end of file
diff --git a/src/App.js b/src/App.js
@@ -0,0 +1,44 @@
+import './App.css';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+import layers from './layers';
+import './lib/control.printPages/control'
+import './lib/control.caption/caption'
+import config from './config'
+import './lib/control.coordinates/coordinates';
+import './lib/control.layers.hotkeys/control.Layers-hotkeys';
+
+function setUp() {
+ const map = L.map('map', {
+ zoomControl: false,
+ fadeAnimation: false,
+ center:[55.75185, 37.61856],
+ zoom: 10
+ });
+
+ new L.Control.Caption(`<a href=mailto:${config.email}">nakarte@nakarte.tk</a>`, {
+ position: 'topleft'
+ }).addTo(map);
+ L.control.zoom().addTo(map);
+
+ {
+ let baseLayers = layers.getBaseMaps();
+ const layersControl = L.control.layers(baseLayers, layers.getOverlays(), {collapsed: false}).addTo(map);
+ map.addLayer(baseLayers['OpenStreetMap']);
+ }
+
+ new L.Control.PrintPages().addTo(map);
+ new L.Control.Coordinates().addTo(map);
+
+ // const hashState = L.HashState();
+ // hashState.bind(map);
+ // hashState.bind(layersControl);
+
+ // let p = L.polyline([[0, 0], [20, 20]]);
+ // p.addTo(map);
+ // map.on('contextmenu', () => {
+ // map.flyTo([10, 20], 13, {duration: 1});
+ // });
+}
+
+export default {setUp};
diff --git a/src/App.test.js b/src/App.test.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+
+it('renders without crashing', () => {
+ const div = document.createElement('div');
+ ReactDOM.render(<App />, div);
+});
diff --git a/src/config.js b/src/config.js
@@ -0,0 +1,3 @@
+export default {
+ email: 'nakarte@nakarte.tk'
+}
diff --git a/src/index.css b/src/index.css
@@ -0,0 +1,9 @@
+body {
+ margin: 0;
+ padding: 0;
+ font-family: sans-serif;
+}
+
+body, html{
+ height: 100%;
+}
diff --git a/src/index.js b/src/index.js
@@ -0,0 +1,5 @@
+import './index.css';
+import App from './App'
+
+App.setUp();
+
diff --git a/src/layers.js b/src/layers.js
@@ -0,0 +1,61 @@
+import L from "leaflet";
+import './lib/layer.yandex/yandex';
+
+function getBaseMaps() {
+ // var bingKey = 'AhZy06XFi8uAADPQvWNyVseFx4NHYAOH-7OTMKDPctGtYo86kMfx2T0zUrF5AAaM';
+ return {
+ 'OpenStreetMap': L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ {code: 'O', scaleDependent: true, print: true, jnx: true}
+ ),
+ 'ESRI Sat': L.tileLayer(
+ 'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ {code: 'E', maxNativeZoom: 17, print: true, jnx: true}
+ ),
+ 'Yandex': new L.Layer.Yandex('map', {scaleDependent: true, code: 'Y', print: true, jnx: true}),
+ 'Yandex Sat': new L.Layer.Yandex('sat', {scaleDependent: false, code: 'S', print: true, jnx: true}),
+ // 'Google': new L.Google('ROADMAP', {code: 'G', scaleDependent: true, print: true, jnx: true}),
+ // 'Google Sat': new L.Google('SATELLITE', {code: 'L', print: true, jnx: true}),
+ // 'Google Sat Hybrid': new L.Google('HYBRID', {code: 'H', scaleDependent: true, print: true, jnx: false}),
+ // 'Bing Sat': L.bingLayer(bingKey, {code: 'I', print: true, jnx: true}),
+ 'marshruty.ru': L.tileLayer('http://maps.marshruty.ru/ml.ashx?x={x}&y={y}&z={z}&i=1&al=1',
+ {code: 'M', maxNativeZoom: 18, noCors: true, scaleDependent: true, print: true, jnx: true}
+ ),
+ 'Topomapper 1km': L.tileLayer(
+ 'http://144.76.234.107//cgi-bin/ta/tilecache.py/1.0.0/topomapper_v2/{z}/{x}/{y}.jpg',
+ {code: 'T', maxNativeZoom: 13, noCors: true, print: true, jnx: true}
+ )
+ };
+}
+
+function getOverlays() {
+ return {
+ "Topo 10km": new L.TileLayer("http://{s}.tiles.nakarte.tk/topo001m/{z}/{x}/{y}",
+ {code: 'D', tms: true, maxNativeZoom: 9, print: true, jnx: true}),
+ "GGC 2 km": new L.TileLayer("http://{s}.tiles.nakarte.tk/ggc2000/{z}/{x}/{y}",
+ {code: 'N', tms: true, maxNativeZoom: 15, print: true, jnx: true}),
+ "ArbaletMO": new L.TileLayer("http://{s}.tiles.nakarte.tk/ArbaletMO/{z}/{x}/{y}",
+ {code: 'A', tms: true, maxNativeZoom: 13, print: true, jnx: true}),
+ "Slazav mountains": new L.TileLayer("http://{s}.tiles.nakarte.tk/map_hr/{z}/{x}/{y}",
+ {code: 'Q', tms: true, maxNativeZoom: 13, print: true, jnx: true}),
+ "GGC 1km": new L.TileLayer("http://{s}.tiles.nakarte.tk/ggc1000/{z}/{x}/{y}",
+ {code: 'J', tms: true, maxNativeZoom: 13, print: true, jnx: true}),
+ "Topo 1km": new L.TileLayer("http://{s}.tiles.nakarte.tk/topo1000/{z}/{x}/{y}",
+ {code: 'C', tms: true, maxNativeZoom: 13, print: true, jnx: true}),
+ "GGC 500m": new L.TileLayer("http://{s}.tiles.nakarte.tk/ggc500/{z}/{x}/{y}",
+ {code: 'F', tms: true, maxNativeZoom: 14, print: true, jnx: true}),
+ "Topo 500m": new L.TileLayer("http://{s}.tiles.nakarte.tk/topo500/{z}/{x}/{y}",
+ {code: 'B', tms: true, maxNativeZoom: 14, print: true, jnx: true}),
+ "GGC 250m": new L.TileLayer("http://{s}.tiles.nakarte.tk/ggc250/{z}/{x}/{y}",
+ {code: 'K', tms: true, maxNativeZoom: 15, print: true, jnx: true}),
+ "Slazav map": new L.TileLayer("http://{s}.tiles.nakarte.tk/map_podm/{z}/{x}/{y}",
+ {code: 'Z', tms: true, maxNativeZoom: 14, print: true, jnx: true}),
+ "O-sport": new L.TileLayer("http://{s}.tiles.nakarte.tk/osport/{z}/{x}/{y}",
+ {code: 'R', tms: true, maxNativeZoom: 17, print: true, jnx: true})
+ // "Soviet military grid": new L.SovietTopoGrid({code: 'Ng'}),
+ // "Wikimapia": new L.Wikimapia({code: 'W', zIndexOffset: 10000}),
+ // "Google Street View": new L.GoogleStreetView('street-view', {print: true, code: 'Gs', zIndexOffset: 10000}),
+ // "Mountain passes (Westra)": new L.WestraPasses('/westraPasses/', {code: 'Wp', print: true, zIndexOffset: 10000})
+ };
+}
+
+export default {getBaseMaps, getOverlays};
+\ No newline at end of file
diff --git a/src/lib/clipboardCopy/clipboardCopy.js b/src/lib/clipboardCopy/clipboardCopy.js
@@ -0,0 +1,41 @@
+import './style.css';
+
+function showNotification(message, mouseEvent) {
+ var el = document.createElement('div');
+ el.innerHTML = message;
+ el.className = 'copy-clipboard-notification';
+ document.body.appendChild(el);
+ var w = el.offsetWidth,
+ h = el.offsetHeight,
+ x = mouseEvent.clientX - w - 8,
+ y = mouseEvent.clientY - h / 2;
+ if (x < 0) {
+ x = 0
+ }
+ if (y < 0) {
+ y = 0
+ }
+ el.style.top = y + 'px';
+ el.style.left = x + 'px';
+ setTimeout(function() {
+ document.body.removeChild(el);
+ }, 1000);
+
+}
+
+function copyToClipboard(s, mouseEvent) {
+ try {
+ var ta = document.createElement('textarea');
+ ta.value = s;
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand('copy');
+ showNotification('Copied', mouseEvent);
+ } catch (e) {
+ prompt("Copy to clipboard: Ctrl+C, Enter", s);
+ } finally {
+ document.body.removeChild(ta);
+ }
+}
+
+export default copyToClipboard;
+\ No newline at end of file
diff --git a/src/lib/clipboardCopy/style.css b/src/lib/clipboardCopy/style.css
@@ -0,0 +1,10 @@
+.copy-clipboard-notification {
+ color: white;
+ background-color: #333;
+ border-radius: 4px;
+ padding: 0.5em 0.7em;
+ position: absolute;
+ z-index: 100000;
+ font-family: Hevetica, arial;
+ font-size: 11px;
+}
diff --git a/src/lib/contextmenu/contextmenu.css b/src/lib/contextmenu/contextmenu.css
@@ -0,0 +1,50 @@
+.contextmenu {
+ position: fixed;
+ background-color: #fff;
+ font: 12px/1.5 "Helvetica Neue",Arial,Helvetica,sans-serif;
+ box-shadow: 0 1px 7px rgba(0,0,0,0.4);
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ padding: 4px 0;
+ cursor: default;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.contextmenu .item {
+ display: block;
+ color: #222;
+ font-size: 12px;
+ line-height: 16px;
+ height: 16px;
+ text-decoration: none;
+ padding: 0 12px;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ cursor: default;
+}
+
+.contextmenu .item:hover {
+ background-color: #f4f4f4;
+ border-top: 1px solid #f0f0f0;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.contextmenu .separator span {
+ background-color: white;
+ padding: 0 0.2em;
+ color: #888;
+ margin: 0 1em;
+}
+.contextmenu .separator {
+ border-bottom: 1px solid #ccc;
+ text-align: center;
+ width: 100%;
+ line-height: 1px;
+ margin: 8px 0;
+}
+
+.contextmenu .disabled {
+ color: #aaa;
+}
+\ No newline at end of file
diff --git a/src/lib/contextmenu/contextmenu.js b/src/lib/contextmenu/contextmenu.js
@@ -0,0 +1,131 @@
+import './contextmenu.css';
+
+/*
+ items = [
+ {text: 'Hello', disabled: true},
+ '-',
+ {text: 'World', callback: fn},
+ {text: 'section', separator: true},
+ ]
+ */
+class Contextmenu {
+ constructor(items) {
+ this.items = items;
+ }
+
+ show(e) {
+ if (this._container) {
+ return;
+ }
+ if (e.originalEvent) {
+ e = e.originalEvent;
+ }
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ e.returnValue = false;
+ }
+
+ const {clientX: x, clientY: y} = e;
+
+ const container = this._container = document.createElement('div');
+ document.body.appendChild(container);
+ container.className = 'contextmenu';
+ container.style.zIndex = 10000;
+
+ window.addEventListener('keydown', this.onKeyDown);
+ window.addEventListener('mousedown', this.onMouseDown, true);
+
+ for (let item of this.createItems()) {
+ container.appendChild(item);
+ }
+ this.setPosition(x, y);
+ }
+
+ hide() {
+ if (!this._container) {
+ return;
+ }
+ document.body.removeChild(this._container);
+ this._container = null;
+
+ window.removeEventListener('keydown', this.onKeyDown);
+ window.removeEventListener('mousedown', this.onMouseDown, true);
+ }
+
+ onKeyDown = (e) => {
+ if (e.keyCode === 27) {
+ this.hide();
+ }
+ };
+
+ onMouseDown = () => {
+ this.hide();
+ };
+
+ setPosition(x, y) {
+ const window_width = window.innerWidth,
+ window_height = window.innerHeight,
+ menu_width = this._container.offsetWidth,
+ menu_height = this._container.offsetHeight;
+ if (x + menu_width >= window_width) {
+ x -= menu_width;
+ }
+ if (y + menu_height >= window_height) {
+ y -= menu_height;
+ }
+ this._container.style.left = `${x}px`;
+ this._container.style.top = `${y}px`;
+ }
+
+ *createItems() {
+ let items = this.items;
+ if (typeof items === 'function') {
+ items = items();
+ }
+ for (let itemOptions of items) {
+ if (typeof itemOptions === 'function') {
+ itemOptions = itemOptions();
+ }
+ if (itemOptions === '-' || itemOptions.separator) {
+ yield this.createSeparator(itemOptions);
+ } else {
+ yield this.createItem(itemOptions);
+ }
+ }
+ }
+
+ createItem(itemOptions) {
+ const el = document.createElement('a');
+ let className = 'item';
+ if (itemOptions.disabled) {
+ className += ' disabled';
+ }
+ el.className = className;
+ el.innerHTML = itemOptions.text;
+
+ const callback = itemOptions.callback;
+ if (callback && !itemOptions.disabled) {
+ el.addEventListener('mousedown', this.onItemClick.bind(this, callback));
+ }
+ return el;
+ }
+
+ onItemClick(callback, e) {
+ callback();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ createSeparator(itemOptions) {
+ const el = document.createElement('div');
+ el.className = 'separator';
+ if (itemOptions.text) {
+ el.innerHTML = `<span>${itemOptions.text}</span>`;
+ }
+ return el;
+ }
+
+}
+
+export default Contextmenu;
+\ No newline at end of file
diff --git a/src/lib/control.caption/caption.js b/src/lib/control.caption/caption.js
@@ -0,0 +1,25 @@
+import L from 'leaflet';
+import './style.css';
+
+L.Control.Caption = L.Control.extend({
+ options: {
+ position: 'bottomright',
+ className: 'leaflet-control-caption'
+ },
+
+ initialize: function (contents, options) {
+ L.setOptions(this, options);
+ this._contents = contents;
+ },
+
+ onAdd: function (map) {
+ this._container = L.DomUtil.create('div', this.options.className);
+ this._container.innerHTML = this._contents;
+ if (L.DomEvent) {
+ L.DomEvent.disableClickPropagation(this._container);
+ }
+ return this._container;
+ }
+
+});
+
diff --git a/src/lib/control.caption/style.css b/src/lib/control.caption/style.css
@@ -0,0 +1,17 @@
+.leaflet-control-container .leaflet-control-caption {
+ background: #fff;
+ background: rgba(255, 255, 255, 0.7);
+ margin: 0;
+ padding: 0 5px;
+ color: #333;
+ text-decoration: none;
+ font-size: 11px;
+}
+
+.leaflet-control-container .leaflet-control-caption a {
+ text-decoration: none;
+}
+
+.leaflet-control-container .leaflet-control-caption a:hover {
+ text-decoration: underline;
+}
diff --git a/src/lib/control.coordinates/coordinates.css b/src/lib/control.coordinates/coordinates.css
@@ -0,0 +1,38 @@
+.leaflet-control-coordinates {
+ display: inline-block;
+ width: 26px;
+ height: 26px;
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ background-image: url('coords16.png');
+ border-radius: 4px;
+ background-color: white;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
+ cursor: pointer;
+}
+
+.leaflet-control-coordinates.expanded {
+ background-image: none;
+ width: auto;
+ padding: 0 0.7em;
+ overflow: hidden;
+}
+
+.leaflet-control-coordinates-text {
+ display: none;
+ white-space: nowrap;
+ float: left;
+ line-height: 26px;
+}
+
+.leaflet-control-coordinates-text:first-child {
+ margin-right: 1em;
+}
+
+.leaflet-control-coordinates.expanded .leaflet-control-coordinates-text {
+ display: inline;
+}
+
+.coordinates-control-active {
+ cursor: default;
+}
+\ No newline at end of file
diff --git a/src/lib/control.coordinates/coordinates.js b/src/lib/control.coordinates/coordinates.js
@@ -0,0 +1,171 @@
+import L from 'leaflet'
+import './coordinates.css';
+import copyToClipboard from '../clipboardCopy/clipboardCopy';
+import Contextmenu from '../contextmenu/contextmenu';
+
+function pad(s, n) {
+ var j = s.indexOf('.');
+ if (j === -1) {
+ j = s.length;
+ }
+ var zeroes = (n - j);
+ if (zeroes > 0) {
+ s = Array(zeroes + 1).join('0') + s;
+ }
+ return s;
+}
+
+L.Control.Coordinates = L.Control.extend({
+ options: {
+ position: 'bottomleft'
+ },
+
+ onAdd: function(map) {
+ this._map = map;
+ var container = this._container = L.DomUtil.create('div', 'leaflet-control leaflet-control-coordinates');
+ this._field_lat = L.DomUtil.create('div', 'leaflet-control-coordinates-text', container);
+ this._field_lon = L.DomUtil.create('div', 'leaflet-control-coordinates-text', container);
+ L.DomEvent
+ .on(container, {
+ 'dblclick': L.DomEvent.stop,
+ 'click': this.onClick
+ }, this);
+ map.on('mousemove', this.onMouseMove, this);
+ this.menu = new Contextmenu([
+ {text: 'Click to copy to clipboard', callback: this.prepareForClickOnMap.bind(this)},
+ '-',
+ {text: '±ddd.ddddd', callback: this.onMenuSelect.bind(this, 'd')},
+ {text: 'ddd.ddddd°', callback: this.onMenuSelect.bind(this, 'D')},
+ {text: 'ddd°mm.mmm\'', callback: this.onMenuSelect.bind(this, 'DM')},
+ {text: 'ddd°mm\'ss.s"', callback: this.onMenuSelect.bind(this, 'DMS')}
+ ]
+ );
+ this.loadStateFromStorage();
+ this.onMouseMove();
+ L.DomEvent.on(container, 'contextmenu', this.onRightClick, this);
+ return container;
+ },
+
+ loadStateFromStorage: function() {
+ var active = false,
+ fmt = 'D';
+ if (window.Storage && window.localStorage) {
+ active = localStorage.leafletCoordinatesActive === '1';
+ fmt = localStorage.leafletCoordinatesFmt || fmt;
+ }
+ this.setEnabled(active);
+ this.setFormat(fmt);
+ },
+
+ saveStateToStorage: function() {
+ if (!(window.Storage && window.localStorage)) {
+ return;
+ }
+ localStorage.leafletCoordinatesActive = this.isEnabled() ? '1' : '0';
+ localStorage.leafletCoordinatesFmt = this.fmt;
+ },
+
+ formatCoodinate: function(value, isLat) {
+ if (value === undefined) {
+ return '-------';
+ }
+
+ var h, d, m, s;
+ if (isLat) {
+ h = (value < 0) ? 'S' : 'N';
+ } else {
+ h = (value < 0) ? 'W' : 'E';
+ }
+ if (this.fmt === 'd') {
+ d = value.toFixed(5);
+ d = pad(d, isLat ? 2 : 3);
+ return d;
+ }
+
+ value = Math.abs(value);
+ if (this.fmt === 'D') {
+ d = value.toFixed(5);
+ d = pad(d, isLat ? 2 : 3);
+ return `${h} ${d}°`;
+ }
+ if (this.fmt === 'DM') {
+ d = Math.floor(value).toString();
+ d = pad(d, isLat ? 2 : 3);
+ m = ((value - d) * 60).toFixed(3);
+ m = pad(m, 2);
+ return `${h} ${d}°${m}'`
+ }
+ if (this.fmt === 'DMS') {
+ d = Math.floor(value).toString();
+ d = pad(d, isLat ? 2 : 3);
+ m = Math.floor((value - d) * 60).toString();
+ m = pad(m, 2);
+ s = ((value - d - m / 60) * 3600).toFixed(2);
+ s = pad(s, 2);
+ return `${h} ${d}°${m}'${s}"`;
+ }
+ },
+
+ onMenuSelect: function(fmt) {
+ this.setFormat(fmt);
+ this.saveStateToStorage();
+ },
+
+ setFormat: function(fmt) {
+ this.fmt = fmt;
+ this.onMouseMove();
+ },
+
+ onMouseMove: function(e) {
+ if (!this.isEnabled()) {
+ return;
+ }
+ var lat, lng;
+ if (e) {
+ ({lat, lng} = e.latlng);
+ }
+ this._field_lat.innerHTML = this.formatCoodinate(lat, true);
+ this._field_lon.innerHTML = this.formatCoodinate(lng, false);
+ },
+
+ setEnabled: function(enabled) {
+ if (enabled) {
+ L.DomUtil.addClass(this._container, 'expanded');
+ L.DomUtil.addClass(this._map._container, 'coordinates-control-active');
+ } else {
+ L.DomUtil.removeClass(this._container, 'expanded');
+ L.DomUtil.removeClass(this._map._container, 'coordinates-control-active');
+ }
+ },
+
+ isEnabled: function() {
+ return L.DomUtil.hasClass(this._container, 'expanded');
+ },
+
+ onClick: function(e) {
+ this.setEnabled(!this.isEnabled());
+ this.saveStateToStorage();
+ this.onMouseMove();
+ },
+
+ onRightClick: function(e) {
+ this.menu.show(e);
+ },
+
+ onMapClick: function(e) {
+ var s = this.formatCoodinate(e.latlng.lat, true) + ' ' + this.formatCoodinate(e.latlng.lng, false);
+ s = s.replace(/°/g, '°');
+ copyToClipboard(s, e.originalEvent);
+ },
+
+ prepareForClickOnMap: function() {
+ this._map.once('click', this.onMapClick, this);
+ }
+
+
+
+ // TODO: onRemove
+
+ }
+);
+
diff --git a/src/lib/control.coordinates/coords16.png b/src/lib/control.coordinates/coords16.png
Binary files differ.
diff --git a/src/lib/control.layers.hotkeys/control.Layers-hotkeys.js b/src/lib/control.layers.hotkeys/control.Layers-hotkeys.js
@@ -0,0 +1,66 @@
+import L from 'leaflet'
+import './style.css';
+
+const originalOnAdd = L.Control.Layers.prototype.onAdd;
+const originalOnRemove = L.Control.Layers.prototype.onRemove;
+const originalAddLayer = L.Control.Layers.prototype._addLayer;
+
+L.Control.Layers.include({
+ _addLayer: function(layer, name, overlay) {
+ if (layer.options) {
+ const code = layer.options.code;
+ if (code.length === 1) {
+ name += `<span class="layers-control-hotkey">${code}</span>`;
+ }
+ }
+ return originalAddLayer.call(this, layer, name, overlay);
+ },
+
+ onAdd: function(map) {
+ var result = originalOnAdd.call(this, map);
+ L.DomEvent.on(document, 'keyup', this._onHotkeyUp, this);
+ L.DomEvent.on(document, 'keydown', this.onKeyDown, this);
+ return result;
+ },
+
+ onRemove: function(map) {
+ L.DomEvent.off(document, 'keyup', this._onHotkeyUp, this);
+ L.DomEvent.off(document, 'keydown', this.onKeyDown, this);
+ originalOnRemove.call(this, map);
+
+ },
+
+ onKeyDown: function(e) {
+ if (e.altKey || e.ctrlKey || e.shiftKey) {
+ return;
+ }
+ this._keyDown = e.keyCode;
+ },
+
+ _onHotkeyUp: function(e) {
+ const pressedKey = this._keyDown;
+ this._keyDown = null;
+ const targetTag = e.target.tagName.toLowerCase();
+ if (('input' === targetTag && e.target.type === 'text')|| 'textarea' === targetTag || pressedKey !== e.keyCode) {
+ return;
+ }
+ const key = String.fromCharCode(e.keyCode);
+ for (let layer of this._layers) {
+ let layerId = L.stamp(layer.layer);
+ if (layer.layer.options && layer.layer.options.code && layer.layer.options.code.toUpperCase() === key) {
+ const inputs = this._form.getElementsByTagName('input');
+ for (let input of inputs) {
+ if (input.layerId === layerId) {
+ input.click();
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ }
+);
+
+
diff --git a/src/lib/control.layers.hotkeys/style.css b/src/lib/control.layers.hotkeys/style.css
@@ -0,0 +1,5 @@
+.layers-control-hotkey {
+ font-size: 78%;
+ color: #aaa;
+ margin-left: 0.5em;
+}
+\ No newline at end of file
diff --git a/src/lib/control.printPages/control.js b/src/lib/control.printPages/control.js
@@ -0,0 +1,177 @@
+import L from 'leaflet'
+import React from 'react';
+import ReactDOM from 'react-dom';
+import '../controls-styles.css';
+import './style.css';
+import PrintPagesForm from './form';
+import PageFeature from './pageFeature';
+import Contextmenu from '../contextmenu/contextmenu';
+import {renderMap} from './map-render'
+
+L.Control.PrintPages = L.Control.extend({
+ options: {position: 'bottomleft'},
+ initialize: function(options) {
+ L.Control.prototype.initialize.call(this, options);
+ this.pages = [];
+ },
+
+ onAdd: function(map) {
+ this._map = map;
+ const container = this._container =
+ L.DomUtil.create('div', 'leaflet-control control-form control-print-pages');
+ L.DomEvent.disableClickPropagation(container);
+ if (!L.Browser.touch) {
+ L.DomEvent.disableScrollPropagation(container);
+ }
+
+ map.on('move', this.updateFormZooms, this);
+ this.form = ReactDOM.render(<PrintPagesForm
+ onAddLandscapePage={this.addLandscapePage.bind(this)}
+ onAddPortraitPage={this.addPortraitPage.bind(this)}
+ onRemovePages={this.removePages.bind(this)}
+ onSavePdf={this.savePdf.bind(this)}
+ onFormDataChanged={this.onFormDataChanged.bind(this)}
+ />, container
+ );
+ this.updateFormZooms();
+ return container;
+ },
+
+ addPage: function(data, landsacape) {
+ let {pageWidth, pageHeight, marginLeft, marginTop, marginRight, marginBottom} = data;
+ if (landsacape) {
+ [pageWidth, pageHeight] = [pageHeight, pageWidth];
+ }
+ const page = new PageFeature(this._map.getCenter(),
+ [pageWidth - marginLeft - marginRight, pageHeight - marginTop - marginBottom],
+ data.scale, (this.pages.length + 1).toString()
+ );
+ page.addTo(this._map);
+ this.pages.push(page);
+ let cm = new Contextmenu(this.makePageContexmenuItems.bind(this, page));
+ page.on('contextmenu', cm.show, cm);
+ page.on('click', this.rotatePage.bind(this, page));
+ page.on('move', this.updateFormZooms, this);
+ this.updateFormZooms();
+ return page
+ },
+
+ addLandscapePage: function(data) {
+ const page = this.addPage(data, true);
+ page._rotated = true;
+ },
+
+ addPortraitPage: function(data) {
+ this.addPage(data, false);
+ },
+
+ removePage: function(page) {
+ let i = this.pages.indexOf(page);
+ this.pages.splice(i, 1);
+ this._map.removeLayer(page);
+ for (; i < this.pages.length; i++) {
+ this.pages[i].setLabel((i + 1).toString());
+ }
+ this.updateFormZooms()
+ },
+
+ removePages: function() {
+ this.pages.forEach((page) => page.removeFrom(this._map));
+ this.pages = [];
+ this.updateFormZooms();
+ },
+
+ savePdf: function(data) {
+ if (!this._map) {
+ return;
+ }
+ renderMap(this._map);
+ },
+
+ onFormDataChanged: function(data) {
+ let {pageWidth, pageHeight, marginLeft, marginTop, marginRight, marginBottom, scale} = data;
+ this.pages.forEach((page) => {
+ let w = pageWidth - marginLeft - marginRight,
+ h = pageHeight - marginTop - marginBottom;
+ if (page._rotated) {
+ [w, h] = [h, w];
+ }
+ page.setSize([w, h], scale);
+ }
+ );
+ this.updateFormZooms();
+ },
+
+ makePageContexmenuItems: function(page) {
+ var items = [
+ {text: 'Rotate', callback: this.rotatePage.bind(this, page)},
+ '-',
+ {text: 'Delete', callback: this.removePage.bind(this, page)},
+ '-',
+ {text: 'Save image', callback: this.savePageJpg.bind(this, page), disabled: true}
+ ];
+ if (this.pages.length > 1) {
+ items.push({text: 'Change order', separator: true});
+ this.pages.forEach((p, i) => {
+ if (p !== page) {
+ items.push({
+ text: (i + 1).toString(),
+ callback: this.renumberPage.bind(this, page, i)
+ }
+ );
+ }
+ }
+ );
+ }
+ return items;
+ },
+
+ rotatePage: function(page) {
+ page._rotated = !page._rotated;
+ page.rotate();
+ },
+
+ savePageJpg: function(page) {
+
+ },
+
+ renumberPage: function(page, newIndex) {
+ const oldIndex = this.pages.indexOf(page);
+ this.pages.splice(oldIndex, 1);
+ this.pages.splice(newIndex, 0, page);
+ for (let i = Math.min(oldIndex, newIndex); i < this.pages.length; i++) {
+ this.pages[i].setLabel((i + 1).toString());
+ }
+ },
+
+ suggestZooms: function() {
+ const scale = this.form.state.scale,
+ resolution = this.form.state.resolution;
+ let referenceLat;
+ if (this.pages.length > 0) {
+ let absLats = this.pages.map((page) => {
+ return Math.abs(page.getLatLngBounds().getSouth())
+ }
+ );
+ referenceLat = Math.min(...absLats);
+ } else {
+ if (!this._map) {
+ return [null, null];
+ }
+ referenceLat = this._map.getCenter().lat;
+ }
+ var targetMetersPerPixel = scale / (resolution / 2.54);
+ var mapUnitsPerPixel = targetMetersPerPixel / Math.cos(referenceLat * Math.PI / 180);
+ var zoomSat = Math.ceil(Math.log(40075016.4 / 256 / mapUnitsPerPixel) / Math.LN2);
+
+ targetMetersPerPixel = scale / (90 / 2.54) / 1.5;
+ mapUnitsPerPixel = targetMetersPerPixel / Math.cos(referenceLat * Math.PI / 180);
+ var zoomMap = Math.round(Math.log(40075016.4 / 256 / mapUnitsPerPixel) / Math.LN2);
+ return {zoomMap, zoomSat};
+ },
+
+ updateFormZooms: function() {
+ this.form.setSuggestedZooms(this.suggestZooms());
+ }
+ }
+);
+\ No newline at end of file
diff --git a/src/lib/control.printPages/form.js b/src/lib/control.printPages/form.js
@@ -0,0 +1,197 @@
+import React, {Component} from 'react';
+
+export default class PrintPagesForm extends Component {
+ constructor() {
+ super();
+ this.state = {
+ scale: 500,
+ pageWidth: 210,
+ pageHeight: 297,
+ marginLeft: 3,
+ marginRight: 3,
+ marginTop: 3,
+ marginBottom: 3,
+ resolution: 300,
+ zoomLevel: 'auto',
+ advancedSettingsExpanded: false
+ }
+ }
+
+ render() {
+ return (
+ <table className="layout">
+ <tbody>
+ <tr>
+ <td colSpan="2">
+ <a title="Add page in portrait orientation" className="button-add-page-vert image-button"
+ onClick={this.props.onAddPortraitPage.bind(null, this.state)}/>
+ <a title="Add page in landscape orientation" className="button-add-page-horiz image-button"
+ onClick={this.props.onAddLandscapePage.bind(null, this.state)}/>
+ <a title="Remove all pages" className="button-remove-pages image-button"
+ onClick={this.props.onRemovePages}/>
+ </td>
+ </tr>
+ <tr>
+ <td className="label">Print scale</td>
+ <td>
+ <div className="preset-values">
+ <a onClick={this.handleScalePresetClicked.bind(null, 100)}>100 m</a>
+ <a onClick={this.handleScalePresetClicked.bind(null, 500)}>500 m</a>
+ <a onClick={this.handleScalePresetClicked.bind(null, 1000)}>1 km</a>
+ </div>
+ <input type="text" className="scale" maxLength="6"
+ value={this.state.scale.toString()}
+ onChange={this.handleNumericChange.bind(null, 'scale')}
+ /> m in 1 cm
+ </td>
+ </tr>
+ {this.state.advancedSettingsExpanded &&
+ <tr>
+ <td className="label">Page size</td>
+ <td>
+ <div className="preset-values">
+ <a onClick={this.handleSizePresetClicked.bind(null, 594, 841)}>A1</a>
+ <a onClick={this.handleSizePresetClicked.bind(null, 420, 594)}>A2</a>
+ <a onClick={this.handleSizePresetClicked.bind(null, 297, 420)}>A3</a>
+ <a onClick={this.handleSizePresetClicked.bind(null, 210, 297)}>A4</a>
+ <a onClick={this.handleSizePresetClicked.bind(null, 148, 210)}>A5</a>
+ </div>
+ <input type="text" maxLength="4" title="width" placeholder="width" className="page-size"
+ value={this.state.pageWidth.toString()}
+ onChange={this.handleNumericChange.bind(null, 'pageWidth')}/>
+ x
+ <input type="text" maxLength="4" title="height" placeholder="height" className="page-size"
+ value={this.state.pageHeight.toString()}
+ onChange={this.handleNumericChange.bind(null, 'pageHeight')}/>
+ mm
+ </td>
+ </tr>
+ }
+ {this.state.advancedSettingsExpanded &&
+ <tr>
+ <td className="label-high">Margins</td>
+ <td>
+ <table className="margins">
+ <tbody>
+ <tr>
+ <td />
+ <td>
+ <input type="text" maxLength="2"
+ value={this.state.marginTop.toString()}
+ onChange={this.handleNumericChange.bind(null, 'marginTop')}/>
+ </td>
+ <td />
+ </tr>
+ <tr>
+ <td>
+ <input type="text" maxLength="2"
+ value={this.state.marginLeft.toString()}
+ onChange={this.handleNumericChange.bind(null, 'marginLeft')}/>
+ </td>
+ <td />
+ <td>
+ <input type="text" maxLength="2"
+ value={this.state.marginRight.toString()}
+ onChange={this.handleNumericChange.bind(null, 'marginRight')}/> mm
+ </td>
+ </tr>
+ <tr>
+ <td />
+ <td>
+ <input type="text" maxLength="2"
+ value={this.state.marginBottom.toString()}
+ onChange={this.handleNumericChange.bind(null, 'marginBottom')}/>
+ </td>
+ <td />
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ }
+ {this.state.advancedSettingsExpanded &&
+ <tr>
+ <td className="label">Resolution</td>
+ <td><input type="text" maxLength="4" className="resolution"
+ value={this.state.resolution.toString()}
+ onChange={this.handleNumericChange.bind(null, 'resolution')}/> dpi
+ </td>
+ </tr>
+ }
+ {this.state.advancedSettingsExpanded &&
+ <tr>
+ <td className="label">Source zoom<br/>level</td>
+ <td>
+ <select value={this.state.zoomLevel} onChange={this.handleZoomChange}>
+ <option value="auto">auto</option>
+ {[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].map((i) =>
+ <option value={i.toString()} key={i.toString()}>{i.toString()}</option>
+ )}
+ </select>
+ </td>
+ </tr>
+ }
+ <tr>
+ <td colSpan="2">
+ <a className="button-settings image-button"
+ onClick={this.toggleSettingsExpanded}/>
+ <div className="settings-summary">
+ {this.state.pageWidth} x {this.state.pageHeight} mm, <br/>
+ {this.state.resolution} dpi, zoom {this.state.zoomLevel}
+ {this.state.zoomLevel === 'auto' &&
+ <span> (
+ <span title="Zoom for satellite and scanned imagery" >{this.state.zoomSat}</span>
+ /
+ <span title="Zoom for maps like OSM and Google" >{this.state.zoomMap}</span>
+ )</span>
+ }
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td colSpan="2">
+ <a className="text-button button-save" onClick={this.props.onSavePdf.bind(null, this.state)}>
+ Save PDF</a>
+ </td>
+ </tr>
+
+ </tbody>
+ </table>
+ )
+ }
+
+ onDataChanged() {
+ setTimeout(() => {this.props.onFormDataChanged(this.state)}, 0);
+ }
+
+ handleNumericChange = (stateField, e) => {
+ const value = parseInt(e.target.value, 10);
+ if (!isNaN(value)) {
+ this.setState({[stateField]: value});
+ this.onDataChanged();
+ }
+ };
+
+ handleZoomChange = (e) => {
+ this.setState({zoomLevel: e.target.value});
+ this.onDataChanged();
+ };
+
+ toggleSettingsExpanded = (e) => {
+ this.setState({advancedSettingsExpanded: !this.state.advancedSettingsExpanded});
+ };
+
+ handleScalePresetClicked = (scale) => {
+ this.setState({scale});
+ this.onDataChanged();
+ };
+
+ handleSizePresetClicked = (pageWidth, pageHeight) => {
+ this.setState({pageWidth, pageHeight});
+ this.onDataChanged();
+ };
+
+ setSuggestedZooms(zooms) {
+ this.setState(zooms);
+ }
+}
diff --git a/src/lib/control.printPages/images/add-page-horiz.png b/src/lib/control.printPages/images/add-page-horiz.png
Binary files differ.
diff --git a/src/lib/control.printPages/images/add-page-vert.png b/src/lib/control.printPages/images/add-page-vert.png
Binary files differ.
diff --git a/src/lib/control.printPages/images/remove-pages.png b/src/lib/control.printPages/images/remove-pages.png
Binary files differ.
diff --git a/src/lib/control.printPages/images/settings.png b/src/lib/control.printPages/images/settings.png
Binary files differ.
diff --git a/src/lib/control.printPages/map-render.js b/src/lib/control.printPages/map-render.js
@@ -0,0 +1,68 @@
+
+function getZIndex(el) {
+ return parseInt(window.getComputedStyle(el).zIndex, 10) || 0;
+}
+
+function compare(i1, i2) {
+ if (i1 < i2) {
+ return -1;
+ } else if (i1 > i2) {
+ return 1;
+ }
+ return 0;
+}
+
+function compareArrays(ar1, ar2) {
+ const len = Math.min(ar1.length, ar2.length);
+ for (let i = 0; i < len; i++) {
+ let c = compare(ar1[i], ar2[i]);
+ if (c) {
+ return c;
+ }
+ }
+ return compare(ar1.length, ar2.length);
+}
+
+function getLayerZOrder(layer) {
+ let el = layer._container || layer._path;
+ if (!el) {
+ throw TypeError('Unsupported layer type');
+ }
+ const order = [];
+ while (el !== layer._map._container) {
+ order.push(getZIndex(el));
+ el = el.parentElement;
+ }
+ return order.reverse()
+}
+
+function compareLayersOrder(layer1, layer2) {
+ return compareArrays(getLayerZOrder(layer1), getLayerZOrder(layer2));
+}
+
+function getLayersForPrint(map) {
+ let layers = [];
+ map.eachLayer((layer) => {
+ if (layer.options.print) {
+ layers.push(layer);
+ }
+ }
+ );
+ layers.sort(compareLayersOrder);
+ layers = layers.map((l) => {l.clone()});
+ return layers;
+}
+
+function renderPage(layers, bounds, zoom, resolution) {
+ // const canvas =
+}
+
+function savePageJpg(map, bounds, zoom, resolution) {
+ const layers = getLayersForPrint(map);
+}
+
+function savePagesPdf(map, boundsList, zoom, resolution) {
+ const layers = getLayersForPrint(map);
+}
+
+export {savePageJpg, savePagesPdf};
+\ No newline at end of file
diff --git a/src/lib/control.printPages/pageFeature.js b/src/lib/control.printPages/pageFeature.js
@@ -0,0 +1,92 @@
+import L from 'leaflet'
+
+
+const PageFeature = L.Marker.extend({
+ initialize: function(centerLatLng, paperSize, scale, label) {
+ this.paperSize = paperSize;
+ this.scale = scale;
+ var icon = L.divIcon({className: "print-page-marker", html: label});
+ L.Marker.prototype.initialize.call(this, centerLatLng, {
+ icon: icon,
+ draggable: true,
+ title: 'Left click to rotate, right click for menu'
+ }
+ );
+ this.on('drag', this.updateView.bind(this, undefined));
+ },
+
+ onAdd: function(map) {
+ L.Marker.prototype.onAdd.call(this, map);
+ map.on('viewreset', this.updateView, this);
+ this.rectangle = L.rectangle(this.getLatLngBounds(),
+ {color: '#ff7800', weight: 2, opacity: 0.7, fillOpacity: 0.2}
+ ).addTo(map);
+ this.updateView();
+
+ },
+
+ onRemove: function(map) {
+ map.off('viewreset', this.updateView, this);
+ L.Marker.prototype.onRemove.call(this, map);
+ this.rectangle.removeFrom(map);
+ },
+
+ getLatLngBounds: function() {
+ var latlng = this.getLatLng(),
+ lng = latlng.lng,
+ lat = latlng.lat;
+ var width = this.paperSize[0] * this.scale / 10 / 111319.49 / Math.cos(lat * Math.PI / 180);
+ var height = this.paperSize[1] * this.scale / 10 / 111319.49;
+ var latlng_sw = [lat - height / 2, lng - width / 2];
+ var latlng_ne = [lat + height / 2, lng + width / 2];
+ return L.latLngBounds([latlng_sw, latlng_ne]);
+ },
+
+ _animateZoom: function(e) {
+ L.Marker.prototype._animateZoom.call(this, e);
+ this.updateView(e.zoom);
+ },
+
+ updateView: function(newZoom) {
+ if (!this._map) {
+ return;
+ }
+ if (newZoom === undefined) {
+ newZoom = this._map.getZoom();
+ }
+ var bounds = this.getLatLngBounds();
+ var pixel_sw = this._map.project(bounds.getSouthWest(), newZoom);
+ var pixel_ne = this._map.project(bounds.getNorthEast(), newZoom);
+ var pixel_center = this._map.project(this.getLatLng(), newZoom);
+ var st = this._icon.style;
+ var pixel_width = pixel_ne.x - pixel_sw.x;
+ var pixel_height = pixel_sw.y - pixel_ne.y;
+ st.width = `${pixel_width}px`;
+ st.height = `${pixel_height}px`;
+ st.marginLeft = `${pixel_sw.x - pixel_center.x}px`;
+ st.marginTop = `${pixel_ne.y - pixel_center.y}px`;
+ st.fontSize = `${Math.min(pixel_width, pixel_height, 500) / 2}px`;
+ st.lineHeight = `${pixel_height}px`;
+ this.rectangle.setBounds(bounds);
+ },
+
+ setLabel: function(s) {
+ this._icon.innerHTML = s;
+ },
+
+ setSize: function(paperSize, scale) {
+ this.paperSize = paperSize;
+ this.scale = scale;
+ console.log(paperSize, scale);
+ this.updateView();
+ },
+
+ rotate: function(e) {
+ this.paperSize = [this.paperSize[1], this.paperSize[0]];
+ this.updateView();
+ }
+
+ }
+);
+
+export default PageFeature;
+\ No newline at end of file
diff --git a/src/lib/control.printPages/style.css b/src/lib/control.printPages/style.css
@@ -0,0 +1,120 @@
+.control-print-pages {
+ line-height: 1.4;
+}
+
+.control-print-pages .scale {
+ width: 3.2em;
+}
+
+.control-print-pages .page-size {
+ width: 2.1em;
+}
+
+.control-print-pages .margins input {
+ width: 1.1em;
+}
+
+.control-print-pages .resolution {
+ width: 2.1em;
+}
+
+.button-add-page-horiz {
+ background-image: url("images/add-page-horiz.png");
+ margin-left: 2mm;
+}
+
+.button-add-page-vert {
+ background-image: url("images/add-page-vert.png");
+}
+
+.button-remove-pages {
+ background-image: url("images/remove-pages.png");
+ float: right;
+}
+
+.button-settings {
+ background-image: url("images/settings.png");
+}
+
+.control-print-pages table {
+ border-collapse: collapse;
+}
+
+.control-print-pages tr,
+.control-print-pages td,
+.control-print-pages tbody {
+ margin: 0;
+ padding: 0;
+}
+
+.control-print-pages td {
+ border-top: 1px solid #eee;
+ padding-top: 1.5mm;
+ padding-bottom: 1mm;
+}
+
+.control-print-pages tr:first-child td {
+ border: 0;
+}
+
+.control-print-pages td:nth-child(2) {
+ padding-left: 2mm;
+}
+
+.control-print-pages .margins td {
+ padding: 0;
+ border: 0;
+}
+
+.control-print-pages .settings-summary {
+ display: inline-block;
+ margin-left: 3mm;
+}
+
+.control-print-pages tr:last-child td {
+ text-align: center;
+}
+
+.preset-values {
+ margin-bottom: 1.5mm;
+}
+
+.preset-values a {
+ color: #999;
+ border-bottom: 1px dotted #999;
+ cursor: pointer;
+ margin-right: 2.5mm;
+}
+
+.preset-values a:last-child {
+ margin-right: 0;
+}
+
+.control-print-pages .label {
+ vertical-align: bottom;
+}
+
+.control-print-pages .label-high {
+ vertical-align: middle;
+}
+
+.print-page-marker {
+ box-sizing: border-box;
+ color: rgba(255, 120, 0, 0.5);
+ text-align: center;
+ /*background-color: rgba(255, 120, 0, 0.2);*/
+ /*border: solid 2px rgba(255, 120, 0, 0.7);*/
+ margin: 0;
+ vertical-align: middle;
+
+}
+
+.leaflet-zoom-anim .leaflet-zoom-animated.print-page-marker {
+ transition:
+ width 0.25s cubic-bezier(0, 0, 0.25, 1),
+ height 0.25s cubic-bezier(0, 0, 0.25, 1),
+ margin 0.25s cubic-bezier(0, 0, 0.25, 1),
+ font-size 0.25s cubic-bezier(0, 0, 0.25, 1),
+ line-height 0.25s cubic-bezier(0, 0, 0.25, 1),
+ transform 0.25s cubic-bezier(0,0,0.25,1);
+}
+\ No newline at end of file
diff --git a/src/lib/controls-styles.css b/src/lib/controls-styles.css
@@ -0,0 +1,44 @@
+.leaflet-control.control-form {
+ background-color: white;
+ border-radius: 5px 5px 5px 5px;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
+ padding: 5px 10px 5px 12px;
+ color: #333;
+}
+
+.leaflet-control.control-form input[type="text"] {
+ padding: 0 5px;
+ margin: 0;
+ border-radius: 3px;
+ border: 1px solid #ddd;
+ text-align: right;
+}
+
+.leaflet-control .image-button {
+ width: 26px;
+ height: 26px;
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ display: inline-block;
+ height: 26px;
+ border-radius: 4px 4px 4px 4px;
+ border: 1px solid #ccc;
+ cursor: pointer;
+}
+
+.leaflet-control .image-button:hover,
+.leaflet-control .text-button:hover {
+ background-color: #f4f4f4;
+}
+
+.leaflet-control .text-button {
+ display: inline-block;
+ height: 26px;
+ border-radius: 4px 4px 4px 4px;
+ border: 1px solid #ccc;
+ cursor: pointer;
+ padding: 0 1em;
+ line-height: 26px;
+ font-weight: bold;
+ color: #333;
+}
+\ No newline at end of file
diff --git a/src/lib/layer.yandex/yandex.js b/src/lib/layer.yandex/yandex.js
@@ -0,0 +1,52 @@
+import L from 'leaflet'
+
+const yandexCrs = L.CRS.EPSG3395;
+
+const origPxBoundsToTileRange = L.TileLayer.prototype._pxBoundsToTileRange;
+const origInitTile = L.TileLayer.prototype._initTile;
+const origCreateTile = L.TileLayer.prototype.createTile;
+
+L.Layer.Yandex = L.TileLayer.extend({
+ options: {
+ subdomains: '1234'
+ },
+
+ initialize: function(mapType, options) {
+ var url;
+ this._mapType = mapType;
+ if (mapType === 'sat') {
+ url = 'https://sat0{s}.maps.yandex.net/tiles?l=sat&x={x}&y={y}&z={z}';
+ } else {
+ url = 'https://vec0{s}.maps.yandex.net/tiles?l=map&x={x}&y={y}&z={z}';
+ }
+
+ L.TileLayer.prototype.initialize.call(this, url, options);
+ },
+
+ _getTilePos: function(coords) {
+ const tilePosLatLng = yandexCrs.pointToLatLng(coords.scaleBy(this.getTileSize()), coords.z);
+ return this._map.project(tilePosLatLng, coords.z).subtract(this._level.origin).round();
+ },
+
+ _pxBoundsToTileRange: function(bounds) {
+ const zoom = this._tileZoom;
+ const bounds2 = new L.Bounds(
+ yandexCrs.latLngToPoint(this._map.unproject(bounds.min, zoom), zoom),
+ yandexCrs.latLngToPoint(this._map.unproject(bounds.max, zoom), zoom));
+ return origPxBoundsToTileRange.call(this, bounds2);
+ },
+
+ createTile: function(coords, done) {
+ const tile = origCreateTile.call(this, coords, done);
+ const coordsBelow = L.point(coords).add([0, 1]);
+ coordsBelow.z = coords.z;
+ tile._adjustHeight = this._getTilePos(coordsBelow).y - this._getTilePos(coords).y;
+ return tile
+ },
+
+ _initTile: function(tile) {
+ origInitTile.call(this, tile);
+ tile.style.height = `${tile._adjustHeight}px`;
+ }
+ }
+);
+\ No newline at end of file
diff --git a/src/lib/xhr-promise/xhr-promise.js b/src/lib/xhr-promise/xhr-promise.js
@@ -0,0 +1,19 @@
+export default function XMLHttpRequestPromise(url, method='GET', data=null, responseType='', timeout=0) {
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+ xhr.responseType = responseType;
+ xhr.timeout = timeout;
+ const result = new Promise(function(resolve, reject) {
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === 4) {
+ if (xhr.status) {
+ resolve(xhr);
+ } else {
+ reject(xhr);
+ }
+ }
+ }
+ });
+ xhr.send();
+ return result;
+}