Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: support for DMS in position control #1623

Merged
merged 3 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scss/_position.scss
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,5 @@

input.o-position-find {
padding: 0;
width: 95px;
width: auto;
}
232 changes: 167 additions & 65 deletions src/controls/position.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import MousePosition from 'ol/control/MousePosition';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import { createStringXY } from 'ol/coordinate';
import { createStringXY, toStringHDMS } from 'ol/coordinate';
import { Component, Icon, Button, Element as El, dom } from '../ui';

const Position = function Position(options = {}) {
Expand All @@ -18,10 +18,8 @@ const Position = function Position(options = {}) {
let view;
const characterError = 'Ogiltigt tecken för koordinat, vänligen försök igen.';
const extentError = 'Angivna koordinater ligger inte inom kartans utsträckning, vänligen försök igen.';
let currentProjection;
let projections;
let projectionCodes;
let projection;

/** Current Map projection code */
let mapProjection;
let precision;
let mousePositionActive;
Expand All @@ -33,6 +31,11 @@ const Position = function Position(options = {}) {
let coordsElement;
let coordsFindElement;
let containerElement;
let currentConfig;
let currentConfigIndex = 0;
let configArray = [];
let currentCaretPos;
let inputEl;

function placeholder() {
return noPositionText.length === 0 ? noPositionText === false : noPositionText;
Expand Down Expand Up @@ -71,10 +74,28 @@ const Position = function Position(options = {}) {
});
}

/**
* Returns a function that formats a coordinate to HDMS format
* @param {any} fractionDigits
*/
function createStringHDMS(fractionDigits) {
return (
(coord) => toStringHDMS(coord, fractionDigits)
);
}

/**
* Returns a funtion that formats a coordinate to a string depending on configuration
* */
function getStringifyFunction() {
return currentConfig.dms ? createStringHDMS(precision) : createStringXY(precision);
}

function addMousePosition() {
const currentProjectionCode = currentConfig.projectionCode;
mousePositionControl = new MousePosition({
coordinateFormat: createStringXY(precision),
projection: currentProjection,
coordinateFormat: getStringifyFunction(),
projection: currentProjectionCode,
target: document.getElementById(`${coordsElement.getId()}`),
placeholder: placeholder()
});
Expand All @@ -101,8 +122,12 @@ const Position = function Position(options = {}) {
document.getElementById(`${viewer.getId()}`).appendChild(markerElement);
}

/**
* Write coords to input field.
* @param {any} coords
*/
function writeCoords(coords) {
document.getElementById(`${coordsFindElement.getId()}`).value = coords;
inputEl.value = coords;
}

function transformCoords(coords, source, destination) {
Expand All @@ -112,47 +137,58 @@ const Position = function Position(options = {}) {
return geometry.transform(source, destination).getCoordinates();
}

function round(coords) {
if (precision) {
return coords.map(coord => coord.toFixed(precision));
}
return coords.map(coord => Math.round(coord));
}

/**
* Update coords in input, transforming if necessary.
* @param {any} sourceCoords
*/
function updateCoords(sourceCoords) {
let coords = sourceCoords;
if (currentProjection !== mapProjection) {
coords = transformCoords(coords, projection, currentProjection);
const currentProjectionCode = currentConfig.projectionCode;
if (currentProjectionCode !== mapProjection) {
coords = transformCoords(coords, mapProjection, currentProjectionCode);
}
coords = round(coords);
const center = coords.join(', ') + suffix;
writeCoords(center);
const formattedCoords = getStringifyFunction()(coords);
writeCoords(formattedCoords);
}

/**
* Eventhandler that is called when map pans.
* */
function onChangeCenter() {
updateCoords(view.getCenter());
}

/**
* Parses a coordinate string and transforms it to map projection if necessary and returns a valid coordinate
* @param {any} strCoords
*/
function validateCoordinate(strCoords) {
const extent = viewer.getExtent() || view.getProjection().getExtent();
let inExtent;

// validate numbers
let coords = strCoords.split(',').map(coord => parseFloat(coord))
.filter((coord) => {
if (!Number.isNaN(coord)) {
return coord;
}
return null;
});
if (coords.length !== 2) {
alert(characterError);
return [];
let coords;
if (currentConfig.dms) {
// Assume that input logic enforces a correct format
const coordArray = strCoords.match(/\d*\.?\d+/g);
const lat = parseInt(coordArray[0], 10) + parseInt(coordArray[1], 10) / 60 + parseFloat(coordArray[2]) / 3600;
const lon = parseInt(coordArray[3], 10) + parseInt(coordArray[4], 10) / 60 + parseFloat(coordArray[5]) / 3600;
coords = [lon, lat];
} else {
// validate numbers
coords = strCoords.split(',').map(coord => parseFloat(coord))
.filter((coord) => {
if (!Number.isNaN(coord)) {
return coord;
}
return null;
});
if (coords.length !== 2) {
alert(characterError);
return [];
}
}

// transform
if (currentProjection !== mapProjection) {
coords = transformCoords(coords, currentProjection, mapProjection);
if (currentConfig.projectionCode !== mapProjection) {
coords = transformCoords(coords, currentConfig.projectionCode, mapProjection);
}

// validate coords within extent
Expand All @@ -165,8 +201,9 @@ const Position = function Position(options = {}) {
return [];
}

/** Centers the map on the coordinates provided by user i textbox */
function findCoordinate() {
const coords = document.getElementById(`${coordsFindElement.getId()}`).value;
const coords = inputEl.value;
const validated = validateCoordinate(coords);
if (validated.length === 2) {
map.getView().animate({
Expand All @@ -176,11 +213,66 @@ const Position = function Position(options = {}) {
}
}

/** Eventhandler that is called when input field gets focus */
function onFindFocus() {
if (currentConfig.dms) {
// Always mark first digit to keep user out of trouble
inputEl.setSelectionRange(0, 1);
currentCaretPos = 0;
}
}

/**
* Move the selection in input field in indicated direction to next position where there is a digit
* @param {any} left True if move left, otherwise move right
*/
function moveCaret(left) {
const step = left ? -1 : 1;
if (currentCaretPos + step < 0 || currentCaretPos + step > inputEl.value.length - 4) {
return;
}
// Stop on next digit. As we already tested if we're on first or last digit, there must be at least one more digit in this direction
currentCaretPos += step;
while (!/\d/.test(inputEl.value[currentCaretPos])) {
currentCaretPos += step;
}
inputEl.setSelectionRange(currentCaretPos, currentCaretPos + 1);
}
/**
* Eventhandler called when user enters something in the coordinate textbox
* @param {any} e
*/
function onFind(e) {
if (e.which === 13) {
if (currentConfig.dms) {
if (e.which === 37) {
moveCaret(true);
} else if (e.which === 39) {
moveCaret(false);
} else if (e.which >= 48 && e.which <= 57) {
inputEl.value = inputEl.value.substring(0, currentCaretPos) + (e.which - 48) + inputEl.value.substring(currentCaretPos + 1);
moveCaret(false);
} else if (e.which === 13) {
findCoordinate();
}
// For DMS, we handle everything ourselves. Ignore all keypresses (including current key) in order to keep browser from interfering
e.preventDefault();
} else if (e.which === 13) {
findCoordinate();
}
}

/**
* Eventhandler that is called when users clicks input field. Is only called if input already has focus.
* @param {any} e
*/
function onFindClick() {
if (currentConfig.dms) {
// Select first digit in order to keep user out of trouble by accidently clicking a non-digit and mess up the string
currentCaretPos = 0;
inputEl.setSelectionRange(currentCaretPos, currentCaretPos + 1);
}
}

function addCenterPosition() {
renderMarker();

Expand All @@ -190,7 +282,9 @@ const Position = function Position(options = {}) {
updateCoords(view.getCenter());
view.on('change:center', onChangeCenter);

document.getElementById(`${coordsFindElement.getId()}`).addEventListener('keypress', onFind);
inputEl.addEventListener('keydown', onFind);
inputEl.addEventListener('focus', onFindFocus);
inputEl.addEventListener('click', onFindClick);
}

function clear() {
Expand All @@ -200,12 +294,14 @@ const Position = function Position(options = {}) {
function removeCenterPosition() {
view.un('change:center', onChangeCenter);
clear();
document.getElementById(`${coordsFindElement.getId()}`).removeEventListener('keypress', onFind);
inputEl.removeEventListener('keydown', onFind);
inputEl.removeEventListener('focus', onFindFocus);
inputEl.removeEventListener('click', onFindClick);

const markerIconElement = document.getElementById(`${markerIcon.getId()}`);
markerIconElement.parentNode.removeChild(markerIconElement);
document.getElementById(`${centerButton.getId()}`).classList.remove('o-active');
document.getElementById(`${coordsFindElement.getId()}`).classList.remove('o-active');
inputEl.classList.remove('o-active');
}

function onTogglePosition() {
Expand All @@ -219,35 +315,37 @@ const Position = function Position(options = {}) {
}
}

function toggleProjectionVal(val) {
let proj;
const index = projectionCodes.indexOf(val);
if (index === projectionCodes.length - 1) {
proj = projectionCodes[0];
} else if (index < projectionCodes.length - 1) {
proj = projectionCodes[index + 1];
function toggleProjectionVal() {
currentConfigIndex += 1;
if (currentConfigIndex === configArray.length) {
currentConfigIndex = 0;
}
return proj;
currentConfig = configArray[currentConfigIndex];
}

function setPrecision() {
if (currentProjection === 'EPSG:4326') {
if (currentConfig.precision) {
precision = currentConfig.precision;
} else if (currentConfig.projectionCode === 'EPSG:4326' && !currentConfig.dms) {
precision = 5;
} else {
precision = 0;
}
const exampleCoord = getStringifyFunction()(view.getCenter());
inputEl.setAttribute('size', exampleCoord.length);
}

function writeProjection() {
document.getElementById(`${projButton.getId()}`).value = currentProjection;
document.getElementById(`${projButton.getId()}`).textContent = projections[currentProjection];
document.getElementById(`${projButton.getId()}`).value = currentConfig.projectionCode;
document.getElementById(`${projButton.getId()}`).textContent = currentConfig.projectionLabel;
}

/** Eventhandler that is called when user clicks toggle button */
function onToggleProjection() {
removeNoCoordsEl();
currentProjection = toggleProjectionVal(document.getElementById(`${projButton.getId()}`).value);
toggleProjectionVal();
setPrecision();
writeProjection(currentProjection);
writeProjection();
if (mousePositionActive) {
removeMousePosition();
addMousePosition();
Expand All @@ -264,20 +362,24 @@ const Position = function Position(options = {}) {
viewer = evt.target;
map = viewer.getMap();
view = map.getView();
projection = view.getProjection();
mapProjection = viewer.getProjectionCode();
projections = options.projections || {};
projectionCodes = Object.getOwnPropertyNames(projections);
if (title) {
currentProjection = mapProjection;
projections[currentProjection] = title;
projectionCodes.unshift(mapProjection);
} else if (projectionCodes.length) {
currentProjection = projectionCodes[0];
// For backwards compatibility, we also accept an object with epsg codes as keys
// New config format must be an array, as same epsg code can be used several times
if (options.projections instanceof Array) {
configArray = options.projections;
} else {
alert('No title or projection is set for position');
Object.keys(options.projections).forEach(currKey => configArray.push({ projectionCode: currKey, projectionLabel: options.projections[currKey] }));
}

// If title is set, add the map projection as first and active setting.
if (title) {
configArray.unshift({ projectionCode: mapProjection, projectionLabel: title });
}

if (configArray.length === 0) {
alert('No title or projection is set for position');
}
currentConfig = configArray[0];
if (!suffix) suffix = '';
if (!title) title = undefined;

Expand Down Expand Up @@ -319,9 +421,9 @@ const Position = function Position(options = {}) {
render() {
const el = dom.html(containerElement.render());
document.getElementById(viewer.getFooter().getId()).firstElementChild.appendChild(el);
inputEl = document.getElementById(`${coordsFindElement.getId()}`);

document.getElementById(`${projButton.getId()}`).value = currentProjection;
document.getElementById(`${projButton.getId()}`).textContent = projections[currentProjection];
writeProjection();

setPrecision();
addMousePosition();
Expand Down