index.js (7447B)
1 import L from 'leaflet'; 2 import ko from 'knockout'; 3 import './coordinates.css'; 4 import copyToClipboard from '~/lib/clipboardCopy'; 5 import Contextmenu from '~/lib/contextmenu'; 6 import {makeButtonWithBar} from '~/lib/leaflet.control.commons'; 7 import safeLocalStorage from '~/lib/safe-localstorage'; 8 import '~/lib/controls-styles/controls-styles.css'; 9 import {ElevationLayer} from '~/lib/leaflet.layer.elevation-display'; 10 import * as formats from './formats'; 11 12 const DEFAULT_FORMAT = formats.DEGREES; 13 const UNKNOWN_COORDINATES = { 14 lat: '-------', 15 lng: '-------' 16 }; 17 18 L.Control.Coordinates = L.Control.extend({ 19 options: { 20 position: 'bottomleft' 21 }, 22 23 includes: L.Mixin.Events, 24 25 formats: [ 26 formats.SIGNED_DEGREES, 27 formats.DEGREES, 28 formats.DEGREES_AND_MINUTES, 29 formats.DEGREES_AND_MINUTES_AND_SECONDS 30 ], 31 32 initialize: function(elevationTilesUrl, options) { 33 L.Control.prototype.initialize.call(this, options); 34 35 this.elevationDisplayLayer = new ElevationLayer(elevationTilesUrl); 36 37 this.latlng = ko.observable(); 38 this.format = ko.observable(DEFAULT_FORMAT); 39 this.formatCode = ko.pureComputed({ 40 read: () => this.format().code, 41 write: (value) => { 42 for (let format of this.formats) { 43 if (value === format.code) { 44 this.format(format); 45 break; 46 } 47 } 48 } 49 }); 50 51 this.formattedCoordinates = ko.pureComputed(() => { 52 if (this.latlng()) { 53 return formats.formatLatLng(this.latlng().wrap(), this.format()); 54 } 55 return UNKNOWN_COORDINATES; 56 }, this); 57 58 this.wrapperClass = ko.pureComputed(() => this.format().wrapperClass, this); 59 60 this.formatCode.subscribe(this.saveStateToStorage, this); 61 }, 62 63 onAdd: function(map) { 64 const {container, link, barContainer} = makeButtonWithBar( 65 'leaflet-contol-coordinates', 66 'Show coordinates at cursor', 67 'icon-coordinates' 68 ); 69 70 this._map = map; 71 this._container = container; 72 this._link = link; 73 74 barContainer.innerHTML = ` 75 <div data-bind="css: wrapperClass"> 76 <div class="leaflet-coordinates-container" data-bind="with: formattedCoordinates()"> 77 <span class="leaflet-coordinates-latitude" data-bind="html: lat"></span> 78 <span class="leaflet-coordinates-longitude" data-bind="html: lng"></span> 79 </div> 80 <hr class="leaflet-coordinates-divider" /> 81 <div data-bind="foreach: formats"> 82 <div> 83 <label title=""> 84 <input type="radio" data-bind="checked: $parent.formatCode, value: code" 85 class="leaflet-coordinates-format-radio"/> 86 <span data-bind="html: label"></span> 87 </label> 88 </div> 89 </div> 90 </div> 91 `; 92 93 barContainer.title = "Right click on map to copy coordinates"; 94 95 this.loadStateFromStorage(); 96 ko.applyBindings(this, container); 97 L.DomEvent.on(link, 'click', this.onClick, this); 98 99 return container; 100 }, 101 102 onRemove: function() { 103 L.DomEvent.off(this._link, 'click', this.onClick, this); 104 }, 105 106 loadStateFromStorage: function() { 107 const active = safeLocalStorage.leafletCoordinatesActive === '1'; 108 const code = safeLocalStorage.leafletCoordinatesFmt || DEFAULT_FORMAT.code; 109 110 this.formatCode(code); 111 this.setEnabled(active); 112 }, 113 114 saveStateToStorage: function() { 115 safeLocalStorage.leafletCoordinatesActive = this.isEnabled() ? '1' : '0'; 116 safeLocalStorage.leafletCoordinatesFmt = this.formatCode(); 117 }, 118 119 onMouseMove: function(e) { 120 this.latlng(e.latlng); 121 }, 122 123 setEnabled: function(enabled) { 124 if (Boolean(enabled) === this.isEnabled()) { 125 return; 126 } 127 const classFunc = enabled ? 'addClass' : 'removeClass'; 128 const eventFunc = enabled ? 'on' : 'off'; 129 L.DomUtil[classFunc](this._container, 'active'); 130 L.DomUtil[classFunc](this._container, 'highlight'); 131 L.DomUtil[classFunc](this._map._container, 'coordinates-control-active'); 132 this._map[eventFunc]('mousemove', this.onMouseMove, this); 133 this._map[eventFunc]('contextmenu', this.onMapRightClick, this); 134 this._map[enabled ? 'addLayer' : 'removeLayer'](this.elevationDisplayLayer); 135 this._isEnabled = Boolean(enabled); 136 this.latlng(null); 137 }, 138 139 onMapRightClick: function(e) { 140 L.DomEvent.stop(e); 141 function createItem(format, elevation, overrides = {}) { 142 const {lat, lng} = formats.formatLatLng(e.latlng.wrap(), format); 143 let text = `${lat} ${lng}`; 144 if (elevation !== null) { 145 text += ` H=${elevation} m`; 146 } 147 148 return {text: `${text} <span class="leaflet-coordinates-menu-fmt">${format.label}</span>`, 149 callback: () => copyToClipboard(text, e.originalEvent), 150 ...overrides}; 151 } 152 153 const items = [ 154 createItem( 155 this.format(), 156 null, 157 { 158 text: '<b>Copy coordinates to clipboard</b>', 159 header: true, 160 }, 161 ), 162 ...this.formats.map((format) => createItem(format, null)), 163 ]; 164 const elevationResult = this.elevationDisplayLayer.getElevation(e.latlng); 165 if (elevationResult.ready && !elevationResult.error && elevationResult.elevation !== null) { 166 const elevation = elevationResult.elevation; 167 items.push( 168 '-', 169 { 170 text: `Copy elevation to clipboard: ${elevation}`, 171 header: true, 172 callback: () => copyToClipboard(elevation, e.originalEvent), 173 }, 174 '-', 175 createItem( 176 this.format(), 177 elevation, 178 { 179 text: '<b>Copy coordinates with elevation to clipboard</b>', 180 header: true, 181 }, 182 ), 183 ...this.formats.map((format) => createItem(format, elevation)), 184 ); 185 } 186 new Contextmenu(items).show(e); 187 }, 188 189 isEnabled: function() { 190 return Boolean(this._isEnabled); 191 }, 192 193 onClick: function() { 194 this.setEnabled(!this.isEnabled()); 195 this.saveStateToStorage(); 196 } 197 } 198 );