commit e2ca3031ebc2a93a1e5ba8e830710b84e6c59508
parent da47dd99b35106e59b855e8b275fd8a81fe77d3f
Author: Igor Sidorov <igor.sidorov@binarycode.ru>
Date: Mon, 4 Jun 2018 13:48:30 +0300
Replace coordinates context menu with radio buttons
Diffstat:
3 files changed, 181 insertions(+), 146 deletions(-)
diff --git a/src/lib/leaflet.control.coordinates/coordinates.css b/src/lib/leaflet.control.coordinates/coordinates.css
@@ -13,4 +13,5 @@
.leaflet-coordinates-menu-fmt{
color: #aaa;
margin-left: 0.5em;
-}
-\ No newline at end of file
+ float: right;
+}
diff --git a/src/lib/leaflet.control.coordinates/formats.js b/src/lib/leaflet.control.coordinates/formats.js
@@ -0,0 +1,88 @@
+function pad(value, size, precision = 0) {
+ if (value < 0) {
+ return value.toFixed(precision);
+ }
+
+ if (precision > 0) size++;
+
+ return value.toFixed(precision).padStart(size + precision, '0');
+}
+
+function parseLatLng(signedDegrees, isLat) {
+ const degrees = Math.abs(signedDegrees);
+ const intDegrees = Math.floor(degrees);
+ const minutes = (degrees - intDegrees) * 60;
+ const intMinutes = Math.floor(minutes);
+ const seconds = (minutes - intMinutes) * 60;
+
+ let direction
+ if (isLat) {
+ direction = (signedDegrees < 0) ? 'S' : 'N';
+ } else {
+ direction = (signedDegrees < 0) ? 'W' : 'E';
+ }
+
+ const degreesPadding = isLat ? 2 : 3;
+
+ return {
+ signedDegrees: pad(signedDegrees, degreesPadding, 5),
+ degrees: pad(degrees, degreesPadding, 5),
+ intDegrees: pad(intDegrees, degreesPadding),
+ minutes: pad(minutes, 2, 3),
+ intMinutes: pad(minutes, 2),
+ seconds: pad(seconds, 2, 2),
+ direction
+ };
+}
+
+function transform(latlng, format) {
+ return {
+ lat: format(parseLatLng(latlng.lat, true)),
+ lng: format(parseLatLng(latlng.lng, false))
+ }
+}
+
+const SIGNED_DEGREES = {
+ code: 'd',
+ label: '±ddd.ddddd',
+ process: (latlng) => {
+ return transform(latlng, ({signedDegrees}) => signedDegrees);
+ }
+};
+
+const DEGREES = {
+ code: 'D',
+ label: 'ddd.ddddd°',
+ process: (latlng) => {
+ return transform(latlng, ({degrees, direction}) => {
+ return `${direction} ${degrees}°`;
+ });
+ }
+};
+
+const DEGREES_AND_MINUTES = {
+ code: 'DM',
+ label: 'ddd°mm.mmm′',
+ process: (latlng) => {
+ return transform(latlng, ({intDegrees, minutes, direction}) => {
+ return `${direction} ${intDegrees}°${minutes}′`;
+ });
+ }
+};
+
+const DEGREES_AND_MINUTES_AND_SECONDS = {
+ code: 'DMS',
+ label: 'ddd°mm′ss.s″',
+ process: (latlng) => {
+ return transform(latlng, ({intDegrees, intMinutes, seconds, direction}) => {
+ return `${direction} ${intDegrees}°${intMinutes}′${seconds}″`;
+ });
+ }
+};
+
+export default {
+ SIGNED_DEGREES,
+ DEGREES,
+ DEGREES_AND_MINUTES,
+ DEGREES_AND_MINUTES_AND_SECONDS
+}
diff --git a/src/lib/leaflet.control.coordinates/index.js b/src/lib/leaflet.control.coordinates/index.js
@@ -1,141 +1,109 @@
import L from 'leaflet'
+import ko from 'vendored/knockout';
import './coordinates.css';
import copyToClipboard from 'lib/clipboardCopy';
import Contextmenu from 'lib/contextmenu';
import {makeButtonWithBar} from 'lib/leaflet.control.commons';
import safeLocalStorage from 'lib/safe-localstorage';
import 'lib/controls-styles/controls-styles.css';
+import FORMATS from './formats';
-
-
-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;
-}
-
-function mod(n, m) {
- return ((n % m) + m) % m;
-}
-
-function normalizeLongitude(lng) {
- return mod(lng + 180, 360) - 180;
-}
+const DEFAULT_FORMAT = FORMATS.DEGREES;
+const UNKNOWN_COORDINATES = {
+ lat: '-------',
+ lng: '-------'
+};
L.Control.Coordinates = L.Control.extend({
options: {
position: 'bottomleft'
},
- formatNames: {
- 'd': 'ddd.ddddd',
- 'D': 'ddd.ddddd°',
- 'DM': 'ddd°mm.mmm\'',
- 'DMS': 'ddd°mm\'ss.s"'
+ formats: [
+ FORMATS.SIGNED_DEGREES,
+ FORMATS.DEGREES,
+ FORMATS.DEGREES_AND_MINUTES,
+ FORMATS.DEGREES_AND_MINUTES_AND_SECONDS
+ ],
+
+ initialize: function(options) {
+ L.Control.prototype.initialize.call(this, options);
+
+ this.latlng = ko.observable();
+ this.formatCode = ko.observable(DEFAULT_FORMAT.code);
+
+ this.format = ko.pureComputed(() => {
+ for (let format of this.formats) {
+ if (format.code === this.formatCode()) return format;
+ }
+ return DEFAULT_FORMAT;
+ }, this);
+
+ this.formattedCoordinates = ko.pureComputed(() => {
+ if (this.latlng()) {
+ return this.format().process(this.latlng().wrap());
+ }
+ return UNKNOWN_COORDINATES;
+ }, this);
+
+ this.formatCode.subscribe((_) => {
+ this.saveStateToStorage();
+ }, this);
},
onAdd: function(map) {
- this._map = map;
const {container, link, barContainer} = makeButtonWithBar(
- 'leaflet-contol-coordinates', 'Show coordinates at cursor', 'icon-coordinates');
- this._container = container;
+ 'leaflet-contol-coordinates',
+ 'Show coordinates at cursor',
+ 'icon-coordinates'
+ );
- this._coordinatesDisplayContainer = L.DomUtil.create('span', '', barContainer);
+ this._map = map;
+ this._container = container;
+ this._link = link;
+
+ barContainer.innerHTML = `
+ <div data-bind="with: formattedCoordinates()">
+ <span data-bind="html: lat"></span>
+
+ <span data-bind="html: lng"></span>
+ </div>
+ <div data-bind="foreach: formats">
+ <div>
+ <label>
+ <input type="radio" data-bind="checked: $parent.formatCode, value: code" />
+ <span data-bind="html: label"></span>
+ </label>
+ </div>
+ </div>
+ `;
- L.DomEvent.on(link, 'click', this.onClick, this);
- this.menu = new Contextmenu([
- {text: 'Click map to copy coordinates 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(barContainer, 'contextmenu', this.onRightClick, this);
+ ko.applyBindings(this, container);
+ L.DomEvent.on(link, 'click', this.onClick, this);
+
return container;
},
+ onRemove: function() {
+ L.DomEvent.off(this._link, 'click', this.onClick, this);
+ },
+
loadStateFromStorage: function() {
- var active = false,
- fmt = 'D';
- active = safeLocalStorage.leafletCoordinatesActive === '1';
- fmt = safeLocalStorage.leafletCoordinatesFmt || fmt;
+ const active = safeLocalStorage.leafletCoordinatesActive === '1';
+ const code = safeLocalStorage.leafletCoordinatesFmt || DEFAULT_FORMAT.code;
+
+ this.formatCode(code);
this.setEnabled(active);
- this.setFormat(fmt);
},
saveStateToStorage: function() {
safeLocalStorage.leafletCoordinatesActive = this.isEnabled() ? '1' : '0';
- safeLocalStorage.leafletCoordinatesFmt = this.fmt;
- },
-
- formatCoodinate: function(value, isLat, fmt) {
- if (value === undefined || isNaN(value)) {
- return '-------';
- }
- fmt = fmt || this.fmt;
- var h, d, m, s;
- if (isLat) {
- h = (value < 0) ? 'S' : 'N';
- } else {
- h = (value < 0) ? 'W' : 'E';
- }
- if (fmt === 'd') {
- d = value.toFixed(5);
- d = pad(d, isLat ? 2 : 3);
- return d;
- }
-
- value = Math.abs(value);
- if (fmt === 'D') {
- d = value.toFixed(5);
- d = pad(d, isLat ? 2 : 3);
- return `${h} ${d}°`;
- }
- if (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 (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();
+ safeLocalStorage.leafletCoordinatesFmt = this.formatCode();
},
onMouseMove: function(e) {
- var lat, lng;
- if (e) {
- ({lat, lng} = e.latlng);
- }
- lng = normalizeLongitude(lng);
- this._coordinatesDisplayContainer.innerHTML = this.formatCoodinate(lat, true) + ' ' + this.formatCoodinate(lng, false);
+ this.latlng(e.latlng);
},
setEnabled: function(enabled) {
@@ -154,25 +122,25 @@ L.Control.Coordinates = L.Control.extend({
onMapRightClick: function(e) {
L.DomEvent.stop(e);
- const items = [{text: '<b>Copy coordinates to clipboard</b>', header: true}, '-'];
-
- const lat = e.latlng.lat,
- lng = normalizeLongitude(e.latlng.lng);
-
- for (let fmt of ['d', 'D', 'DM', 'DMS']) {
- let strLat = this.formatCoodinate(lat, true, fmt);
- let strLng = this.formatCoodinate(lng, false, fmt);
- let s = `${strLat} ${strLng}`;
- items.push({
- text: `${s} <span class="leaflet-coordinates-menu-fmt">${this.formatNames[fmt]}</span>`,
- callback: () => {
- copyToClipboard(s.replace(/°/g, '°'), e.originalEvent);
- }
- }
- )
+
+ const createItem = (format, options = {}) => {
+ const {lat, lng} = format.process(e.latlng.wrap());
+ const coordinates = `${lat} ${lng}`;
+
+ return Object.assign({
+ text: `${coordinates} <span class="leaflet-coordinates-menu-fmt">${format.label}</span>`,
+ callback: () => copyToClipboard(coordinates, e.originalEvent)
+ }, options);
}
- const menu = new Contextmenu(items);
- menu.show(e);
+
+ const header = createItem(this.format(), {
+ text: '<b>Copy coordinates to clipboard</b>',
+ header: true,
+ });
+ const items = this.formats.map((format) => createItem(format));
+ items.unshift(header, '-');
+
+ new Contextmenu(items).show(e);
},
isEnabled: function() {
@@ -182,27 +150,6 @@ L.Control.Coordinates = L.Control.extend({
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
-
}
);
-