|
@@ -0,0 +1,655 @@
|
|
|
+/**
|
|
|
+ * 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 if (!inDelim.includes("Direction")) {
|
|
|
+ // 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)) {
|
|
|
+ if (inDelim.includes("Direction")) {
|
|
|
+ // Split on directions
|
|
|
+ split = input.split(/[NnEeSsWw]/g);
|
|
|
+ if (split[0] === "") {
|
|
|
+ // Remove first element if direction preceding
|
|
|
+ split = split.slice(1);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ 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.toUpperCase().match(/[NESW]/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;
|
|
|
+}
|