Browse Source

Merge branch 'j433866-coordinates'

n1474335 6 năm trước cách đây
mục cha
commit
6f8a5ea1be

+ 6 - 1
CHANGELOG.md

@@ -2,8 +2,11 @@
 All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master).
 
 
+### [8.24.0] - 2019-01-18
+- 'Convert co-ordinate format' operation added [@j433866] | [#476]
+
 ### [8.23.0] - 2019-01-18
-- 'YARA Rules' operatio added [@artemisbot] | [#468]
+- 'YARA Rules' operation added [@artemisbot] | [#468]
 
 ### [8.22.0] - 2019-01-10
 - 'Subsection' operation added [@j433866] | [#467]
@@ -100,6 +103,7 @@ All major and minor version changes will be documented in this file. Details of
 
 
 
+[8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0
 [8.23.0]: https://github.com/gchq/CyberChef/releases/tag/v8.23.0
 [8.22.0]: https://github.com/gchq/CyberChef/releases/tag/v8.22.0
 [8.21.0]: https://github.com/gchq/CyberChef/releases/tag/v8.21.0
@@ -181,3 +185,4 @@ All major and minor version changes will be documented in this file. Details of
 [#461]: https://github.com/gchq/CyberChef/pull/461
 [#467]: https://github.com/gchq/CyberChef/pull/467
 [#468]: https://github.com/gchq/CyberChef/pull/468
+[#476]: https://github.com/gchq/CyberChef/pull/476

+ 5 - 0
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",

+ 1 - 0
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",

+ 2 - 3
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"
         ]
     },
     {

+ 646 - 0
src/core/lib/ConvertCoordinates.mjs

@@ -0,0 +1,646 @@
+/**
+ * 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";
+import OperationError from "../errors/OperationError";
+
+/**
+ * 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,
+        hash,
+        utm,
+        mgrs,
+        osng,
+        splitLat,
+        splitLong,
+        lat,
+        lon;
+
+    // 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 new OperationError("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 new OperationError("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
+    switch (inFormat) {
+        case "Geohash":
+            hash = geohash.decode(input.replace(/[^A-Za-z0-9]/g, ""));
+            latlon = new geodesy.LatLonEllipsoidal(hash.latitude, hash.longitude);
+            break;
+        case "Military Grid Reference System":
+            utm = geodesy.Mgrs.parse(input.replace(/[^A-Za-z0-9]/g, "")).toUtm();
+            latlon = utm.toLatLonE();
+            break;
+        case "Ordnance Survey National Grid":
+            osng = geodesy.OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, ""));
+            latlon = geodesy.OsGridRef.osGridToLatLon(osng);
+            break;
+        case "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);
+            }
+            utm = geodesy.Utm.parse(input);
+            latlon = utm.toLatLonE();
+            break;
+        case "Degrees Minutes Seconds":
+            if (isPair) {
+                // Split up the lat/long into degrees / minutes / seconds values
+                splitLat = splitInput(split[0]);
+                splitLong = splitInput(split[1]);
+
+                if (splitLat.length >= 3 && splitLong.length >= 3) {
+                    lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2], 10);
+                    lon = convDMSToDD(splitLong[0], splitLong[1], splitLong[2], 10);
+                    latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
+                } else {
+                    throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
+                }
+            } else {
+                // Not a pair, so only try to convert one set of co-ordinates
+                splitLat = splitInput(split[0]);
+                if (splitLat.length >= 3) {
+                    lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2]);
+                    latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
+                } else {
+                    throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
+                }
+            }
+            break;
+        case "Degrees Decimal Minutes":
+            if (isPair) {
+                splitLat = splitInput(split[0]);
+                splitLong = splitInput(split[1]);
+                if (splitLat.length !== 2 || splitLong.length !== 2) {
+                    throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
+                }
+                // Convert to decimal degrees, and then convert to a geodesy object
+                lat = convDDMToDD(splitLat[0], splitLat[1], 10);
+                lon = convDDMToDD(splitLong[0], splitLong[1], 10);
+                latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
+            } else {
+                // Not a pair, so only try to convert one set of co-ordinates
+                splitLat = splitInput(input);
+                if (splitLat.length !== 2) {
+                    throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
+                }
+                lat = convDDMToDD(splitLat[0], splitLat[1], 10);
+                latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
+            }
+            break;
+        case "Decimal Degrees":
+            if (isPair) {
+                splitLat =  splitInput(split[0]);
+                splitLong = splitInput(split[1]);
+                if (splitLat.length !== 1 || splitLong.length !== 1) {
+                    throw new OperationError("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
+                splitLat = splitInput(split[0]);
+                if (splitLat.length !== 1) {
+                    throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
+                }
+                latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLat[0]);
+            }
+            break;
+        default:
+            throw new OperationError(`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 = -latlon.lat;
+            }
+            if (dirs.length >= 2) {
+                if (dirs[1] === "S" || dirs[1] === "W" && latlon.lon > 0) {
+                    latlon.lon = -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
+    switch (outFormat) {
+        case "Decimal Degrees":
+            // We could use the built in latlon.toString(),
+            // but this makes adjusting the output harder
+            lat = convDDToDD(latlon.lat, precision);
+            lon = convDDToDD(latlon.lon, precision);
+            convLat = lat.string;
+            convLon = lon.string;
+            break;
+        case "Degrees Decimal Minutes":
+            lat = convDDToDDM(latlon.lat, precision);
+            lon = convDDToDDM(latlon.lon, precision);
+            convLat = lat.string;
+            convLon = lon.string;
+            break;
+        case "Degrees Minutes Seconds":
+            lat = convDDToDMS(latlon.lat, precision);
+            lon = convDDToDMS(latlon.lon, precision);
+            convLat = lat.string;
+            convLon = lon.string;
+            break;
+        case "Geohash":
+            convLat = geohash.encode(latlon.lat, latlon.lon, precision);
+            break;
+        case "Military Grid Reference System":
+            utm = latlon.toUtm();
+            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);
+            break;
+        case "Ordnance Survey National Grid":
+            osng = geodesy.OsGridRef.latLonToOsGrid(latlon);
+            if (osng.toString() === "") {
+                throw new OperationError("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);
+            break;
+        case "Universal Transverse Mercator":
+            utm = latlon.toUtm();
+            convLat = utm.toString(precision);
+            break;
+    }
+
+    if (convLat === undefined) {
+        throw new OperationError("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) {
+            return dirs.length === 2 ? [dirs[0], dirs[1]] : [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 && split[1] !== "") {
+                    long = split[1];
+                }
+            }
+        }
+    } else {
+        const split = upperInput.split(dirExp);
+        if (split.length > 1) {
+            lat = split[0] === "" ? split[1] : split[0];
+            if (split.length > 2 && split[2] !== "") {
+                long = split[2];
+            }
+        }
+    }
+
+    if (lat) {
+        lat = parseFloat(lat);
+        latDir = lat < 0 ? "S" : "N";
+    }
+
+    if (long) {
+        long = parseFloat(long);
+        longDir = long < 0 ? "W" : "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) {
+            testData = split[0] === "" ? split[1] : split[0];
+        }
+    } else if (delim !== null && delim !== "") {
+        if (input.includes(delim)) {
+            const split = input.split(delim);
+            if (split.length > 1) {
+                testData = split[0] === "" ? split[1] : split[0];
+            }
+        } else {
+            testData = input;
+        }
+    }
+
+    // Test non-degrees formats
+    if (!degPattern.test(input)) {
+        const filteredInput = input.toUpperCase().replace(delim, "");
+
+        if (utmPattern.test(filteredInput)) {
+            return "Universal Transverse Mercator";
+        }
+        if (mgrsPattern.test(filteredInput)) {
+            return "Military Grid Reference System";
+        }
+        if (osngPattern.test(filteredInput)) {
+            return "Ordnance Survey National Grid";
+        }
+        if (geohashPattern.test(filteredInput)) {
+            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
+ * @returns {boolean}
+ */
+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;
+}

+ 95 - 0
src/core/operations/ConvertCoordinateFormat.mjs

@@ -0,0 +1,95 @@
+/**
+ * @author j433866 [j433866@gmail.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+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 = "Converts geographical coordinates between different formats.<br><br>Supported formats:<ul><li>Degrees Minutes Seconds (DMS)</li><li>Degrees Decimal Minutes (DDM)</li><li>Decimal Degrees (DD)</li><li>Geohash</li><li>Military Grid Reference System (MGRS)</li><li>Ordnance Survey National Grid (OSNG)</li><li>Universal Transverse Mercator (UTM)</li></ul><br>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;
+            const result = convertCoordinates(input, inFormat, inDelim, outFormat, outDelim, incDirection, precision);
+            return result;
+        } else {
+            return input;
+        }
+    }
+}
+
+export default ConvertCoordinateFormat;

+ 0 - 44
src/core/operations/FromGeohash.mjs

@@ -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, <code>ww8p1r4t8</code> becomes <code>37.8324,112.5584</code>.";
-        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;

+ 0 - 53
src/core/operations/ToGeohash.mjs

@@ -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, <code>37.8324,112.5584</code> becomes <code>ww8p1r4t8</code>.";
-        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;

+ 1 - 2
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";

+ 211 - 0
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]
+            },
+        ],
+    }
+]);

+ 0 - 55
tests/operations/tests/FromGeohash.mjs

@@ -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: [],
-            },
-        ],
-    },
-]);

+ 0 - 55
tests/operations/tests/ToGeohash.mjs

@@ -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],
-            },
-        ],
-    },
-]);