index.js (8544B)
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 this.displayElevation = ko.observable(true); 37 38 this.latlng = ko.observable(); 39 this.format = ko.observable(DEFAULT_FORMAT); 40 this.formatCode = ko.pureComputed({ 41 read: () => this.format().code, 42 write: (value) => { 43 for (let format of this.formats) { 44 if (value === format.code) { 45 this.format(format); 46 break; 47 } 48 } 49 } 50 }); 51 52 this.formattedCoordinates = ko.pureComputed(() => { 53 if (this.latlng()) { 54 return formats.formatLatLng(this.latlng().wrap(), this.format()); 55 } 56 return UNKNOWN_COORDINATES; 57 }, this); 58 59 this.wrapperClass = ko.pureComputed(() => this.format().wrapperClass, this); 60 61 this.formatCode.subscribe(this.saveFormatStateToStorage, this); 62 this.displayElevation.subscribe(this.onDisplayElevationChange, this); 63 }, 64 65 onAdd: function(map) { 66 const {container, link, barContainer} = makeButtonWithBar( 67 'leaflet-contol-coordinates', 68 'Show coordinates at cursor', 69 'icon-coordinates' 70 ); 71 72 this._map = map; 73 this._container = container; 74 this._link = link; 75 76 barContainer.innerHTML = ` 77 <div data-bind="css: wrapperClass"> 78 <div class="leaflet-coordinates-container" data-bind="with: formattedCoordinates()"> 79 <span class="leaflet-coordinates-latitude" data-bind="html: lat"></span> 80 <span class="leaflet-coordinates-longitude" data-bind="html: lng"></span> 81 </div> 82 <hr class="leaflet-coordinates-divider" /> 83 <div data-bind="foreach: formats"> 84 <div> 85 <label title=""> 86 <input type="radio" data-bind="checked: $parent.formatCode, value: code" 87 class="leaflet-coordinates-format-radio"/> 88 <span data-bind="html: label"></span> 89 </label> 90 </div> 91 </div> 92 <hr class="leaflet-coordinates-divider" /> 93 <label title=""> 94 <input type="checkbox" 95 data-bind="checked: displayElevation" 96 class="leaflet-coordinates-elevation-toggle"/> 97 Display elevation 98 </label> 99 </div> 100 `; 101 102 barContainer.title = "Right click on map to copy coordinates"; 103 104 this.loadStateFromStorage(); 105 ko.applyBindings(this, container); 106 L.DomEvent.on(link, 'click', this.onClick, this); 107 108 return container; 109 }, 110 111 onRemove: function() { 112 L.DomEvent.off(this._link, 'click', this.onClick, this); 113 }, 114 115 loadStateFromStorage: function() { 116 const active = safeLocalStorage.leafletCoordinatesActive === '1'; 117 const code = safeLocalStorage.leafletCoordinatesFmt || DEFAULT_FORMAT.code; 118 const elevationDisplayed = (safeLocalStorage.leafletCoordinatesDisplayElevation ?? '1') === '1'; 119 120 this.formatCode(code); 121 this.displayElevation(elevationDisplayed); 122 this.setEnabled(active); 123 }, 124 125 saveEnabledStateToStorage: function() { 126 safeLocalStorage.leafletCoordinatesActive = this.isEnabled() ? '1' : '0'; 127 }, 128 129 saveFormatStateToStorage: function() { 130 safeLocalStorage.leafletCoordinatesFmt = this.formatCode(); 131 }, 132 133 saveElevationDisplayStateToStorage: function() { 134 safeLocalStorage.leafletCoordinatesDisplayElevation = this.displayElevation() ? '1' : '0'; 135 }, 136 137 onMouseMove: function(e) { 138 this.latlng(e.latlng); 139 }, 140 141 setEnabled: function(enabled) { 142 if (Boolean(enabled) === this.isEnabled()) { 143 return; 144 } 145 const classFunc = enabled ? 'addClass' : 'removeClass'; 146 const eventFunc = enabled ? 'on' : 'off'; 147 L.DomUtil[classFunc](this._container, 'active'); 148 L.DomUtil[classFunc](this._container, 'highlight'); 149 L.DomUtil[classFunc](this._map._container, 'coordinates-control-active'); 150 this._map[eventFunc]('mousemove', this.onMouseMove, this); 151 this._map[eventFunc]('contextmenu', this.onMapRightClick, this); 152 this._map[enabled ? 'addLayer' : 'removeLayer'](this.elevationDisplayLayer); 153 this._isEnabled = Boolean(enabled); 154 this.latlng(null); 155 }, 156 157 onMapRightClick: function(e) { 158 L.DomEvent.stop(e); 159 function createItem(format, elevation, overrides = {}) { 160 const {lat, lng} = formats.formatLatLng(e.latlng.wrap(), format); 161 let text = `${lat} ${lng}`; 162 if (elevation !== null) { 163 text += ` H=${elevation} m`; 164 } 165 166 return {text: `${text} <span class="leaflet-coordinates-menu-fmt">${format.label}</span>`, 167 callback: () => copyToClipboard(text, e.originalEvent), 168 ...overrides}; 169 } 170 171 const items = [ 172 createItem( 173 this.format(), 174 null, 175 { 176 text: '<b>Copy coordinates to clipboard</b>', 177 header: true, 178 }, 179 ), 180 ...this.formats.map((format) => createItem(format, null)), 181 ]; 182 const elevationResult = this.elevationDisplayLayer.getElevation(e.latlng); 183 if (elevationResult.ready && !elevationResult.error && elevationResult.elevation !== null) { 184 const elevation = elevationResult.elevation; 185 items.push( 186 '-', 187 { 188 text: `Copy elevation to clipboard: ${elevation}`, 189 header: true, 190 callback: () => copyToClipboard(elevation, e.originalEvent), 191 }, 192 '-', 193 createItem( 194 this.format(), 195 elevation, 196 { 197 text: '<b>Copy coordinates with elevation to clipboard</b>', 198 header: true, 199 }, 200 ), 201 ...this.formats.map((format) => createItem(format, elevation)), 202 ); 203 } 204 new Contextmenu(items).show(e); 205 }, 206 207 isEnabled: function() { 208 return Boolean(this._isEnabled); 209 }, 210 211 onClick: function() { 212 this.setEnabled(!this.isEnabled()); 213 this.saveEnabledStateToStorage(); 214 }, 215 216 onDisplayElevationChange: function(on) { 217 this.saveElevationDisplayStateToStorage(); 218 this.elevationDisplayLayer.enableElevationDisplay(on); 219 }, 220 } 221 );