diff --git a/package-lock.json b/package-lock.json index 57ff15fc..73620cee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5699,6 +5699,11 @@ "globule": "^1.0.0" } }, + "geodesy": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/geodesy/-/geodesy-1.1.3.tgz", + "integrity": "sha512-H/0XSd1KjKZGZ2YGZcOYzRyY/foYAawwTEumNSo+YUwf+u5d4CfvBRg2i2Qimrx9yUEjWR8hLvMnhghuVFN0Zg==" + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", diff --git a/package.json b/package.json index e6f62093..49af6938 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "esprima": "^4.0.1", "exif-parser": "^0.1.12", "file-saver": "^2.0.0", + "geodesy": "^1.1.3", "highlight.js": "^9.13.1", "jimp": "^0.6.0", "jquery": "^3.3.1", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index fd713868..3da6a5e0 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -215,6 +215,7 @@ "Convert mass", "Convert speed", "Convert data units", + "Convert co-ordinate format", "Parse UNIX file permissions", "Swap endianness", "Parse colour code", @@ -306,9 +307,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/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs new file mode 100644 index 00000000..d4641fe6 --- /dev/null +++ b/src/core/lib/ConvertCoordinates.mjs @@ -0,0 +1,628 @@ +/** + * Co-ordinate conversion resources. + * + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import geohash from "ngeohash"; +import geodesy from "geodesy"; + +/** + * Co-ordinate formats + */ +export const FORMATS = [ + "Degrees Minutes Seconds", + "Degrees Decimal Minutes", + "Decimal Degrees", + "Geohash", + "Military Grid Reference System", + "Ordnance Survey National Grid", + "Universal Transverse Mercator" +]; + +/** + * Formats that should be passed to the conversion module as-is + */ +const NO_CHANGE = [ + "Geohash", + "Military Grid Reference System", + "Ordnance Survey National Grid", + "Universal Transverse Mercator", +]; + +/** + * Convert a given latitude and longitude into a different format. + * @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} A formatted string of the converted co-ordinates + */ +export function convertCoordinates (input, inFormat, inDelim, outFormat, outDelim, includeDir, precision) { + let isPair = false, + split, + latlon, + 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."; + } + } + // 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; + } + + // 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); + } else if (inFormat === "Military Grid Reference System") { + 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(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); + } + const utm = geodesy.Utm.parse(input); + latlon = utm.toLatLonE(); + } else if (inFormat === "Degrees Minutes Seconds") { + if (isPair) { + // 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(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 { + // 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(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") { + 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."; + } + // 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."; + } + 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 { + // 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."; + } + latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLat[0]); + } + } else { + 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") { + // 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") { + const lat = convDDToDDM(latlon.lat, precision); + const lon = convDDToDDM(latlon.lon, precision); + convLat = lat.string; + convLon = lon.string; + } else if (outFormat === "Degrees Minutes Seconds") { + const lat = convDDToDMS(latlon.lat, precision); + const lon = convDDToDMS(latlon.lon, precision); + convLat = lat.string; + convLon = lon.string; + } else if (outFormat === "Geohash") { + convLat = geohash.encode(latlon.lat, latlon.lon, precision); + } else if (outFormat === "Military Grid Reference System") { + const utm = latlon.toUtm(); + const mgrs = utm.toMgrs(); + // 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?"; + } + // 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(); + convLat = utm.toString(precision); + } + + if (convLat === undefined) { + throw "Error converting co-ordinates."; + } + + if (outFormat.includes("Degrees")) { + // 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 (longDir === "W" && includeDir !== "None") { + convLon = convLon.replace("-", ""); + } + + let outConv = ""; + if (includeDir === "Before") { + outConv += latDir + " "; + } + + outConv += convLat; + if (includeDir === "After") { + outConv += " " + latDir; + } + outConv += outDelim; + if (isPair) { + if (includeDir === "Before") { + outConv += longDir + " "; + } + outConv += convLon; + if (includeDir === "After") { + outConv += " " + longDir; + } + outConv += outDelim; + } + conv = outConv; + } else { + conv = convLat + outDelim; + } + + return conv; +} + +/** + * 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(/\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)); + } + }); + 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 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 + }; +} + +/** + * 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 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 + }; +} + +/** + * 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) { + 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, 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 + }; +} + +/** + * 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 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; + } + + return { + "degrees": degrees, + "minutes": decMinutes, + "string": outString, + }; +} + +/** + * 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 (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]]; + } 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[0]; + } + if (split.length >= 2) { + if (split[1] !== "") { + long = split[1]; + } + } + } + } + } 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) { + 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(/^[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.]+$/), + degPattern = new RegExp(/[°'"]/g); + input = input.trim(); + if (delim !== null && delim.includes("Direction")) { + const split = input.split(/[NnEeSsWw]/); + if (split.length > 1) { + if (split[0] === "") { + testData = split[1]; + } else { + testData = split[0]; + } + } + } 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; + } + } + + // Test non-degrees formats + if (!degPattern.test(input)) { + 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 (isUtm) { + return "Universal Transverse Mercator"; + } + if (isMgrs) { + return "Military Grid Reference System"; + } + if (isOsng) { + return "Ordnance Survey National Grid"; + } + if (isGeohash) { + return "Geohash"; + } + } + + // Test DMS/DDM/DD formats + if (testData !== undefined) { + const split = splitInput(testData); + switch (split.length){ + case 3: + return "Degrees Minutes Seconds"; + case 2: + return "Degrees Decimal Minutes"; + case 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 = [",", ";", ":"]; + const testDir = input.match(/[NnEeSsWw]/g); + if (testDir !== null && testDir.length > 0 && testDir.length < 3) { + // Possibly contains a direction + const splitInput = input.split(/[NnEeSsWw]/); + if (splitInput.length <= 3 && splitInput.length > 0) { + // 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] === "") { + return "Direction Following"; + } + } + } + + // 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)) { + 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; + } + } + } + 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]; +} + +/** + * 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; +} diff --git a/src/core/operations/ConvertCoordinateFormat.mjs b/src/core/operations/ConvertCoordinateFormat.mjs new file mode 100644 index 00000000..09e1620c --- /dev/null +++ b/src/core/operations/ConvertCoordinateFormat.mjs @@ -0,0 +1,100 @@ +/** + * @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} from "../lib/ConvertCoordinates"; + +/** + * 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.

Supported formats:
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"; + this.args = [ + { + "name": "Input Format", + "type": "option", + "value": ["Auto"].concat(FORMATS) + }, + { + "name": "Input Delimiter", + "type": "option", + "value": [ + "Auto", + "Direction Preceding", + "Direction Following", + "\\n", + "Comma", + "Semi-colon", + "Colon" + ] + }, + { + "name": "Output Format", + "type": "option", + "value": FORMATS + }, + { + "name": "Output Delimiter", + "type": "option", + "value": [ + "Space", + "\\n", + "Comma", + "Semi-colon", + "Colon" + ] + }, + { + "name": "Include Compass Directions", + "type": "option", + "value": [ + "None", + "Before", + "After" + ] + }, + { + "name": "Precision", + "type": "number", + "value": 3 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + 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; + } + } +} + +export default ConvertCoordinateFormat; diff --git a/src/core/operations/FromGeohash.mjs b/src/core/operations/FromGeohash.mjs deleted file mode 100644 index da261555..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 = "Crypto"; - 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 7859bd16..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 = "Crypto"; - 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; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 0fdda9bc..fb68ed9c 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -45,7 +45,6 @@ import "./tests/DateTime"; import "./tests/ExtractEmailAddresses"; import "./tests/Fork"; import "./tests/FromDecimal"; -import "./tests/FromGeohash"; import "./tests/Hash"; import "./tests/HaversineDistance"; import "./tests/Hexdump"; @@ -77,13 +76,13 @@ import "./tests/SetUnion"; import "./tests/StrUtils"; import "./tests/SymmetricDifference"; import "./tests/TextEncodingBruteForce"; -import "./tests/ToGeohash"; import "./tests/TranslateDateTimeFormat"; import "./tests/Magic"; import "./tests/ParseTLV"; import "./tests/Media"; import "./tests/ToFromInsensitiveRegex"; import "./tests/YARA.mjs"; +import "./tests/ConvertCoordinateFormat"; // Cannot test operations that use the File type yet //import "./tests/SplitColourChannels"; diff --git a/tests/operations/tests/ConvertCoordinateFormat.mjs b/tests/operations/tests/ConvertCoordinateFormat.mjs new file mode 100644 index 00000000..1291aa4d --- /dev/null +++ b/tests/operations/tests/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/tests/operations/tests/FromGeohash.mjs b/tests/operations/tests/FromGeohash.mjs deleted file mode 100644 index 9190ea69..00000000 --- a/tests/operations/tests/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/tests/operations/tests/ToGeohash.mjs b/tests/operations/tests/ToGeohash.mjs deleted file mode 100644 index 30f7337e..00000000 --- a/tests/operations/tests/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], - }, - ], - }, -]);