commit 76251795ee71edbbb8eea2f79ca08770b820f175
parent a034447a440a15013cc93346c93fc9466181f65c
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Tue, 3 Jul 2018 01:13:08 +0300
Merge branch 'binarycode-coordinates'
Diffstat:
4 files changed, 229 insertions(+), 159 deletions(-)
diff --git a/src/lib/contextmenu/contextmenu.css b/src/lib/contextmenu/contextmenu.css
@@ -1,6 +1,6 @@
.contextmenu {
position: fixed;
- background-color: #fff;
+ background-color: white;
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;
@@ -14,7 +14,7 @@
.contextmenu .item {
display: block;
- color: #222;
+ color: hsl(0, 0%, 15%);
font-size: 12px;
line-height: 16px;
height: 16px;
@@ -26,30 +26,28 @@
}
.contextmenu .item:hover {
- background-color: #f4f4f4;
- border-top: 1px solid #f0f0f0;
- border-bottom: 1px solid #f0f0f0;
+ background-color: hsl(0, 0%, 95%);
}
.contextmenu .separator span {
background-color: white;
padding: 0 0.2em;
- color: #888;
+ color: hsl(0, 0%, 50%);
margin: 0 1em;
}
.contextmenu .separator {
- border-bottom: 1px solid #ccc;
+ border-bottom: 1px solid hsl(0, 0%, 80%);
text-align: center;
width: 100%;
- line-height: 1px;
+ line-height: 0;
margin: 8px 0;
}
.contextmenu .disabled {
- color: #aaa;
+ color: hsl(0, 0%, 66%);
}
.contextmenu .header {
- color: #555555;
+ color: hsl(0, 0%, 20%);
font-weight: bold;
}
\ No newline at end of file
diff --git a/src/lib/leaflet.control.coordinates/coordinates.css b/src/lib/leaflet.control.coordinates/coordinates.css
@@ -10,7 +10,44 @@
cursor: crosshair;
}
-.leaflet-coordinates-menu-fmt{
- color: #aaa;
+.leaflet-coordinates-menu-fmt {
+ color: hsl(0, 0%, 66%);
margin-left: 0.5em;
-}
-\ No newline at end of file
+ float: right;
+}
+
+.leaflet-coordinates-wrapper-signed-degrees {
+ min-width: 11em;
+}
+
+.leaflet-coordinates-wrapper-degrees {
+ min-width: 14em;
+}
+
+.leaflet-coordinates-wrapper-degrees-and-minutes {
+ min-width: 14.5em;
+}
+
+.leaflet-coordinates-wrapper-degrees-and-minutes-and-seconds {
+ min-width: 16em;
+}
+
+.leaflet-coordinates-container {
+ padding: 0 5px;
+}
+
+.leaflet-coordinates-longitude {
+ float: right;
+}
+
+.leaflet-coordinates-format-radio {
+ margin-top: -1px;
+ vertical-align: middle;
+}
+
+.leaflet-coordinates-divider {
+ border: none;
+ border-top: 1px solid hsl(0, 0%, 92%);
+ margin-top: 0px;
+ margin-bottom: 3px;
+}
diff --git a/src/lib/leaflet.control.coordinates/formats.js b/src/lib/leaflet.control.coordinates/formats.js
@@ -0,0 +1,76 @@
+function formatNumber(value, size, precision = 0) {
+ if (value < 0) {
+ return value.toFixed(precision);
+ }
+
+ if (precision > 0) size++;
+
+ return value.toFixed(precision).padStart(size + precision, '0');
+}
+
+function coordinatePresentations(coordinate, isLat) {
+ const degrees = Math.abs(coordinate);
+ 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 = (coordinate < 0) ? 'S' : 'N';
+ } else {
+ direction = (coordinate < 0) ? 'W' : 'E';
+ }
+
+ return {
+ signedDegrees: formatNumber(coordinate, 0, 5),
+ degrees: formatNumber(degrees, 0, 5),
+ intDegrees: formatNumber(intDegrees, 0),
+ minutes: formatNumber(minutes, 2, 3),
+ intMinutes: formatNumber(intMinutes, 2),
+ seconds: formatNumber(seconds, 2, 2),
+ direction
+ };
+}
+
+function formatLatLng(latlng, format) {
+ return {
+ lat: format.formatter(coordinatePresentations(latlng.lat, true)),
+ lng: format.formatter(coordinatePresentations(latlng.lng, false))
+ }
+}
+
+const SIGNED_DEGREES = {
+ code: 'd',
+ label: '±ddd.ddddd',
+ wrapperClass: 'leaflet-coordinates-wrapper-signed-degrees',
+ formatter: ({signedDegrees}) => signedDegrees
+};
+
+const DEGREES = {
+ code: 'D',
+ label: 'ddd.ddddd°',
+ wrapperClass: 'leaflet-coordinates-wrapper-degrees',
+ formatter: ({degrees, direction}) => `${direction} ${degrees}°`
+};
+
+const DEGREES_AND_MINUTES = {
+ code: 'DM',
+ label: 'ddd°mm.mmm′',
+ wrapperClass: 'leaflet-coordinates-wrapper-degrees-and-minutes',
+ formatter: ({intDegrees, minutes, direction}) => `${direction} ${intDegrees}°${minutes}′`
+};
+
+const DEGREES_AND_MINUTES_AND_SECONDS = {
+ code: 'DMS',
+ label: 'ddd°mm′ss.s″',
+ wrapperClass: 'leaflet-coordinates-wrapper-degrees-and-minutes-and-seconds',
+ formatter: ({intDegrees, intMinutes, seconds, direction}) => `${direction} ${intDegrees}°${intMinutes}′${seconds}″`};
+
+export default {
+ SIGNED_DEGREES,
+ DEGREES,
+ DEGREES_AND_MINUTES,
+ DEGREES_AND_MINUTES_AND_SECONDS,
+ formatLatLng
+}
diff --git a/src/lib/leaflet.control.coordinates/index.js b/src/lib/leaflet.control.coordinates/index.js
@@ -1,141 +1,121 @@
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.format = ko.observable(DEFAULT_FORMAT);
+ this.formatCode = ko.pureComputed({
+ read: () => this.format().code,
+ write: (value) => {
+ for (let format of this.formats) {
+ if (value === format.code) {
+ this.format(format);
+ break
+ }
+ }
+ }
+ });
+
+ this.formattedCoordinates = ko.pureComputed(() => {
+ if (this.latlng()) {
+ return formats.formatLatLng(this.latlng().wrap(), this.format());
+ }
+ return UNKNOWN_COORDINATES;
+ }, this);
+
+ this.wrapperClass = ko.pureComputed(() => {
+ return this.format().wrapperClass;
+ }, 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="css: wrapperClass">
+ <div class="leaflet-coordinates-container" data-bind="with: formattedCoordinates()">
+ <span class="leaflet-coordinates-latitude" data-bind="html: lat"></span>
+ <span class="leaflet-coordinates-longitude" data-bind="html: lng"></span>
+ </div>
+ <hr class="leaflet-coordinates-divider" />
+ <div data-bind="foreach: formats">
+ <div>
+ <label title="">
+ <input type="radio" data-bind="checked: $parent.formatCode, value: code" class="leaflet-coordinates-format-radio"/>
+ <span data-bind="html: label"></span>
+ </label>
+ </div>
+ </div>
+ </div>
+ `;
+
+ barContainer.title = "Right click on map to copy coordinates";
- 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) {
@@ -150,29 +130,30 @@ L.Control.Coordinates = L.Control.extend({
this._map[eventFunc]('mousemove', this.onMouseMove, this);
this._map[eventFunc]('contextmenu', this.onMapRightClick, this);
this._isEnabled = !!enabled;
+ this.latlng(null);
},
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 menu = new Contextmenu(items);
- menu.show(e);
+
+ const createItem = (format, options = {}) => {
+ const {lat, lng} = formats.formatLatLng(e.latlng.wrap(), format);
+ 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 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 +163,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
-
}
);
-