From 9e63e40dab12874585a82445e3abf63dac6308b1 Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 10 Jan 2019 15:24:29 +0000 Subject: [PATCH 01/15] Add new MGRS module and update webpack-dev-server --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 48751e21..92fda38b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "url-loader": "^1.1.2", "web-resource-inliner": "^4.2.1", "webpack": "^4.25.1", - "webpack-dev-server": "^3.1.10", + "webpack-dev-server": "^3.1.14", "webpack-node-externals": "^1.7.2", "worker-loader": "^2.0.0" }, @@ -104,6 +104,7 @@ "lodash": "^4.17.11", "loglevel": "^1.6.1", "loglevel-message-prefix": "^3.0.0", + "mgrs": "^1.0.0", "moment": "^2.22.2", "moment-timezone": "^0.5.23", "ngeohash": "^0.6.0", From abdd70c6fa5f15dd53a6dfff9e5d379ef421d49e Mon Sep 17 00:00:00 2001 From: j433866 Date: Fri, 11 Jan 2019 11:59:13 +0000 Subject: [PATCH 02/15] Add ConvertCoordinates to lib folder --- src/core/lib/ConvertCoordinates.mjs | 210 ++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 src/core/lib/ConvertCoordinates.mjs diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs new file mode 100644 index 00000000..45ab0690 --- /dev/null +++ b/src/core/lib/ConvertCoordinates.mjs @@ -0,0 +1,210 @@ +/** + * Co-ordinate conversion resources. + * + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import geohash from "ngeohash"; +import mgrs from "mgrs"; + +/** + * Co-ordinate formats + */ +export const FORMATS = [ + "Degrees Minutes Seconds", + "Degrees Decimal Minutes", + "Decimal Degrees", + "Geohash", + "Military Grid Reference System" +]; + +/** + * Convert a given latitude and longitude into a different format. + * @param {string} inLat - Input latitude to be converted. Use this for supplying single values for conversion (e.g. geohash) + * @param {string} inLong - Input longitude to be converted + * @param {string} inFormat - Format of the input coordinates + * @param {string} outFormat - Format to convert to + * @param {number} precision - Precision of the result + * @returns {string[]} Array containing the converted latitude and longitude + */ +export function convertCoordinates (inLat, inLong, inFormat, outFormat, precision) { + let convLat = inLat; + let convLong = inLong; + if (inFormat === "Geohash") { + const hash = geohash.decode(inLat); + convLat = hash.latitude.toString(); + convLong = hash.longitude.toString(); + } else if (inFormat === "Military Grid Reference System") { + const result = mgrs.toPoint(inLat.replace(" ", "")); + convLat = result[1]; + convLong = result[0]; + } else { + convLat = convertSingleCoordinate(inLat, inFormat, "Decimal Degrees", 15).split("°"); + convLong = convertSingleCoordinate(inLong, inFormat, "Decimal Degrees", 15).split("°"); + } + + if (outFormat === "Geohash") { + convLat = geohash.encode(parseFloat(convLat), parseFloat(convLong), precision); + } else if (outFormat === "Military Grid Reference System") { + convLat = mgrs.forward([parseFloat(convLong), parseFloat(convLat)], precision); + } else { + convLat = convertSingleCoordinate(convLat.toString(), "Decimal Degrees", outFormat, precision); + convLong = convertSingleCoordinate(convLong.toString(), "Decimal Degrees", outFormat, precision); + } + + return [convLat, convLong]; +} + +/** + * @param {string} input - The input co-ordinate to be converted + * @param {string} inFormat - The format of the input co-ordinates + * @param {string} outFormat - The format which input should be converted to + * @param {boolean} returnRaw - When true, returns the raw float instead of a String + * @returns {string|{Object}} The converted co-ordinate result, as either the raw object or a formatted string + */ +export function convertSingleCoordinate (input, inFormat, outFormat, precision, returnRaw = false){ + let converted; + precision = Math.pow(10, precision); + const convData = splitInput(input); + // Convert everything to decimal degrees first + switch (inFormat) { + case "Degrees Minutes Seconds": + if (convData.length < 3) { + throw "Invalid co-ordinates format."; + } + converted = convDMSToDD(convData[0], convData[1], convData[2], precision); + break; + case "Degrees Decimal Minutes": + if (convData.length < 2) { + throw "Invalid co-ordinates format."; + } + converted = convDDMToDD(convData[0], convData[1], precision); + break; + case "Decimal Degrees": + if (convData.length < 1) { + throw "Invalid co-ordinates format."; + } + converted = convDDToDD(convData[0], precision); + break; + default: + throw "Unknown input format selection."; + } + + // Convert from decimal degrees to the output format + switch (outFormat) { + case "Decimal Degrees": + break; + case "Degrees Minutes Seconds": + converted = convDDToDMS(converted.degrees); + break; + case "Degrees Decimal Minutes": + converted = convDDToDDM(converted.degrees, precision); + break; + default: + throw "Unknown output format selection."; + } + if (returnRaw) { + return converted; + } else { + return converted.string; + } +} + +/** + * Split up the input using a space, and sanitise the result + * @param {string} input - The input data to be split + * @returns {number[]} An array of the different items in the string, stored as floats + */ +function splitInput (input){ + const split = []; + + input.split(" ").forEach(item => { + // Remove any character that isn't a digit + item = item.replace(/[^0-9.-]/g, ""); + if (item.length > 0){ + split.push(parseFloat(item, 10)); + } + }); + return split; +} + +/** + * Convert Degrees Minutes Seconds to Decimal Degrees + * @param {number} degrees - The degrees of the input co-ordinates + * @param {number} minutes - The minutes of the input co-ordinates + * @param {number} seconds - The seconds of the input co-ordinates + * @param {number} precision - The precision the result should be rounded to + * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string) + */ +function convDMSToDD (degrees, minutes, seconds, precision){ + const converted = new Object(); + converted.degrees = degrees + (minutes / 60) + (seconds / 3600); + converted.string = (Math.round(converted.degrees * precision) / precision) + "°"; + return converted; +} + +/** + * Convert Decimal Degrees Minutes to Decimal Degrees + * @param {number} degrees - The input degrees to be converted + * @param {number} minutes - The input minutes to be converted + * @param {number} precision - The precision which the result should be rounded to + * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string) + */ +function convDDMToDD (degrees, minutes, precision) { + const converted = new Object(); + converted.degrees = degrees + minutes / 60; + converted.string = ((Math.round(converted.degrees * precision) / precision) + "°"); + return converted; +} + +/** + * Convert Decimal Degrees to Decimal Degrees + * Doesn't affect the input, just puts it into an object + * @param {number} degrees - The input degrees to be converted + * @param {number} precision - The precision which the result should be rounded to + * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string) + */ +function convDDToDD (degrees, precision) { + const converted = new Object(); + converted.degrees = degrees; + converted.string = Math.round(converted.degrees * precision) / precision + "°"; + return converted; +} + +/** + * Convert Decimal Degrees to Degrees Minutes Seconds + * @param {number} decDegrees - The input data to be converted + * @returns {{string: string, degrees: number, minutes: number, seconds: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes, .seconds), and a formatted string version (obj.string) + */ +function convDDToDMS (decDegrees) { + const degrees = Math.floor(decDegrees); + const minutes = Math.floor(60 * (decDegrees - degrees)); + const seconds = Math.round(3600 * (decDegrees - degrees) - 60 * minutes); + + const converted = new Object(); + converted.degrees = degrees; + converted.minutes = minutes; + converted.seconds = seconds; + converted.string = degrees + "° " + minutes + "' " + seconds + "\""; + return converted; +} + +/** + * Convert Decimal Degrees to Degrees Decimal Minutes + * @param {number} decDegrees - The input degrees to be converted + * @param {number} precision - The precision the input data should be rounded to + * @returns {{string: string, degrees: number, minutes: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes), and a formatted string version (obj.string) + */ +function convDDToDDM (decDegrees, precision) { + const degrees = Math.floor(decDegrees); + const minutes = decDegrees - degrees; + const decMinutes = Math.round((minutes * 60) * precision) / precision; + + const converted = new Object(); + converted.degrees = degrees; + converted.minutes = decMinutes; + converted.string = degrees + "° " + decMinutes + "'"; + return converted; +} From 68fbbb64dba87ef1d7edf2138870e8b98aa3e9d8 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 14 Jan 2019 11:49:57 +0000 Subject: [PATCH 03/15] Add new Convert co-ordinate format module. Also added autodetect of co-ordinate format / delimiter --- src/core/config/Categories.json | 1 + src/core/lib/ConvertCoordinates.mjs | 95 ++++++++ .../operations/ConvertCoordinateFormat.mjs | 227 ++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 src/core/operations/ConvertCoordinateFormat.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index e9fe3399..13ea76c9 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -210,6 +210,7 @@ "Convert mass", "Convert speed", "Convert data units", + "Convert co-ordinate format", "Parse UNIX file permissions", "Swap endianness", "Parse colour code", diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index 45ab0690..a9b3d551 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -208,3 +208,98 @@ function convDDToDDM (decDegrees, precision) { converted.string = degrees + "° " + decMinutes + "'"; return converted; } + +/** + * + * @param {string} input - The input data whose format we need to detect + * @param {string} delim - The delimiter separating the data in input + * @returns {string} The input format + */ +export function findFormat (input, delim) { + input = input.trim(); + let testData; + if (delim.includes("Direction")) { + const split = input.split(/[NnEeSsWw]/); + if (split.length > 0) { + if (split[0] === "") { + // Direction Preceding + testData = split[1]; + } else { + // Direction Following + testData = split[0]; + } + } + } else if (delim !== "") { + const split = input.split(delim); + if (!input.includes(delim)) { + testData = input; + } + if (split.length > 0) { + if (split[0] !== "") { + testData = split[0]; + } else if (split.length > 1) { + testData = split[1]; + } + } + } + + // Test MGRS and Geohash + if (input.split(" ").length === 1) { + const mgrsPattern = new RegExp(/^[0-9]{2}[C-HJ-NP-X]{2}[A-Z]+/); + const geohashPattern = new RegExp(/^[0123456789bcdefghjkmnpqrstuvwxyz]+$/); + if (mgrsPattern.test(input.toUpperCase())) { + return "Military Grid Reference System"; + } else if (geohashPattern.test(input.toLowerCase())) { + return "Geohash"; + } + } + + // Test DMS/DDM/DD formats + if (testData !== undefined) { + const split = splitInput(testData); + if (split.length === 3) { + // DMS + return "Degrees Minutes Seconds"; + } else if (split.length === 2) { + // DDM + return "Degrees Decimal Minutes"; + } else if (split.length === 1) { + return "Decimal Degrees"; + } + } + return null; +} + +/** + * Automatically find the delimeter type from the given input + * @param {string} input + * @returns {string} Delimiter type + */ +export function findDelim (input) { + input = input.trim(); + const delims = [",", ";", ":"]; + // Direction + const testDir = input.match(/[NnEeSsWw]/g); + if (testDir !== null && testDir.length > 0 && testDir.length < 3) { + // Possible direction + const splitInput = input.split(/[NnEeSsWw]/); + if (splitInput.length <= 3 && splitInput.length > 0) { + // One of the splits should be an empty string + if (splitInput[0] === "") { + return "Direction Preceding"; + } else if (splitInput[splitInput.length - 1] === "") { + return "Direction Following"; + } + } + } + for (let i = 0; i < delims.length; i++) { + const delim = delims[i]; + if (input.includes(delim)) { + const splitInput = input.split(delim); + if (splitInput.length <= 3 && splitInput.length > 0) { + return delim; + } + } + } + return null; +} diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs new file mode 100644 index 00000000..5f336630 --- /dev/null +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -0,0 +1,227 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import {FORMATS, convertCoordinates, convertSingleCoordinate, findDelim, findFormat} from "../lib/ConvertCoordinates"; +import Utils from "../Utils"; + +/** + * Convert co-ordinate format operation + */ +class ConvertCoordinateFormat extends Operation { + + /** + * ConvertCoordinateFormat constructor + */ + constructor() { + super(); + + this.name = "Convert co-ordinate format"; + this.module = "Hashing"; + this.description = "Convert geographical coordinates between different formats.

Currently supported formats:
  • Degrees Minutes Seconds (DMS)
  • Degrees Decimal Minutes (DDM)
  • Decimal Degrees (DD)
  • Geohash
  • Military Grid Reference System (MGRS)
"; + this.infoURL = "https://wikipedia.org/wiki/Geographic_coordinate_conversion"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Input Format", + "type": "option", + "value": ["Auto"].concat(FORMATS) + }, + { + "name": "Input Delimiter", + "type": "option", + "value": [ + "Auto", + "Direction Preceding", // Need better names + "Direction Following", + "\\n", + "Comma", + "Semi-colon", + "Colon" + ] + }, + { + "name": "Output Format", + "type": "option", + "value": FORMATS + }, + { + "name": "Output Delimiter", + "type": "option", + "value": [ + "Space", + "Direction Preceding", // Need better names + "Direction Following", + "\\n", + "Comma", + "Semi-colon", + "Colon" + ] + }, + { + "name": "Precision", + "type": "number", + "value": 3 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const outFormat = args[2], + outDelim = args[3], + precision = args[4]; + let inFormat = args[0], + inDelim = args[1], + inLat, + inLong, + outLat, + outLong, + latDir = "", + longDir = "", + outSeparator = " "; + + // Autodetect input delimiter + if (inDelim === "Auto") { + inDelim = findDelim(input); + log.error("DATA: " + input + " DELIM: " + inDelim); + if (inDelim === null) { + inDelim = ""; + // throw new OperationError("Could not automatically detect the input delimiter."); + } + } else if (!inDelim.includes("Direction")) { + // Get the actual delimiter from the regex + inDelim = String(Utils.regexRep(inDelim)).slice(1, 2); + } + if (inFormat === "Auto") { + inFormat = findFormat(input, inDelim); + log.error("DATA: " + input + " FORMAT: " + inFormat); + if (inFormat === null) { + throw new OperationError("Could not automatically detect the input"); + } + } + + if (inDelim === "" && (inFormat !== "Geohash" && inFormat !== "Military Grid Reference System")) { + throw new OperationError("Could not automatically detect the input delimiter."); + } + + // Prepare input data + if (inFormat === "Geohash" || inFormat === "Military Grid Reference System") { + // Geohash only has one value, so just use the input + inLat = input; + } else if (inDelim === "Direction Preceding") { + // Split on the compass directions + const splitInput = input.split(/[NnEeSsWw]/); + const dir = input.match(/[NnEeSsWw]/g); + if (splitInput.length > 1) { + inLat = splitInput[1]; + if (dir !== null) { + latDir = dir[0]; + } + if (splitInput.length > 2) { + inLong = splitInput[2]; + if (dir !== null && dir.length > 1) { + longDir = dir[1]; + } + } + } + } else if (inDelim === "Direction Following") { + // Split on the compass directions + const splitInput = input.split(/[NnEeSsWw]/); + if (splitInput.length >= 1) { + inLat = splitInput[0]; + if (splitInput.length >= 2) { + inLong = splitInput[1]; + } + } + } else { + // Split on the delimiter + const splitInput = input.split(inDelim); + log.error(splitInput); + if (splitInput.length > 0) { + inLat = splitInput[0]; + if (splitInput.length >= 2) { + inLong = splitInput[1]; + } + } + } + + if (inFormat !== "Geohash" && inFormat !== "Military Grid Reference System" && outDelim.includes("Direction")) { + // Match on compass directions, and store the first 2 matches for the output + const dir = input.match(/[NnEeSsWw]/g); + if (dir !== null) { + latDir = dir[0]; + if (dir.length > 1) { + longDir = dir[1]; + } + } + } else if (outDelim === "\\n") { + outSeparator = "\n"; + } else if (outDelim === "Space") { + outSeparator = " "; + } else if (!outDelim.includes("Direction")) { + // Cut out the regex syntax (/) from the delimiter + outSeparator = String(Utils.regexRep(outDelim)).slice(1, 2); + } + + // Convert the co-ordinates + if (inLat !== undefined) { + if (inLong === undefined) { + if (inFormat !== "Geohash" && inFormat !== "Military Grid Reference System") { + if (outFormat === "Geohash" || outFormat === "Military Grid Reference System"){ + throw new OperationError(`${outFormat} needs both a latitude and a longitude to be calculated`); + } + } + if (inFormat === "Geohash" || inFormat === "Military Grid Reference System") { + // Geohash conversion is in convertCoordinates despite needing + // only one input as it needs to output two values + [outLat, outLong] = convertCoordinates(inLat, inLat, inFormat, outFormat, precision); + } else { + outLat = convertSingleCoordinate(inLat, inFormat, outFormat, precision); + } + } else { + [outLat, outLong] = convertCoordinates(inLat, inLong, inFormat, outFormat, precision); + } + } else { + throw new OperationError("No co-ordinates were detected in the input."); + } + + // Output conversion results if successful + if (outLat !== undefined) { + let output = ""; + if (outDelim === "Direction Preceding" && outFormat !== "Geohash" && outFormat !== "Military Grid Reference System") { + output += latDir += " "; + } + output += outLat; + if (outDelim === "Direction Following" && outFormat !== "Geohash" && outFormat !== "Military Grid Reference System") { + output += " " + latDir; + } + output += outSeparator; + + if (outLong !== undefined && outFormat !== "Geohash" && outFormat !== "Military Grid Reference System") { + if (outDelim === "Direction Preceding") { + output += longDir + " "; + } + output += outLong; + if (outDelim === "Direction Following") { + output += " " + longDir; + } + output += outSeparator; + } + return output; + } else { + throw new OperationError("Co-ordinate conversion failed."); + } + } +} + +export default ConvertCoordinateFormat; From 8d1f668fc550f5795a02f5f157411ffe45da89e0 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 14 Jan 2019 11:56:27 +0000 Subject: [PATCH 04/15] Remove old Geohash modules --- src/core/config/Categories.json | 4 +-- src/core/operations/FromGeohash.mjs | 44 ------------------------ src/core/operations/ToGeohash.mjs | 53 ----------------------------- 3 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 src/core/operations/FromGeohash.mjs delete mode 100644 src/core/operations/ToGeohash.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 13ea76c9..2224050c 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -302,9 +302,7 @@ "Adler-32 Checksum", "CRC-16 Checksum", "CRC-32 Checksum", - "TCP/IP Checksum", - "To Geohash", - "From Geohash" + "TCP/IP Checksum" ] }, { diff --git a/src/core/operations/FromGeohash.mjs b/src/core/operations/FromGeohash.mjs deleted file mode 100644 index b70273da..00000000 --- a/src/core/operations/FromGeohash.mjs +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @author gchq77703 [] - * @copyright Crown Copyright 2018 - * @license Apache-2.0 - */ - -import Operation from "../Operation"; -import geohash from "ngeohash"; - -/** - * From Geohash operation - */ -class FromGeohash extends Operation { - - /** - * FromGeohash constructor - */ - constructor() { - super(); - - this.name = "From Geohash"; - this.module = "Hashing"; - this.description = "Converts Geohash strings into Lat/Long coordinates. For example, ww8p1r4t8 becomes 37.8324,112.5584."; - this.infoURL = "https://wikipedia.org/wiki/Geohash"; - this.inputType = "string"; - this.outputType = "string"; - this.args = []; - } - - /** - * @param {string} input - * @param {Object[]} args - * @returns {string} - */ - run(input, args) { - return input.split("\n").map(line => { - const coords = geohash.decode(line); - return [coords.latitude, coords.longitude].join(","); - }).join("\n"); - } - -} - -export default FromGeohash; diff --git a/src/core/operations/ToGeohash.mjs b/src/core/operations/ToGeohash.mjs deleted file mode 100644 index 0e7f53ac..00000000 --- a/src/core/operations/ToGeohash.mjs +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @author gchq77703 [] - * @copyright Crown Copyright 2018 - * @license Apache-2.0 - */ - -import Operation from "../Operation"; -import geohash from "ngeohash"; - -/** - * To Geohash operation - */ -class ToGeohash extends Operation { - - /** - * ToGeohash constructor - */ - constructor() { - super(); - - this.name = "To Geohash"; - this.module = "Hashing"; - this.description = "Converts Lat/Long coordinates into a Geohash string. For example, 37.8324,112.5584 becomes ww8p1r4t8."; - this.infoURL = "https://wikipedia.org/wiki/Geohash"; - this.inputType = "string"; - this.outputType = "string"; - this.args = [ - { - name: "Precision", - type: "number", - value: 9 - } - ]; - } - - /** - * @param {string} input - * @param {Object[]} args - * @returns {string} - */ - run(input, args) { - const [precision] = args; - - return input.split("\n").map(line => { - line = line.replace(/ /g, ""); - if (line === "") return ""; - return geohash.encode(...line.split(",").map(num => parseFloat(num)), precision); - }).join("\n"); - } - -} - -export default ToGeohash; From 8b77ad77480b7cf1450e32d2c63dc17f0a6f46ee Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 14 Jan 2019 12:49:28 +0000 Subject: [PATCH 05/15] Stop delimiters breaking MGRS conversion --- src/core/operations/ConvertCoordinateFormat.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs index 5f336630..d24b2135 100644 --- a/src/core/operations/ConvertCoordinateFormat.mjs +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -117,7 +117,8 @@ class ConvertCoordinateFormat extends Operation { // Prepare input data if (inFormat === "Geohash" || inFormat === "Military Grid Reference System") { // Geohash only has one value, so just use the input - inLat = input; + // Replace anything that isn't a valid character in Geohash / MGRS + inLat = input.replace(/[^A-Za-z0-9]/, ""); } else if (inDelim === "Direction Preceding") { // Split on the compass directions const splitInput = input.split(/[NnEeSsWw]/); From 1a88a0164cf8d27d260afc9e8b8b72d5db39fe3a Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 14 Jan 2019 13:00:14 +0000 Subject: [PATCH 06/15] Fix delimiter breaking Geohash detection --- src/core/lib/ConvertCoordinates.mjs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index a9b3d551..9b9d8881 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -45,6 +45,7 @@ export function convertCoordinates (inLat, inLong, inFormat, outFormat, precisio convLong = convertSingleCoordinate(inLong, inFormat, "Decimal Degrees", 15).split("°"); } + // Convert Geohash and MGRS here, as they need both the lat and long values if (outFormat === "Geohash") { convLat = geohash.encode(parseFloat(convLat), parseFloat(convLong), precision); } else if (outFormat === "Military Grid Reference System") { @@ -244,12 +245,14 @@ export function findFormat (input, delim) { } // Test MGRS and Geohash - if (input.split(" ").length === 1) { + if (input.split(" ").length <= 1) { + const filteredInput = input.replace(/[^A-Za-z0-9]/, "").toUpperCase(); const mgrsPattern = new RegExp(/^[0-9]{2}[C-HJ-NP-X]{2}[A-Z]+/); - const geohashPattern = new RegExp(/^[0123456789bcdefghjkmnpqrstuvwxyz]+$/); - if (mgrsPattern.test(input.toUpperCase())) { + const geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/); + log.error(filteredInput); + if (mgrsPattern.test(filteredInput)) { return "Military Grid Reference System"; - } else if (geohashPattern.test(input.toLowerCase())) { + } else if (geohashPattern.test(filteredInput)) { return "Geohash"; } } @@ -292,6 +295,8 @@ export function findDelim (input) { } } } + + // Loop through the standard delimiters, and try to find them in the input for (let i = 0; i < delims.length; i++) { const delim = delims[i]; if (input.includes(delim)) { From b3ac8d0835e90c9301cfd86f58d2f619a1d246c6 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 14 Jan 2019 13:49:49 +0000 Subject: [PATCH 07/15] Removed some debug logging --- src/core/lib/ConvertCoordinates.mjs | 1 - src/core/operations/ConvertCoordinateFormat.mjs | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index 9b9d8881..0078cfb7 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -249,7 +249,6 @@ export function findFormat (input, delim) { const filteredInput = input.replace(/[^A-Za-z0-9]/, "").toUpperCase(); const mgrsPattern = new RegExp(/^[0-9]{2}[C-HJ-NP-X]{2}[A-Z]+/); const geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/); - log.error(filteredInput); if (mgrsPattern.test(filteredInput)) { return "Military Grid Reference System"; } else if (geohashPattern.test(filteredInput)) { diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs index d24b2135..7528fccc 100644 --- a/src/core/operations/ConvertCoordinateFormat.mjs +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -22,7 +22,7 @@ class ConvertCoordinateFormat extends Operation { this.name = "Convert co-ordinate format"; this.module = "Hashing"; - this.description = "Convert geographical coordinates between different formats.

Currently supported formats:
  • Degrees Minutes Seconds (DMS)
  • Degrees Decimal Minutes (DDM)
  • Decimal Degrees (DD)
  • Geohash
  • Military Grid Reference System (MGRS)
"; + this.description = "Convert geographical coordinates between different formats.

Supported formats:
  • Degrees Minutes Seconds (DMS)
  • Degrees Decimal Minutes (DDM)
  • Decimal Degrees (DD)
  • Geohash
  • Military Grid Reference System (MGRS)
"; this.infoURL = "https://wikipedia.org/wiki/Geographic_coordinate_conversion"; this.inputType = "string"; this.outputType = "string"; @@ -93,10 +93,8 @@ class ConvertCoordinateFormat extends Operation { // Autodetect input delimiter if (inDelim === "Auto") { inDelim = findDelim(input); - log.error("DATA: " + input + " DELIM: " + inDelim); if (inDelim === null) { inDelim = ""; - // throw new OperationError("Could not automatically detect the input delimiter."); } } else if (!inDelim.includes("Direction")) { // Get the actual delimiter from the regex @@ -104,9 +102,8 @@ class ConvertCoordinateFormat extends Operation { } if (inFormat === "Auto") { inFormat = findFormat(input, inDelim); - log.error("DATA: " + input + " FORMAT: " + inFormat); if (inFormat === null) { - throw new OperationError("Could not automatically detect the input"); + throw new OperationError("Could not automatically detect the input format."); } } @@ -147,7 +144,6 @@ class ConvertCoordinateFormat extends Operation { } else { // Split on the delimiter const splitInput = input.split(inDelim); - log.error(splitInput); if (splitInput.length > 0) { inLat = splitInput[0]; if (splitInput.length >= 2) { From 04b0b8c72344aa136dd2cd879fc99f2ffb678ec5 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 14 Jan 2019 14:58:41 +0000 Subject: [PATCH 08/15] Tidy up code --- src/core/lib/ConvertCoordinates.mjs | 21 +++++++++---------- .../operations/ConvertCoordinateFormat.mjs | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index 0078cfb7..1c6ae7e2 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -259,14 +259,13 @@ export function findFormat (input, delim) { // Test DMS/DDM/DD formats if (testData !== undefined) { const split = splitInput(testData); - if (split.length === 3) { - // DMS - return "Degrees Minutes Seconds"; - } else if (split.length === 2) { - // DDM - return "Degrees Decimal Minutes"; - } else if (split.length === 1) { - return "Decimal Degrees"; + switch (split.length){ + case 3: + return "Degrees Minutes Seconds"; + case 2: + return "Degrees Decimal Minutes"; + case 1: + return "Decimal Degrees"; } } return null; @@ -280,13 +279,12 @@ export function findFormat (input, delim) { export function findDelim (input) { input = input.trim(); const delims = [",", ";", ":"]; - // Direction const testDir = input.match(/[NnEeSsWw]/g); if (testDir !== null && testDir.length > 0 && testDir.length < 3) { - // Possible direction + // Possibly contains a direction const splitInput = input.split(/[NnEeSsWw]/); if (splitInput.length <= 3 && splitInput.length > 0) { - // One of the splits should be an empty string + // If there's 3 splits (one should be empty), then assume we have directions if (splitInput[0] === "") { return "Direction Preceding"; } else if (splitInput[splitInput.length - 1] === "") { @@ -301,6 +299,7 @@ export function findDelim (input) { if (input.includes(delim)) { const splitInput = input.split(delim); if (splitInput.length <= 3 && splitInput.length > 0) { + // Don't want to try and convert more than 2 co-ordinates return delim; } } diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs index 7528fccc..b9a9766c 100644 --- a/src/core/operations/ConvertCoordinateFormat.mjs +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -37,7 +37,7 @@ class ConvertCoordinateFormat extends Operation { "type": "option", "value": [ "Auto", - "Direction Preceding", // Need better names + "Direction Preceding", "Direction Following", "\\n", "Comma", @@ -55,7 +55,7 @@ class ConvertCoordinateFormat extends Operation { "type": "option", "value": [ "Space", - "Direction Preceding", // Need better names + "Direction Preceding", "Direction Following", "\\n", "Comma", From ee360521bb757c9cae9010b13db0932a46aa1e02 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 14 Jan 2019 16:41:06 +0000 Subject: [PATCH 09/15] Remove MGRS npm module --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 92fda38b..414e4d50 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,6 @@ "lodash": "^4.17.11", "loglevel": "^1.6.1", "loglevel-message-prefix": "^3.0.0", - "mgrs": "^1.0.0", "moment": "^2.22.2", "moment-timezone": "^0.5.23", "ngeohash": "^0.6.0", From ad4451a757f0650ee492c41a89014b36ead8993b Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 15 Jan 2019 10:13:11 +0000 Subject: [PATCH 10/15] Rewrite MGRS to use new Geodesy module. Added Ordnance Survey grid reference support --- package.json | 1 + src/core/lib/ConvertCoordinates.mjs | 43 ++++++++++++++++--- .../operations/ConvertCoordinateFormat.mjs | 25 +++++------ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 414e4d50..4e38e4fa 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "esprima": "^4.0.1", "exif-parser": "^0.1.12", "file-saver": "^2.0.0-rc.4", + "geodesy": "^1.1.3", "highlight.js": "^9.13.1", "jquery": "^3.3.1", "js-crc": "^0.2.0", diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index 1c6ae7e2..06e40f02 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -7,7 +7,7 @@ */ import geohash from "ngeohash"; -import mgrs from "mgrs"; +import geodesy from "geodesy"; /** * Co-ordinate formats @@ -17,7 +17,19 @@ export const FORMATS = [ "Degrees Decimal Minutes", "Decimal Degrees", "Geohash", - "Military Grid Reference System" + "Military Grid Reference System", + "Ordnance Survey National Grid" +]; + +/** + * Formats that are made up of one string + * These formats skip bits like filtering delimiters and + * are outputted differently (only one output) + */ +export const STRING_FORMATS = [ + "Geohash", + "Military Grid Reference System", + "Ordnance Survey National Grid" ]; /** @@ -37,9 +49,22 @@ export function convertCoordinates (inLat, inLong, inFormat, outFormat, precisio convLat = hash.latitude.toString(); convLong = hash.longitude.toString(); } else if (inFormat === "Military Grid Reference System") { - const result = mgrs.toPoint(inLat.replace(" ", "")); - convLat = result[1]; - convLong = result[0]; + const utm = geodesy.Mgrs.parse(inLat).toUtm(); + const result = utm.toLatLonE().toString("d", 4).replace(/[^0-9.,]/g, ""); + const splitResult = result.split(","); + if (splitResult.length === 2) { + convLat = splitResult[0]; + convLong = splitResult[1]; + } + } else if (inFormat === "Ordnance Survey National Grid") { + const osng = geodesy.OsGridRef.parse(inLat); + const latlon = geodesy.OsGridRef.osGridToLatLon(osng, geodesy.LatLonEllipsoidal.datum.WGS84); + const result = latlon.toString("d", 4).replace(/[^0-9.,]/g, ""); + const splitResult = result.split(","); + if (splitResult.length === 2) { + convLat = splitResult[0]; + convLong = splitResult[1]; + } } else { convLat = convertSingleCoordinate(inLat, inFormat, "Decimal Degrees", 15).split("°"); convLong = convertSingleCoordinate(inLong, inFormat, "Decimal Degrees", 15).split("°"); @@ -49,7 +74,13 @@ export function convertCoordinates (inLat, inLong, inFormat, outFormat, precisio if (outFormat === "Geohash") { convLat = geohash.encode(parseFloat(convLat), parseFloat(convLong), precision); } else if (outFormat === "Military Grid Reference System") { - convLat = mgrs.forward([parseFloat(convLong), parseFloat(convLat)], precision); + const utm = new geodesy.LatLonEllipsoidal(parseFloat(convLat), parseFloat(convLong)).toUtm(); + const mgrs = utm.toMgrs(); + convLat = mgrs.toString(); + } else if (outFormat === "Ordnance Survey National Grid") { + const latlon = new geodesy.LatLonEllipsoidal(parseFloat(convLat), parseFloat(convLong)); + const osng = geodesy.OsGridRef.latLonToOsGrid(latlon); + convLat = osng.toString(); } else { convLat = convertSingleCoordinate(convLat.toString(), "Decimal Degrees", outFormat, precision); convLong = convertSingleCoordinate(convLong.toString(), "Decimal Degrees", outFormat, precision); diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs index b9a9766c..afc95982 100644 --- a/src/core/operations/ConvertCoordinateFormat.mjs +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -6,7 +6,7 @@ import Operation from "../Operation"; import OperationError from "../errors/OperationError"; -import {FORMATS, convertCoordinates, convertSingleCoordinate, findDelim, findFormat} from "../lib/ConvertCoordinates"; +import {FORMATS, STRING_FORMATS, convertCoordinates, convertSingleCoordinate, findDelim, findFormat} from "../lib/ConvertCoordinates"; import Utils from "../Utils"; /** @@ -22,7 +22,7 @@ class ConvertCoordinateFormat extends Operation { this.name = "Convert co-ordinate format"; this.module = "Hashing"; - this.description = "Convert geographical coordinates between different formats.

Supported formats:
  • Degrees Minutes Seconds (DMS)
  • Degrees Decimal Minutes (DDM)
  • Decimal Degrees (DD)
  • Geohash
  • Military Grid Reference System (MGRS)
"; + this.description = "Convert geographical coordinates between different formats.

Supported formats:
  • Degrees Minutes Seconds (DMS)
  • Degrees Decimal Minutes (DDM)
  • Decimal Degrees (DD)
  • Geohash
  • Military Grid Reference System (MGRS)
  • Ordnance Survey National Grid (OSNG)
"; this.infoURL = "https://wikipedia.org/wiki/Geographic_coordinate_conversion"; this.inputType = "string"; this.outputType = "string"; @@ -107,14 +107,14 @@ class ConvertCoordinateFormat extends Operation { } } - if (inDelim === "" && (inFormat !== "Geohash" && inFormat !== "Military Grid Reference System")) { + if (inDelim === "" && (!STRING_FORMATS.includes(inFormat))) { throw new OperationError("Could not automatically detect the input delimiter."); } // Prepare input data - if (inFormat === "Geohash" || inFormat === "Military Grid Reference System") { + if (STRING_FORMATS.includes(inFormat)) { // Geohash only has one value, so just use the input - // Replace anything that isn't a valid character in Geohash / MGRS + // Replace anything that isn't a valid character in Geohash / MGRS / OSNG inLat = input.replace(/[^A-Za-z0-9]/, ""); } else if (inDelim === "Direction Preceding") { // Split on the compass directions @@ -152,7 +152,7 @@ class ConvertCoordinateFormat extends Operation { } } - if (inFormat !== "Geohash" && inFormat !== "Military Grid Reference System" && outDelim.includes("Direction")) { + if (!STRING_FORMATS.includes(inFormat) && outDelim.includes("Direction")) { // Match on compass directions, and store the first 2 matches for the output const dir = input.match(/[NnEeSsWw]/g); if (dir !== null) { @@ -173,14 +173,15 @@ class ConvertCoordinateFormat extends Operation { // Convert the co-ordinates if (inLat !== undefined) { if (inLong === undefined) { - if (inFormat !== "Geohash" && inFormat !== "Military Grid Reference System") { - if (outFormat === "Geohash" || outFormat === "Military Grid Reference System"){ + if (!STRING_FORMATS.includes(inFormat)) { + if (STRING_FORMATS.includes(outFormat)){ throw new OperationError(`${outFormat} needs both a latitude and a longitude to be calculated`); } } - if (inFormat === "Geohash" || inFormat === "Military Grid Reference System") { + if (STRING_FORMATS.includes(inFormat)) { // Geohash conversion is in convertCoordinates despite needing // only one input as it needs to output two values + inLat = inLat.replace(/[^A-Za-z0-9]/g, ""); [outLat, outLong] = convertCoordinates(inLat, inLat, inFormat, outFormat, precision); } else { outLat = convertSingleCoordinate(inLat, inFormat, outFormat, precision); @@ -195,16 +196,16 @@ class ConvertCoordinateFormat extends Operation { // Output conversion results if successful if (outLat !== undefined) { let output = ""; - if (outDelim === "Direction Preceding" && outFormat !== "Geohash" && outFormat !== "Military Grid Reference System") { + if (outDelim === "Direction Preceding" && !STRING_FORMATS.includes(outFormat)) { output += latDir += " "; } output += outLat; - if (outDelim === "Direction Following" && outFormat !== "Geohash" && outFormat !== "Military Grid Reference System") { + if (outDelim === "Direction Following" && !STRING_FORMATS.includes(outFormat)) { output += " " + latDir; } output += outSeparator; - if (outLong !== undefined && outFormat !== "Geohash" && outFormat !== "Military Grid Reference System") { + if (outLong !== undefined && !STRING_FORMATS.includes(outFormat)) { if (outDelim === "Direction Preceding") { output += longDir + " "; } From 5e68959c03cc460edd93d7dbe9444d8250958711 Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 15 Jan 2019 10:25:49 +0000 Subject: [PATCH 11/15] Catch when OS grid references aren't calculated --- src/core/lib/ConvertCoordinates.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index 06e40f02..b6f9e9bf 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -81,6 +81,9 @@ export function convertCoordinates (inLat, inLong, inFormat, outFormat, precisio const latlon = new geodesy.LatLonEllipsoidal(parseFloat(convLat), parseFloat(convLong)); const osng = geodesy.OsGridRef.latLonToOsGrid(latlon); convLat = osng.toString(); + if (convLat === "") { + throw "Couldn't convert co-ordinates to Ordnance Survey National Grid. Are they out of range?"; + } } else { convLat = convertSingleCoordinate(convLat.toString(), "Decimal Degrees", outFormat, precision); convLong = convertSingleCoordinate(convLong.toString(), "Decimal Degrees", outFormat, precision); From d00b0f4c0e9e06af134de7cf680425b2be3668d9 Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 15 Jan 2019 15:55:49 +0000 Subject: [PATCH 12/15] Basically rewrote the whole thing using the new geodesy module --- src/core/lib/ConvertCoordinates.mjs | 464 +++++++++++++----- .../operations/ConvertCoordinateFormat.mjs | 162 +----- 2 files changed, 353 insertions(+), 273 deletions(-) diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index b6f9e9bf..cec2439c 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -18,148 +18,240 @@ export const FORMATS = [ "Decimal Degrees", "Geohash", "Military Grid Reference System", - "Ordnance Survey National Grid" + "Ordnance Survey National Grid", + "Universal Transverse Mercator" ]; /** - * Formats that are made up of one string - * These formats skip bits like filtering delimiters and - * are outputted differently (only one output) + * Formats that should be passed to Geodesy module as-is + * Spaces are still removed */ -export const STRING_FORMATS = [ +const NO_CHANGE = [ "Geohash", "Military Grid Reference System", - "Ordnance Survey National Grid" + "Ordnance Survey National Grid", + "Universal Transverse Mercator", ]; /** * Convert a given latitude and longitude into a different format. - * @param {string} inLat - Input latitude to be converted. Use this for supplying single values for conversion (e.g. geohash) - * @param {string} inLong - Input longitude to be converted + * @param {string} input - Input string to be converted * @param {string} inFormat - Format of the input coordinates + * @param {string} inDelim - The delimiter splitting the lat/long of the input * @param {string} outFormat - Format to convert to + * @param {string} outDelim - The delimiter to separate the output with + * @param {string} includeDir - Whether or not to include the compass direction in the output * @param {number} precision - Precision of the result - * @returns {string[]} Array containing the converted latitude and longitude + * @returns {string} A formatted string of the converted co-ordinates */ -export function convertCoordinates (inLat, inLong, inFormat, outFormat, precision) { - let convLat = inLat; - let convLong = inLong; +export function convertCoordinates (input, inFormat, inDelim, outFormat, outDelim, includeDir, precision) { + let isPair = false, + split, + latlon, + conv, + inLatDir, + inLongDir; + + if (inDelim === "Auto") { + inDelim = findDelim(input); + } else { + inDelim = realDelim(inDelim); + } + if (inFormat === "Auto") { + inFormat = findFormat(input, inDelim); + if (inFormat === null) { + throw "Unable to detect the input format automatically."; + } + } + if (inDelim === null && !inFormat.includes("Direction")) { + throw "Unable to detect the input delimiter automatically."; + } + outDelim = realDelim(outDelim); + + if (!NO_CHANGE.includes(inFormat)) { + split = input.split(inDelim); + if (split.length > 1) { + isPair = true; + } + } else { + input = input.replace(inDelim, ""); + isPair = true; + } + + if (inFormat.includes("Degrees")) { + [inLatDir, inLongDir] = findDirs(input, inDelim); + } + if (inFormat === "Geohash") { - const hash = geohash.decode(inLat); - convLat = hash.latitude.toString(); - convLong = hash.longitude.toString(); + const hash = geohash.decode(input.replace(/[^A-Za-z0-9]/g, "")); + latlon = new geodesy.LatLonEllipsoidal(hash.latitude, hash.longitude); } else if (inFormat === "Military Grid Reference System") { - const utm = geodesy.Mgrs.parse(inLat).toUtm(); - const result = utm.toLatLonE().toString("d", 4).replace(/[^0-9.,]/g, ""); - const splitResult = result.split(","); - if (splitResult.length === 2) { - convLat = splitResult[0]; - convLong = splitResult[1]; - } + const utm = geodesy.Mgrs.parse(input.replace(/[^A-Za-z0-9]/g, "")).toUtm(); + latlon = utm.toLatLonE(); } else if (inFormat === "Ordnance Survey National Grid") { - const osng = geodesy.OsGridRef.parse(inLat); - const latlon = geodesy.OsGridRef.osGridToLatLon(osng, geodesy.LatLonEllipsoidal.datum.WGS84); - const result = latlon.toString("d", 4).replace(/[^0-9.,]/g, ""); - const splitResult = result.split(","); - if (splitResult.length === 2) { - convLat = splitResult[0]; - convLong = splitResult[1]; + const osng = geodesy.OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, "")); + latlon = geodesy.OsGridRef.osGridToLatLon(osng); + } else if (inFormat === "Universal Transverse Mercator") { + if (/^[\d]{2}[A-Za-z]/.test(input)) { + input = input.slice(0, 2) + " " + input.slice(2); + } + const utm = geodesy.Utm.parse(input); + latlon = utm.toLatLonE(); + } else if (inFormat === "Degrees Minutes Seconds") { + if (isPair) { + split[0] = split[0].replace(/[NnEeSsWw]/g, "").trim(); + split[1] = split[1].replace(/[NnEeSsWw]/g, "").trim(); + const splitLat = split[0].split(/[°′″'"\s]/g), + splitLong = split[1].split(/[°′″'"\s]/g); + + if (splitLat.length >= 3 && splitLong.length >= 3) { + const lat = convDMSToDD(parseFloat(splitLat[0]), parseFloat(splitLat[1]), parseFloat(splitLat[2]), 10); + const long = convDMSToDD(parseFloat(splitLong[0]), parseFloat(splitLong[1]), parseFloat(splitLong[2]), 10); + latlon = new geodesy.LatLonEllipsoidal(lat.degrees, long.degrees); + } + } else { + // Create a new latlon object anyway, but we can ignore the lon value + split[0] = split[0].replace(/[NnEeSsWw]/g, "").trim(); + const splitLat = split[0].split(/[°′″'"\s]/g); + if (splitLat.length >= 3) { + const lat = convDMSToDD(parseFloat(splitLat[0]), parseFloat(splitLat[1]), parseFloat(splitLat[2])); + latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees); + } + } + } else if (inFormat === "Degrees Decimal Minutes") { + if (isPair) { + const splitLat = splitInput(split[0]); + const splitLong = splitInput(split[1]); + if (splitLat.length !== 2 || splitLong.length !== 2) { + throw "Invalid co-ordinate format for Degrees Decimal Minutes."; + } + const lat = convDDMToDD(splitLat[0], splitLat[1], 10); + const long = convDDMToDD(splitLong[0], splitLong[1], 10); + latlon = new geodesy.LatLonEllipsoidal(lat.degrees, long.degrees); + } else { + const splitLat = splitInput(input); + if (splitLat.length !== 2) { + throw "Invalid co-ordinate format for Degrees Decimal Minutes."; + } + const lat = convDDMToDD(splitLat[0], splitLat[1], 10); + latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees); + } + } else if (inFormat === "Decimal Degrees") { + if (isPair) { + const splitLat = splitInput(split[0]); + const splitLong = splitInput(split[1]); + if (splitLat.length !== 1 || splitLong.length !== 1) { + throw "Invalid co-ordinate format for Decimal Degrees."; + } + latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLong[0]); + } else { + const splitLat = splitInput(split[0]); + if (splitLat.length !== 1) { + throw "Invalid co-ordinate format for Decimal Degrees."; + } + latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLat[0]); } } else { - convLat = convertSingleCoordinate(inLat, inFormat, "Decimal Degrees", 15).split("°"); - convLong = convertSingleCoordinate(inLong, inFormat, "Decimal Degrees", 15).split("°"); + throw "Invalid input co-ordinate format selected."; } - // Convert Geohash and MGRS here, as they need both the lat and long values - if (outFormat === "Geohash") { - convLat = geohash.encode(parseFloat(convLat), parseFloat(convLong), precision); + // Everything is now a geodesy latlon object + if (outFormat === "Decimal Degrees") { + conv = latlon.toString("d", precision); + if (!isPair) { + conv = conv.split(",")[0]; + } + } else if (outFormat === "Degrees Decimal Minutes") { + conv = latlon.toString("dm", precision); + if (!isPair) { + conv = conv.split(",")[0]; + } + } else if (outFormat === "Degrees Minutes Seconds") { + conv = latlon.toString("dms", precision); + if (!isPair) { + conv = conv.split(",")[0]; + } + } else if (outFormat === "Geohash") { + conv = geohash.encode(latlon.lat.toString(), latlon.lon.toString(), precision); } else if (outFormat === "Military Grid Reference System") { - const utm = new geodesy.LatLonEllipsoidal(parseFloat(convLat), parseFloat(convLong)).toUtm(); + const utm = latlon.toUtm(); const mgrs = utm.toMgrs(); - convLat = mgrs.toString(); + conv = mgrs.toString(precision); } else if (outFormat === "Ordnance Survey National Grid") { - const latlon = new geodesy.LatLonEllipsoidal(parseFloat(convLat), parseFloat(convLong)); const osng = geodesy.OsGridRef.latLonToOsGrid(latlon); - convLat = osng.toString(); - if (convLat === "") { - throw "Couldn't convert co-ordinates to Ordnance Survey National Grid. Are they out of range?"; + if (osng.toString() === "") { + throw "Could not convert co-ordinates to OS National Grid. Are the co-ordinates in range?"; } - } else { - convLat = convertSingleCoordinate(convLat.toString(), "Decimal Degrees", outFormat, precision); - convLong = convertSingleCoordinate(convLong.toString(), "Decimal Degrees", outFormat, precision); + conv = osng.toString(precision); + } else if (outFormat === "Universal Transverse Mercator") { + const utm = latlon.toUtm(); + conv = utm.toString(precision); } - return [convLat, convLong]; + if (conv === undefined) { + throw "Error converting co-ordinates."; + } + if (outFormat.includes("Degrees")) { + let [latDir, longDir] = findDirs(conv, outDelim); + if (inLatDir !== undefined) { + latDir = inLatDir; + } + if (inLongDir !== undefined) { + longDir = inLongDir; + } + // DMS/DDM/DD + conv = conv.replace(", ", outDelim); + // Remove any directions from the current string, + // so we can put them where we want them + conv = conv.replace(/[NnEeSsWw]/g, ""); + if (includeDir !== "None") { + let outConv = ""; + if (!isPair) { + if (includeDir === "Before") { + outConv += latDir + " " + conv; + } else { + outConv += conv + " " + latDir; + } + } else { + const splitConv = conv.split(outDelim); + if (splitConv.length === 2) { + if (includeDir === "Before") { + outConv += latDir + " "; + } + outConv += splitConv[0]; + if (includeDir === "After") { + outConv += " " + latDir; + } + outConv += outDelim; + if (includeDir === "Before") { + outConv += longDir + " "; + } + outConv += splitConv[1]; + if (includeDir === "After") { + outConv += " " + longDir; + } + } + } + conv = outConv; + } + } + + return conv; } /** - * @param {string} input - The input co-ordinate to be converted - * @param {string} inFormat - The format of the input co-ordinates - * @param {string} outFormat - The format which input should be converted to - * @param {boolean} returnRaw - When true, returns the raw float instead of a String - * @returns {string|{Object}} The converted co-ordinate result, as either the raw object or a formatted string - */ -export function convertSingleCoordinate (input, inFormat, outFormat, precision, returnRaw = false){ - let converted; - precision = Math.pow(10, precision); - const convData = splitInput(input); - // Convert everything to decimal degrees first - switch (inFormat) { - case "Degrees Minutes Seconds": - if (convData.length < 3) { - throw "Invalid co-ordinates format."; - } - converted = convDMSToDD(convData[0], convData[1], convData[2], precision); - break; - case "Degrees Decimal Minutes": - if (convData.length < 2) { - throw "Invalid co-ordinates format."; - } - converted = convDDMToDD(convData[0], convData[1], precision); - break; - case "Decimal Degrees": - if (convData.length < 1) { - throw "Invalid co-ordinates format."; - } - converted = convDDToDD(convData[0], precision); - break; - default: - throw "Unknown input format selection."; - } - - // Convert from decimal degrees to the output format - switch (outFormat) { - case "Decimal Degrees": - break; - case "Degrees Minutes Seconds": - converted = convDDToDMS(converted.degrees); - break; - case "Degrees Decimal Minutes": - converted = convDDToDDM(converted.degrees, precision); - break; - default: - throw "Unknown output format selection."; - } - if (returnRaw) { - return converted; - } else { - return converted.string; - } -} - -/** - * Split up the input using a space, and sanitise the result + * Split up the input using a space or degrees signs, and sanitise the result * @param {string} input - The input data to be split * @returns {number[]} An array of the different items in the string, stored as floats */ function splitInput (input){ const split = []; - input.split(" ").forEach(item => { + input.split(/[°′″'"\s]/).forEach(item => { // Remove any character that isn't a digit item = item.replace(/[^0-9.-]/g, ""); if (item.length > 0){ - split.push(parseFloat(item, 10)); + split.push(parseFloat(item)); } }); return split; @@ -245,47 +337,153 @@ function convDDToDDM (decDegrees, precision) { } /** - * + * Finds and returns the compass directions in an input string + * @param {string} input - The input co-ordinates containing the direction + * @param {string} delim - The delimiter separating latitide and longitude + * @returns {string[]} String array containing the latitude and longitude directions + */ +export function findDirs(input, delim) { + const upperInput = input.toUpperCase(); + const dirExp = new RegExp(/[NESW]/g); + + const dirs = upperInput.match(dirExp); + + if (dirExp.test(upperInput)) { + // If there's actually compass directions in the string + if (dirs.length <= 2 && dirs.length >= 1) { + if (dirs.length === 2) { + return [dirs[0], dirs[1]]; + } else { + return [dirs[0], ""]; + } + } + } + // Nothing was returned, so guess the directions + let lat = upperInput, + long, + latDir = "", + longDir = ""; + if (!delim.includes("Direction")) { + if (upperInput.includes(delim)) { + const split = upperInput.split(delim); + if (split.length > 1) { + if (split[0] === "") { + lat = split[1]; + } else { + lat = split[0]; + } + if (split.length > 2) { + if (split[2] !== "") { + long = split[2]; + } + } + } + } + } else { + const split = upperInput.split(dirExp); + if (split.length > 1) { + if (split[0] === "") { + lat = split[1]; + } else { + lat = split[0]; + } + if (split.length > 2) { + if (split[2] !== "") { + long = split[2]; + } + } + } + } + if (lat) { + lat = parseFloat(lat); + if (lat < 0) { + latDir = "S"; + } else { + latDir = "N"; + } + } + if (long) { + long = parseFloat(long); + if (long < 0) { + longDir = "W"; + } else { + longDir = "E"; + } + } + + return [latDir, longDir]; +} + +/** + * Detects the co-ordinate format of the input data * @param {string} input - The input data whose format we need to detect * @param {string} delim - The delimiter separating the data in input * @returns {string} The input format */ export function findFormat (input, delim) { - input = input.trim(); let testData; - if (delim.includes("Direction")) { + const mgrsPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]{1}\s?[A-HJ-NP-Z][A-HJ-NP-V]\s?[0-9\s]+/), + osngPattern = new RegExp(/^[STNHO][A-HJ-Z][0-9]+$/), + geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/), + utmPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]\s[0-9\.]+\s?[0-9\.]+$/), + degPattern = new RegExp(/[°'"]/g); + input = input.trim(); + if (delim !== null && delim.includes("Direction")) { const split = input.split(/[NnEeSsWw]/); - if (split.length > 0) { + if (split.length > 1) { if (split[0] === "") { - // Direction Preceding testData = split[1]; } else { - // Direction Following testData = split[0]; } } - } else if (delim !== "") { - const split = input.split(delim); - if (!input.includes(delim)) { + } else if (delim !== null && delim !== "") { + if (input.includes(delim)) { + const split = input.split(delim); + if (split.length > 1) { + if (split[0] === "") { + testData = split[1]; + } else { + testData = split[0]; + } + } + } else { testData = input; } - if (split.length > 0) { - if (split[0] !== "") { - testData = split[0]; - } else if (split.length > 1) { - testData = split[1]; - } - } } // Test MGRS and Geohash - if (input.split(" ").length <= 1) { - const filteredInput = input.replace(/[^A-Za-z0-9]/, "").toUpperCase(); - const mgrsPattern = new RegExp(/^[0-9]{2}[C-HJ-NP-X]{2}[A-Z]+/); - const geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/); - if (mgrsPattern.test(filteredInput)) { + if (!degPattern.test(input)) { + const filteredInput = input.toUpperCase(); + const isMgrs = mgrsPattern.test(filteredInput); + const isOsng = osngPattern.test(filteredInput); + const isGeohash = geohashPattern.test(filteredInput); + const isUtm = utmPattern.test(filteredInput); + if (isMgrs && (isOsng || isGeohash)) { + if (filteredInput.includes("I")) { + // Only MGRS can have an i! + return "Military Grid Reference System"; + } + } + if (isUtm) { + return "Universal Transverse Mercator"; + } + if (isOsng && isGeohash) { + // Geohash doesn't have A, L or O, but OSNG does. + const testExp = new RegExp(/[ALO]/g); + if (testExp.test(filteredInput)) { + return "Ordnance Survey National Grid"; + } else { + return "Geohash"; + } + } + if (isMgrs) { return "Military Grid Reference System"; - } else if (geohashPattern.test(filteredInput)) { + } + if (isOsng) { + return "Ordnance Survey National Grid"; + } + if (isGeohash) { return "Geohash"; } } @@ -312,7 +510,7 @@ export function findFormat (input, delim) { */ export function findDelim (input) { input = input.trim(); - const delims = [",", ";", ":"]; + const delims = [",", ";", ":", " "]; const testDir = input.match(/[NnEeSsWw]/g); if (testDir !== null && testDir.length > 0 && testDir.length < 3) { // Possibly contains a direction @@ -340,3 +538,19 @@ export function findDelim (input) { } return null; } + +/** + * Gets the real string for a delimiter name. + * @param {string} delim The delimiter to be matched + * @returns {string} + */ +export function realDelim (delim) { + return { + "Auto": "Auto", + "Space": " ", + "\\n": "\n", + "Comma": ",", + "Semi-colon": ";", + "Colon": ":" + }[delim]; +} diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs index afc95982..770920f4 100644 --- a/src/core/operations/ConvertCoordinateFormat.mjs +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -5,9 +5,7 @@ */ import Operation from "../Operation"; -import OperationError from "../errors/OperationError"; -import {FORMATS, STRING_FORMATS, convertCoordinates, convertSingleCoordinate, findDelim, findFormat} from "../lib/ConvertCoordinates"; -import Utils from "../Utils"; +import {FORMATS, convertCoordinates} from "../lib/ConvertCoordinates"; /** * Convert co-ordinate format operation @@ -22,7 +20,7 @@ class ConvertCoordinateFormat extends Operation { this.name = "Convert co-ordinate format"; this.module = "Hashing"; - this.description = "Convert geographical coordinates between different formats.

Supported formats:
  • Degrees Minutes Seconds (DMS)
  • Degrees Decimal Minutes (DDM)
  • Decimal Degrees (DD)
  • Geohash
  • Military Grid Reference System (MGRS)
  • Ordnance Survey National Grid (OSNG)
"; + this.description = "Convert geographical coordinates between different formats.

Supported formats:
  • Degrees Minutes Seconds (DMS)
  • Degrees Decimal Minutes (DDM)
  • Decimal Degrees (DD)
  • Geohash
  • Military Grid Reference System (MGRS)
  • Ordnance Survey National Grid (OSNG)
  • Universal Transverse Mercator (UTM)

The operation can try to detect the input co-ordinate format and delimiter automatically, but this may not always work correctly."; this.infoURL = "https://wikipedia.org/wiki/Geographic_coordinate_conversion"; this.inputType = "string"; this.outputType = "string"; @@ -39,6 +37,7 @@ class ConvertCoordinateFormat extends Operation { "Auto", "Direction Preceding", "Direction Following", + "Space", "\\n", "Comma", "Semi-colon", @@ -55,14 +54,21 @@ class ConvertCoordinateFormat extends Operation { "type": "option", "value": [ "Space", - "Direction Preceding", - "Direction Following", "\\n", "Comma", "Semi-colon", "Colon" ] }, + { + "name": "Include Compass Directions", + "type": "option", + "value": [ + "None", + "Before", + "After" + ] + }, { "name": "Precision", "type": "number", @@ -77,148 +83,8 @@ class ConvertCoordinateFormat extends Operation { * @returns {string} */ run(input, args) { - const outFormat = args[2], - outDelim = args[3], - precision = args[4]; - let inFormat = args[0], - inDelim = args[1], - inLat, - inLong, - outLat, - outLong, - latDir = "", - longDir = "", - outSeparator = " "; - - // Autodetect input delimiter - if (inDelim === "Auto") { - inDelim = findDelim(input); - if (inDelim === null) { - inDelim = ""; - } - } else if (!inDelim.includes("Direction")) { - // Get the actual delimiter from the regex - inDelim = String(Utils.regexRep(inDelim)).slice(1, 2); - } - if (inFormat === "Auto") { - inFormat = findFormat(input, inDelim); - if (inFormat === null) { - throw new OperationError("Could not automatically detect the input format."); - } - } - - if (inDelim === "" && (!STRING_FORMATS.includes(inFormat))) { - throw new OperationError("Could not automatically detect the input delimiter."); - } - - // Prepare input data - if (STRING_FORMATS.includes(inFormat)) { - // Geohash only has one value, so just use the input - // Replace anything that isn't a valid character in Geohash / MGRS / OSNG - inLat = input.replace(/[^A-Za-z0-9]/, ""); - } else if (inDelim === "Direction Preceding") { - // Split on the compass directions - const splitInput = input.split(/[NnEeSsWw]/); - const dir = input.match(/[NnEeSsWw]/g); - if (splitInput.length > 1) { - inLat = splitInput[1]; - if (dir !== null) { - latDir = dir[0]; - } - if (splitInput.length > 2) { - inLong = splitInput[2]; - if (dir !== null && dir.length > 1) { - longDir = dir[1]; - } - } - } - } else if (inDelim === "Direction Following") { - // Split on the compass directions - const splitInput = input.split(/[NnEeSsWw]/); - if (splitInput.length >= 1) { - inLat = splitInput[0]; - if (splitInput.length >= 2) { - inLong = splitInput[1]; - } - } - } else { - // Split on the delimiter - const splitInput = input.split(inDelim); - if (splitInput.length > 0) { - inLat = splitInput[0]; - if (splitInput.length >= 2) { - inLong = splitInput[1]; - } - } - } - - if (!STRING_FORMATS.includes(inFormat) && outDelim.includes("Direction")) { - // Match on compass directions, and store the first 2 matches for the output - const dir = input.match(/[NnEeSsWw]/g); - if (dir !== null) { - latDir = dir[0]; - if (dir.length > 1) { - longDir = dir[1]; - } - } - } else if (outDelim === "\\n") { - outSeparator = "\n"; - } else if (outDelim === "Space") { - outSeparator = " "; - } else if (!outDelim.includes("Direction")) { - // Cut out the regex syntax (/) from the delimiter - outSeparator = String(Utils.regexRep(outDelim)).slice(1, 2); - } - - // Convert the co-ordinates - if (inLat !== undefined) { - if (inLong === undefined) { - if (!STRING_FORMATS.includes(inFormat)) { - if (STRING_FORMATS.includes(outFormat)){ - throw new OperationError(`${outFormat} needs both a latitude and a longitude to be calculated`); - } - } - if (STRING_FORMATS.includes(inFormat)) { - // Geohash conversion is in convertCoordinates despite needing - // only one input as it needs to output two values - inLat = inLat.replace(/[^A-Za-z0-9]/g, ""); - [outLat, outLong] = convertCoordinates(inLat, inLat, inFormat, outFormat, precision); - } else { - outLat = convertSingleCoordinate(inLat, inFormat, outFormat, precision); - } - } else { - [outLat, outLong] = convertCoordinates(inLat, inLong, inFormat, outFormat, precision); - } - } else { - throw new OperationError("No co-ordinates were detected in the input."); - } - - // Output conversion results if successful - if (outLat !== undefined) { - let output = ""; - if (outDelim === "Direction Preceding" && !STRING_FORMATS.includes(outFormat)) { - output += latDir += " "; - } - output += outLat; - if (outDelim === "Direction Following" && !STRING_FORMATS.includes(outFormat)) { - output += " " + latDir; - } - output += outSeparator; - - if (outLong !== undefined && !STRING_FORMATS.includes(outFormat)) { - if (outDelim === "Direction Preceding") { - output += longDir + " "; - } - output += outLong; - if (outDelim === "Direction Following") { - output += " " + longDir; - } - output += outSeparator; - } - return output; - } else { - throw new OperationError("Co-ordinate conversion failed."); - } + const [inFormat, inDelim, outFormat, outDelim, incDirection, precision] = args; + return convertCoordinates(input, inFormat, inDelim, outFormat, outDelim, incDirection, precision); } } From 69797e58cb6fc5d5622c55dbd42fab83de1ad668 Mon Sep 17 00:00:00 2001 From: j433866 Date: Wed, 16 Jan 2019 16:57:58 +0000 Subject: [PATCH 13/15] Add better error handling. Also now doesn't do anything if there's no input --- src/core/operations/ConvertCoordinateFormat.mjs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs index 770920f4..09e1620c 100644 --- a/src/core/operations/ConvertCoordinateFormat.mjs +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -5,6 +5,7 @@ */ import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; import {FORMATS, convertCoordinates} from "../lib/ConvertCoordinates"; /** @@ -37,7 +38,6 @@ class ConvertCoordinateFormat extends Operation { "Auto", "Direction Preceding", "Direction Following", - "Space", "\\n", "Comma", "Semi-colon", @@ -83,8 +83,17 @@ class ConvertCoordinateFormat extends Operation { * @returns {string} */ run(input, args) { - const [inFormat, inDelim, outFormat, outDelim, incDirection, precision] = args; - return convertCoordinates(input, inFormat, inDelim, outFormat, outDelim, incDirection, precision); + if (input.replace(/[\s+]/g, "") !== "") { + const [inFormat, inDelim, outFormat, outDelim, incDirection, precision] = args; + try { + const result = convertCoordinates(input, inFormat, inDelim, outFormat, outDelim, incDirection, precision); + return result; + } catch (error) { + throw new OperationError(error); + } + } else { + return input; + } } } From 439654ed7f11d7c7bf3a4364d0f4bb95dce34aee Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 17 Jan 2019 13:49:36 +0000 Subject: [PATCH 14/15] Add tests for new co-ordinate conversion module. Removed To/From geohash tests --- test/index.mjs | 3 +- .../operations/ConvertCoordinateFormat.mjs | 211 ++++++++++++++++++ test/tests/operations/FromGeohash.mjs | 55 ----- test/tests/operations/ToGeohash.mjs | 55 ----- 4 files changed, 212 insertions(+), 112 deletions(-) create mode 100644 test/tests/operations/ConvertCoordinateFormat.mjs delete mode 100644 test/tests/operations/FromGeohash.mjs delete mode 100644 test/tests/operations/ToGeohash.mjs diff --git a/test/index.mjs b/test/index.mjs index 9c11f6ae..8d7bd798 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -44,7 +44,6 @@ import "./tests/operations/DateTime"; import "./tests/operations/ExtractEmailAddresses"; import "./tests/operations/Fork"; import "./tests/operations/FromDecimal"; -import "./tests/operations/FromGeohash"; import "./tests/operations/Hash"; import "./tests/operations/HaversineDistance"; import "./tests/operations/Hexdump"; @@ -74,10 +73,10 @@ import "./tests/operations/SetIntersection"; import "./tests/operations/SetUnion"; import "./tests/operations/StrUtils"; import "./tests/operations/SymmetricDifference"; -import "./tests/operations/ToGeohash.mjs"; import "./tests/operations/TranslateDateTimeFormat"; import "./tests/operations/Magic"; import "./tests/operations/ParseTLV"; +import "./tests/operations/ConvertCoordinateFormat"; let allTestsPassing = true; const testStatusCounts = { diff --git a/test/tests/operations/ConvertCoordinateFormat.mjs b/test/tests/operations/ConvertCoordinateFormat.mjs new file mode 100644 index 00000000..89690b97 --- /dev/null +++ b/test/tests/operations/ConvertCoordinateFormat.mjs @@ -0,0 +1,211 @@ +/** + * Convert co-ordinate format tests + * + * @author j433866 + * + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +/** + * TEST CO-ORDINATES + * DD: 51.504°,-0.126°, + * DDM: 51° 30.24',-0° 7.56', + * DMS: 51° 30' 14.4",-0° 7' 33.6", + * Geohash: gcpvj0h0x, + * MGRS: 30U XC 99455 09790, + * OSNG: TQ 30163 80005, + * UTM: 30N 699456 5709791, + */ + +import TestRegister from "../../TestRegister"; + +TestRegister.addTests([ + { + name: "Co-ordinates: From Decimal Degrees to Degrees Minutes Seconds", + input: "51.504°,-0.126°,", + expectedOutput: "51° 30' 14.4\",-0° 7' 33.6\",", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Degrees Minutes Seconds", "Comma", "None", 1] + }, + ], + }, + { + name: "Co-ordinates: From Degrees Minutes Seconds to Decimal Degrees", + input: "51° 30' 14.4\",-0° 7' 33.6\",", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Degrees Minutes Seconds", "Comma", "Decimal Degrees", "Comma", "None", 3] + }, + ], + }, + { + name: "Co-ordinates: From Decimal Degrees to Degrees Decimal Minutes", + input: "51.504°,-0.126°,", + expectedOutput: "51° 30.24',-0° 7.56',", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Degrees Decimal Minutes", "Comma", "None", 2] + } + ] + }, + { + name: "Co-ordinates: From Degrees Decimal Minutes to Decimal Degrees", + input: "51° 30.24',-0° 7.56',", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Degrees Decimal Minutes", "Comma", "Decimal Degrees", "Comma", "None", 3] + } + ] + }, + { + name: "Co-ordinates: From Decimal Degrees to Decimal Degrees", + input: "51.504°,-0.126°,", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Decimal Degrees", "Comma", "None", 3] + } + ] + }, + { + name: "Co-ordinates: From Decimal Degrees to Geohash", + input: "51.504°,-0.126°,", + expectedOutput: "gcpvj0h0x,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Geohash", "Comma", "None", 9] + }, + ], + }, + { + name: "Co-ordinates: From Geohash to Decimal Degrees", + input: "gcpvj0h0x,", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Geohash", "Comma", "Decimal Degrees", "Comma", "None", 3] + }, + ], + }, + { + name: "Co-ordinates: From Decimal Degrees to MGRS", + input: "51.504°,-0.126°,", + expectedOutput: "30U XC 99455 09790,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Military Grid Reference System", "Comma", "None", 10] + }, + ], + }, + { + name: "Co-ordinates: From MGRS to Decimal Degrees", + input: "30U XC 99455 09790,", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Military Grid Reference System", "Comma", "Decimal Degrees", "Comma", "None", 3] + } + ] + }, + { + name: "Co-ordinates: From Decimal Degrees to OSNG", + input: "51.504°,-0.126°,", + expectedOutput: "TQ 30163 80005,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Ordnance Survey National Grid", "Comma", "None", 10] + }, + ], + }, + { + name: "Co-ordinates: From OSNG to Decimal Degrees", + input: "TQ 30163 80005,", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Ordnance Survey National Grid", "Comma", "Decimal Degrees", "Comma", "None", 3] + }, + ], + }, + { + name: "Co-ordinates: From Decimal Degrees to UTM", + input: "51.504°,-0.126°,", + expectedOutput: "30 N 699456 5709791,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Universal Transverse Mercator", "Comma", "None", 0] + }, + ], + }, + { + name: "Co-ordinates: From UTM to Decimal Degrees", + input: "30 N 699456 5709791,", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Universal Transverse Mercator", "Comma", "Decimal Degrees", "Comma", "None", 3] + }, + ], + }, + { + name: "Co-ordinates: Directions in input, not output", + input: "N51.504°,W0.126°,", + expectedOutput: "51.504°,-0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Decimal Degrees", "Comma", "None", 3] + }, + ], + }, + { + name: "Co-ordinates: Directions in input and output", + input: "N51.504°,W0.126°,", + expectedOutput: "N 51.504°,W 0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Decimal Degrees", "Comma", "Before", 3] + }, + ], + }, + { + name: "Co-ordinates: Directions not in input, in output", + input: "51.504°,-0.126°,", + expectedOutput: "N 51.504°,W 0.126°,", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Decimal Degrees", "Comma", "Before", 3] + }, + ], + }, + { + name: "Co-ordinates: Directions not in input, in converted output", + input: "51.504°,-0.126°,", + expectedOutput: "N 51° 30' 14.4\",W 0° 7' 33.6\",", + recipeConfig: [ + { + op: "Convert co-ordinate format", + args: ["Decimal Degrees", "Comma", "Degrees Minutes Seconds", "Comma", "Before", 3] + }, + ], + } +]); diff --git a/test/tests/operations/FromGeohash.mjs b/test/tests/operations/FromGeohash.mjs deleted file mode 100644 index 2ac68c58..00000000 --- a/test/tests/operations/FromGeohash.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * To Geohash tests - * - * @author gchq77703 - * @copyright Crown Copyright 2018 - * @license Apache-2.0 - */ -import TestRegister from "../../TestRegister"; - -TestRegister.addTests([ - { - name: "From Geohash", - input: "ww8p1r4t8", - expectedOutput: "37.83238649368286,112.55838632583618", - recipeConfig: [ - { - op: "From Geohash", - args: [], - }, - ], - }, - { - name: "From Geohash", - input: "ww8p1r", - expectedOutput: "37.83416748046875,112.5604248046875", - recipeConfig: [ - { - op: "From Geohash", - args: [], - }, - ], - }, - { - name: "From Geohash", - input: "ww8", - expectedOutput: "37.265625,113.203125", - recipeConfig: [ - { - op: "From Geohash", - args: [], - }, - ], - }, - { - name: "From Geohash", - input: "w", - expectedOutput: "22.5,112.5", - recipeConfig: [ - { - op: "From Geohash", - args: [], - }, - ], - }, -]); diff --git a/test/tests/operations/ToGeohash.mjs b/test/tests/operations/ToGeohash.mjs deleted file mode 100644 index b50e7280..00000000 --- a/test/tests/operations/ToGeohash.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * To Geohash tests - * - * @author gchq77703 - * @copyright Crown Copyright 2018 - * @license Apache-2.0 - */ -import TestRegister from "../../TestRegister"; - -TestRegister.addTests([ - { - name: "To Geohash", - input: "37.8324,112.5584", - expectedOutput: "ww8p1r4t8", - recipeConfig: [ - { - op: "To Geohash", - args: [9], - }, - ], - }, - { - name: "To Geohash", - input: "37.9324,-112.2584", - expectedOutput: "9w8pv3ruj", - recipeConfig: [ - { - op: "To Geohash", - args: [9], - }, - ], - }, - { - name: "To Geohash", - input: "37.8324,112.5584", - expectedOutput: "ww8", - recipeConfig: [ - { - op: "To Geohash", - args: [3], - }, - ], - }, - { - name: "To Geohash", - input: "37.9324,-112.2584", - expectedOutput: "9w8pv3rujxy5b99", - recipeConfig: [ - { - op: "To Geohash", - args: [15], - }, - ], - }, -]); From 4bd923dc063f901d54237204834ad0b85295964b Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 17 Jan 2019 13:53:42 +0000 Subject: [PATCH 15/15] Improved handling of negative numbers and weirder inputs. Negative numbers shouldn't make it go weird any more. Automatic detection of input formats should be more reliable. --- src/core/lib/ConvertCoordinates.mjs | 352 +++++++++++++++++----------- 1 file changed, 212 insertions(+), 140 deletions(-) diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index cec2439c..d4641fe6 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -23,8 +23,7 @@ export const FORMATS = [ ]; /** - * Formats that should be passed to Geodesy module as-is - * Spaces are still removed + * Formats that should be passed to the conversion module as-is */ const NO_CHANGE = [ "Geohash", @@ -48,40 +47,50 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli let isPair = false, split, latlon, - conv, - inLatDir, - inLongDir; + convLat, + convLon, + conv; + // Can't have a precision less than 0! + if (precision < 0) { + precision = 0; + } if (inDelim === "Auto") { + // Try to detect a delimiter in the input. inDelim = findDelim(input); + if (inDelim === null) { + throw "Unable to detect the input delimiter automatically."; + } } else { + // Convert the delimiter argument value to the actual character inDelim = realDelim(inDelim); } if (inFormat === "Auto") { + // Try to detect the format of the input data inFormat = findFormat(input, inDelim); if (inFormat === null) { throw "Unable to detect the input format automatically."; } } - if (inDelim === null && !inFormat.includes("Direction")) { - throw "Unable to detect the input delimiter automatically."; - } + // Convert the output delimiter argument to the real character outDelim = realDelim(outDelim); if (!NO_CHANGE.includes(inFormat)) { split = input.split(inDelim); + // Replace any co-ordinate symbols with spaces so we can split on them later + for (let i = 0; i < split.length; i++) { + split[i] = split[i].replace(/[°˝´'"]/g, " "); + } if (split.length > 1) { isPair = true; } } else { + // Remove any delimiters from the input input = input.replace(inDelim, ""); isPair = true; } - if (inFormat.includes("Degrees")) { - [inLatDir, inLongDir] = findDirs(input, inDelim); - } - + // Conversions from the input format into a geodesy latlon object if (inFormat === "Geohash") { const hash = geohash.decode(input.replace(/[^A-Za-z0-9]/g, "")); latlon = new geodesy.LatLonEllipsoidal(hash.latitude, hash.longitude); @@ -92,6 +101,7 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli const osng = geodesy.OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, "")); latlon = geodesy.OsGridRef.osGridToLatLon(osng); } else if (inFormat === "Universal Transverse Mercator") { + // Geodesy needs a space between the first 2 digits and the next letter if (/^[\d]{2}[A-Za-z]/.test(input)) { input = input.slice(0, 2) + " " + input.slice(2); } @@ -99,23 +109,25 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli latlon = utm.toLatLonE(); } else if (inFormat === "Degrees Minutes Seconds") { if (isPair) { - split[0] = split[0].replace(/[NnEeSsWw]/g, "").trim(); - split[1] = split[1].replace(/[NnEeSsWw]/g, "").trim(); - const splitLat = split[0].split(/[°′″'"\s]/g), - splitLong = split[1].split(/[°′″'"\s]/g); + // Split up the lat/long into degrees / minutes / seconds values + const splitLat = splitInput(split[0]), + splitLong = splitInput(split[1]); if (splitLat.length >= 3 && splitLong.length >= 3) { - const lat = convDMSToDD(parseFloat(splitLat[0]), parseFloat(splitLat[1]), parseFloat(splitLat[2]), 10); - const long = convDMSToDD(parseFloat(splitLong[0]), parseFloat(splitLong[1]), parseFloat(splitLong[2]), 10); + const lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2], 10); + const long = convDMSToDD(splitLong[0], splitLong[1], splitLong[2], 10); latlon = new geodesy.LatLonEllipsoidal(lat.degrees, long.degrees); + } else { + throw "Invalid co-ordinate format for Degrees Minutes Seconds"; } } else { - // Create a new latlon object anyway, but we can ignore the lon value - split[0] = split[0].replace(/[NnEeSsWw]/g, "").trim(); - const splitLat = split[0].split(/[°′″'"\s]/g); + // Not a pair, so only try to convert one set of co-ordinates + const splitLat = splitInput(split[0]); if (splitLat.length >= 3) { - const lat = convDMSToDD(parseFloat(splitLat[0]), parseFloat(splitLat[1]), parseFloat(splitLat[2])); + const lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2]); latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees); + } else { + throw "Invalid co-ordinate format for Degrees Minutes Seconds"; } } } else if (inFormat === "Degrees Decimal Minutes") { @@ -125,10 +137,12 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli if (splitLat.length !== 2 || splitLong.length !== 2) { throw "Invalid co-ordinate format for Degrees Decimal Minutes."; } + // Convert to decimal degrees, and then convert to a geodesy object const lat = convDDMToDD(splitLat[0], splitLat[1], 10); const long = convDDMToDD(splitLong[0], splitLong[1], 10); latlon = new geodesy.LatLonEllipsoidal(lat.degrees, long.degrees); } else { + // Not a pair, so only try to convert one set of co-ordinates const splitLat = splitInput(input); if (splitLat.length !== 2) { throw "Invalid co-ordinate format for Degrees Decimal Minutes."; @@ -145,6 +159,7 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli } latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLong[0]); } else { + // Not a pair, so only try to convert one set of co-ordinates const splitLat = splitInput(split[0]); if (splitLat.length !== 1) { throw "Invalid co-ordinate format for Decimal Degrees."; @@ -152,88 +167,115 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLat[0]); } } else { - throw "Invalid input co-ordinate format selected."; + throw `Unknown input format '${inFormat}'`; } // Everything is now a geodesy latlon object + // These store the latitude and longitude as decimal + if (inFormat.includes("Degrees")) { + // If the input string contains directions, we need to check if they're S or W. + // If either of the directions are, we should make the decimal value negative + const dirs = input.match(/[NnEeSsWw]/g); + if (dirs && dirs.length >= 1) { + // Make positive lat/lon values with S/W directions into negative values + if (dirs[0] === "S" || dirs[0] === "W" && latlon.lat > 0) { + latlon.lat = 0 - latlon.lat; + } + if (dirs.length >= 2) { + if (dirs[1] === "S" || dirs[1] === "W" && latlon.lon > 0) { + latlon.lon = 0 - latlon.lon; + } + } + } + } + // Try to find the compass directions of the lat and long + const [latDir, longDir] = findDirs(latlon.lat + "," + latlon.lon, ","); + // Output conversions for each output format if (outFormat === "Decimal Degrees") { - conv = latlon.toString("d", precision); - if (!isPair) { - conv = conv.split(",")[0]; - } + // We could use the built in latlon.toString(), + // but this makes adjusting the output harder + const lat = convDDToDD(latlon.lat, precision); + const lon = convDDToDD(latlon.lon, precision); + convLat = lat.string; + convLon = lon.string; } else if (outFormat === "Degrees Decimal Minutes") { - conv = latlon.toString("dm", precision); - if (!isPair) { - conv = conv.split(",")[0]; - } + const lat = convDDToDDM(latlon.lat, precision); + const lon = convDDToDDM(latlon.lon, precision); + convLat = lat.string; + convLon = lon.string; } else if (outFormat === "Degrees Minutes Seconds") { - conv = latlon.toString("dms", precision); - if (!isPair) { - conv = conv.split(",")[0]; - } + const lat = convDDToDMS(latlon.lat, precision); + const lon = convDDToDMS(latlon.lon, precision); + convLat = lat.string; + convLon = lon.string; } else if (outFormat === "Geohash") { - conv = geohash.encode(latlon.lat.toString(), latlon.lon.toString(), precision); + convLat = geohash.encode(latlon.lat, latlon.lon, precision); } else if (outFormat === "Military Grid Reference System") { const utm = latlon.toUtm(); const mgrs = utm.toMgrs(); - conv = mgrs.toString(precision); + // MGRS wants a precision that's an even number between 2 and 10 + if (precision % 2 !== 0) { + precision = precision + 1; + } + if (precision > 10) { + precision = 10; + } + convLat = mgrs.toString(precision); } else if (outFormat === "Ordnance Survey National Grid") { const osng = geodesy.OsGridRef.latLonToOsGrid(latlon); if (osng.toString() === "") { throw "Could not convert co-ordinates to OS National Grid. Are the co-ordinates in range?"; } - conv = osng.toString(precision); + // OSNG wants a precision that's an even number between 2 and 10 + if (precision % 2 !== 0) { + precision = precision + 1; + } + if (precision > 10) { + precision = 10; + } + convLat = osng.toString(precision); } else if (outFormat === "Universal Transverse Mercator") { const utm = latlon.toUtm(); - conv = utm.toString(precision); + convLat = utm.toString(precision); } - if (conv === undefined) { + if (convLat === undefined) { throw "Error converting co-ordinates."; } + if (outFormat.includes("Degrees")) { - let [latDir, longDir] = findDirs(conv, outDelim); - if (inLatDir !== undefined) { - latDir = inLatDir; + // Format DD/DDM/DMS for output + // If we're outputting a compass direction, remove the negative sign + if (latDir === "S" && includeDir !== "None") { + convLat = convLat.replace("-", ""); } - if (inLongDir !== undefined) { - longDir = inLongDir; + if (longDir === "W" && includeDir !== "None") { + convLon = convLon.replace("-", ""); } - // DMS/DDM/DD - conv = conv.replace(", ", outDelim); - // Remove any directions from the current string, - // so we can put them where we want them - conv = conv.replace(/[NnEeSsWw]/g, ""); - if (includeDir !== "None") { - let outConv = ""; - if (!isPair) { - if (includeDir === "Before") { - outConv += latDir + " " + conv; - } else { - outConv += conv + " " + latDir; - } - } else { - const splitConv = conv.split(outDelim); - if (splitConv.length === 2) { - if (includeDir === "Before") { - outConv += latDir + " "; - } - outConv += splitConv[0]; - if (includeDir === "After") { - outConv += " " + latDir; - } - outConv += outDelim; - if (includeDir === "Before") { - outConv += longDir + " "; - } - outConv += splitConv[1]; - if (includeDir === "After") { - outConv += " " + longDir; - } - } + + let outConv = ""; + if (includeDir === "Before") { + outConv += latDir + " "; + } + + outConv += convLat; + if (includeDir === "After") { + outConv += " " + latDir; + } + outConv += outDelim; + if (isPair) { + if (includeDir === "Before") { + outConv += longDir + " "; } - conv = outConv; + outConv += convLon; + if (includeDir === "After") { + outConv += " " + longDir; + } + outConv += outDelim; } + conv = outConv; + } else { + conv = convLat + outDelim; } return conv; @@ -247,10 +289,11 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli function splitInput (input){ const split = []; - input.split(/[°′″'"\s]/).forEach(item => { - // Remove any character that isn't a digit + input.split(/\s+/).forEach(item => { + // Remove any character that isn't a digit, decimal point or negative sign item = item.replace(/[^0-9.-]/g, ""); if (item.length > 0){ + // Turn the item into a float split.push(parseFloat(item)); } }); @@ -266,10 +309,17 @@ function splitInput (input){ * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string) */ function convDMSToDD (degrees, minutes, seconds, precision){ - const converted = new Object(); - converted.degrees = degrees + (minutes / 60) + (seconds / 3600); - converted.string = (Math.round(converted.degrees * precision) / precision) + "°"; - return converted; + const absDegrees = Math.abs(degrees); + let conv = absDegrees + (minutes / 60) + (seconds / 3600); + let outString = round(conv, precision) + "°"; + if (isNegativeZero(degrees) || degrees < 0) { + conv = -conv; + outString = "-" + outString; + } + return { + "degrees": conv, + "string": outString + }; } /** @@ -280,10 +330,17 @@ function convDMSToDD (degrees, minutes, seconds, precision){ * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string) */ function convDDMToDD (degrees, minutes, precision) { - const converted = new Object(); - converted.degrees = degrees + minutes / 60; - converted.string = ((Math.round(converted.degrees * precision) / precision) + "°"); - return converted; + const absDegrees = Math.abs(degrees); + let conv = absDegrees + minutes / 60; + let outString = round(conv, precision) + "°"; + if (isNegativeZero(degrees) || degrees < 0) { + conv = -conv; + outString = "-" + outString; + } + return { + "degrees": conv, + "string": outString + }; } /** @@ -294,28 +351,34 @@ function convDDMToDD (degrees, minutes, precision) { * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string) */ function convDDToDD (degrees, precision) { - const converted = new Object(); - converted.degrees = degrees; - converted.string = Math.round(converted.degrees * precision) / precision + "°"; - return converted; + return { + "degrees": degrees, + "string": round(degrees, precision) + "°" + }; } /** * Convert Decimal Degrees to Degrees Minutes Seconds * @param {number} decDegrees - The input data to be converted + * @param {number} precision - The precision which the result should be rounded to * @returns {{string: string, degrees: number, minutes: number, seconds: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes, .seconds), and a formatted string version (obj.string) */ -function convDDToDMS (decDegrees) { - const degrees = Math.floor(decDegrees); - const minutes = Math.floor(60 * (decDegrees - degrees)); - const seconds = Math.round(3600 * (decDegrees - degrees) - 60 * minutes); - - const converted = new Object(); - converted.degrees = degrees; - converted.minutes = minutes; - converted.seconds = seconds; - converted.string = degrees + "° " + minutes + "' " + seconds + "\""; - return converted; +function convDDToDMS (decDegrees, precision) { + const absDegrees = Math.abs(decDegrees); + let degrees = Math.floor(absDegrees); + const minutes = Math.floor(60 * (absDegrees - degrees)), + seconds = round(3600 * (absDegrees - degrees) - 60 * minutes, precision); + let outString = degrees + "° " + minutes + "' " + seconds + "\""; + if (isNegativeZero(decDegrees) || decDegrees < 0) { + degrees = -degrees; + outString = "-" + outString; + } + return { + "degrees": degrees, + "minutes": minutes, + "seconds": seconds, + "string": outString + }; } /** @@ -325,15 +388,21 @@ function convDDToDMS (decDegrees) { * @returns {{string: string, degrees: number, minutes: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes), and a formatted string version (obj.string) */ function convDDToDDM (decDegrees, precision) { - const degrees = Math.floor(decDegrees); - const minutes = decDegrees - degrees; - const decMinutes = Math.round((minutes * 60) * precision) / precision; + const absDegrees = Math.abs(decDegrees); + let degrees = Math.floor(absDegrees); + const minutes = absDegrees - degrees, + decMinutes = round(minutes * 60, precision); + let outString = degrees + "° " + decMinutes + "'"; + if (decDegrees < 0 || isNegativeZero(decDegrees)) { + degrees = -degrees; + outString = "-" + outString; + } - const converted = new Object(); - converted.degrees = degrees; - converted.minutes = decMinutes; - converted.string = degrees + "° " + decMinutes + "'"; - return converted; + return { + "degrees": degrees, + "minutes": decMinutes, + "string": outString, + }; } /** @@ -348,8 +417,9 @@ export function findDirs(input, delim) { const dirs = upperInput.match(dirExp); - if (dirExp.test(upperInput)) { - // If there's actually compass directions in the string + if (dirs) { + // If there's actually compass directions + // in the input, use these to work out the direction if (dirs.length <= 2 && dirs.length >= 1) { if (dirs.length === 2) { return [dirs[0], dirs[1]]; @@ -366,15 +436,13 @@ export function findDirs(input, delim) { if (!delim.includes("Direction")) { if (upperInput.includes(delim)) { const split = upperInput.split(delim); - if (split.length > 1) { - if (split[0] === "") { - lat = split[1]; - } else { + if (split.length >= 1) { + if (split[0] !== "") { lat = split[0]; } - if (split.length > 2) { - if (split[2] !== "") { - long = split[2]; + if (split.length >= 2) { + if (split[1] !== "") { + long = split[1]; } } } @@ -423,9 +491,9 @@ export function findDirs(input, delim) { export function findFormat (input, delim) { let testData; const mgrsPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]{1}\s?[A-HJ-NP-Z][A-HJ-NP-V]\s?[0-9\s]+/), - osngPattern = new RegExp(/^[STNHO][A-HJ-Z][0-9]+$/), + osngPattern = new RegExp(/^[A-HJ-Z]{2}\s+[0-9\s]+$/), geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/), - utmPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]\s[0-9\.]+\s?[0-9\.]+$/), + utmPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]\s[0-9.]+\s?[0-9.]+$/), degPattern = new RegExp(/[°'"]/g); input = input.trim(); if (delim !== null && delim.includes("Direction")) { @@ -452,31 +520,16 @@ export function findFormat (input, delim) { } } - // Test MGRS and Geohash + // Test non-degrees formats if (!degPattern.test(input)) { - const filteredInput = input.toUpperCase(); + const filteredInput = input.toUpperCase().replace(delim, ""); const isMgrs = mgrsPattern.test(filteredInput); const isOsng = osngPattern.test(filteredInput); const isGeohash = geohashPattern.test(filteredInput); const isUtm = utmPattern.test(filteredInput); - if (isMgrs && (isOsng || isGeohash)) { - if (filteredInput.includes("I")) { - // Only MGRS can have an i! - return "Military Grid Reference System"; - } - } if (isUtm) { return "Universal Transverse Mercator"; } - if (isOsng && isGeohash) { - // Geohash doesn't have A, L or O, but OSNG does. - const testExp = new RegExp(/[ALO]/g); - if (testExp.test(filteredInput)) { - return "Ordnance Survey National Grid"; - } else { - return "Geohash"; - } - } if (isMgrs) { return "Military Grid Reference System"; } @@ -510,7 +563,7 @@ export function findFormat (input, delim) { */ export function findDelim (input) { input = input.trim(); - const delims = [",", ";", ":", " "]; + const delims = [",", ";", ":"]; const testDir = input.match(/[NnEeSsWw]/g); if (testDir !== null && testDir.length > 0 && testDir.length < 3) { // Possibly contains a direction @@ -554,3 +607,22 @@ export function realDelim (delim) { "Colon": ":" }[delim]; } + +/** + * Returns true if a zero is negative + * @param {number} zero + */ +function isNegativeZero(zero) { + return zero === 0 && (1/zero < 0); +} + +/** + * Rounds a number to a specified number of decimal places + * @param {number} input - The number to be rounded + * @param {precision} precision - The number of decimal places the number should be rounded to + * @returns {number} + */ +function round(input, precision) { + precision = Math.pow(10, precision); + return Math.round(input * precision) / precision; +}