소스 검색

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.

j433866 6 년 전
부모
커밋
4bd923dc06
1개의 변경된 파일215개의 추가작업 그리고 143개의 파일을 삭제
  1. 215 143
      src/core/lib/ConvertCoordinates.mjs

+ 215 - 143
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
-    if (outFormat === "Decimal Degrees") {
-        conv = latlon.toString("d", precision);
-        if (!isPair) {
-            conv = conv.split(",")[0];
+    // 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") {
-        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;
-        }
-        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;
-                    }
-                }
+        // 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 + " ";
             }
-            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 converted = new Object();
-    converted.degrees = degrees;
-    converted.minutes = decMinutes;
-    converted.string = degrees + "° " + decMinutes + "'";
-    return converted;
+    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,
+    };
 }
 
 /**
@@ -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;
+}