286 lines
11 KiB
JavaScript
286 lines
11 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.parseCoordinates = parseCoordinates;
|
||
exports.parseDecimalCoordinate = parseDecimalCoordinate;
|
||
exports.parseCoordinate = parseCoordinate;
|
||
exports.roundGpsDecimal = roundGpsDecimal;
|
||
exports.parsePosition = parsePosition;
|
||
exports.processCoordinate = processCoordinate;
|
||
const Number_1 = require("./Number");
|
||
const String_1 = require("./String");
|
||
// Constants
|
||
const MAX_LATITUDE_DEGREES = 90;
|
||
const MAX_LONGITUDE_DEGREES = 180;
|
||
class CoordinateParseError extends Error {
|
||
constructor(message) {
|
||
super(message);
|
||
this.name = "CoordinateParseError";
|
||
}
|
||
}
|
||
// Regex to match simple decimal coordinates, like "37.5, -122.5"
|
||
const DecimalCoordsRE = /^(-?\d+(?:\.\d+)?)[,\s]+(-?\d+(?:\.\d+)?)$/;
|
||
/**
|
||
* Parses a string containing both latitude and longitude coordinates.
|
||
* @param input - String containing both coordinates
|
||
* @returns Object containing latitude and longitude in decimal degrees, or undefined if parsing fails
|
||
* @throws CoordinateParseError if the input format is invalid
|
||
*/
|
||
function parseCoordinates(input) {
|
||
input = (0, String_1.toS)(input).trim();
|
||
if (input.length === 0) {
|
||
throw new CoordinateParseError("Input string cannot be empty");
|
||
}
|
||
if (DecimalCoordsRE.test(input)) {
|
||
const split = input.split(/[\s,]+/);
|
||
const [latitude, longitude] = split
|
||
.map(Number_1.toFloat)
|
||
.map((ea) => (ea == null ? null : roundGpsDecimal(ea)));
|
||
if (latitude == null || longitude == null) {
|
||
throw new CoordinateParseError("Failed to parse decimal coordinates");
|
||
}
|
||
return { latitude, longitude };
|
||
}
|
||
let latitude;
|
||
let longitude;
|
||
for (const coord of parseStringCoordinates(input)) {
|
||
if (!coord.direction) {
|
||
throw new CoordinateParseError("Direction is required for position parsing");
|
||
}
|
||
if (coord.direction === "S" || coord.direction === "N") {
|
||
if (latitude !== undefined) {
|
||
throw new CoordinateParseError("Multiple latitude values found");
|
||
}
|
||
latitude = toDecimalDegrees(coord);
|
||
}
|
||
else {
|
||
if (longitude != null) {
|
||
throw new CoordinateParseError("Multiple longitude values found");
|
||
}
|
||
longitude = toDecimalDegrees(coord);
|
||
}
|
||
}
|
||
const missing = [];
|
||
if (latitude == null)
|
||
missing.push("latitude");
|
||
if (longitude == null)
|
||
missing.push("longitude");
|
||
if (latitude == null || longitude == null) {
|
||
throw new CoordinateParseError(`Missing ${missing.join(" and ")}`);
|
||
}
|
||
else {
|
||
return { latitude, longitude };
|
||
}
|
||
}
|
||
/**
|
||
* Parses a string containing one or more coordinates.
|
||
* @param input - String containing coordinates
|
||
* @returns Array of parsed coordinates
|
||
*/
|
||
function parseStringCoordinates(input) {
|
||
if (!input?.trim()) {
|
||
throw new CoordinateParseError("Input string cannot be empty");
|
||
}
|
||
const lat = parseCoordinate(input, true);
|
||
const remainders = lat.remainder;
|
||
if ((0, String_1.blank)(remainders)) {
|
||
throw new CoordinateParseError("Expected multiple coordinates");
|
||
}
|
||
return [lat, parseCoordinate(remainders)];
|
||
}
|
||
/**
|
||
* Parses a coordinate string in decimal degrees format.
|
||
* @param input - String containing a single coordinate
|
||
* @returns Object containing degrees and direction, or undefined if parsing fails
|
||
* @throws CoordinateParseError if the format is not decimal degrees or direction is missing
|
||
*/
|
||
function parseDecimalCoordinate(input) {
|
||
if (!input?.trim()) {
|
||
throw new CoordinateParseError("Input string cannot be empty");
|
||
}
|
||
const coord = parseCoordinate(input);
|
||
if (coord.format !== "D") {
|
||
throw new CoordinateParseError("Expected decimal degrees format");
|
||
}
|
||
if (!coord.direction) {
|
||
throw new CoordinateParseError("Missing direction");
|
||
}
|
||
return { decimal: toDecimalDegrees(coord), direction: coord.direction };
|
||
}
|
||
const DecimalCoordRE = /^(-?\d+(?:\.\d+)?)$/;
|
||
/**
|
||
* Parses a single coordinate string into its components.
|
||
* @param input - String containing a single coordinate
|
||
* @param expectRemainders - If true, allow additional text after the coordinate
|
||
* @returns Parsed coordinate object
|
||
* @throws CoordinateParseError if the format is invalid
|
||
*/
|
||
function parseCoordinate(input, expectRemainders = false) {
|
||
input = (0, String_1.toS)(input).trim();
|
||
if (input.length === 0) {
|
||
throw new CoordinateParseError("Input string cannot be empty");
|
||
}
|
||
if (DecimalCoordRE.test(input)) {
|
||
const f = (0, Number_1.toFloat)(input);
|
||
if (f == null) {
|
||
throw new CoordinateParseError("Failed to parse decimal coordinate");
|
||
}
|
||
const r = roundGpsDecimal(f);
|
||
return {
|
||
degrees: r,
|
||
decimal: r,
|
||
format: "D",
|
||
direction: undefined,
|
||
minutes: undefined,
|
||
seconds: undefined,
|
||
remainder: "",
|
||
};
|
||
}
|
||
const dmsPattern = /^(?<degrees>-?\d+)\s*(?:°|DEG)\s*(?<minutes>\d+)\s*['′]\s*(?<seconds>\d+(?:\.\d+)?)\s*["″]\s?(?<direction>[NSEW])?[\s,]{0,3}(?<remainder>.*)$/i;
|
||
const dmPattern = /^(?<degrees>-?\d+)\s*(?:°|DEG)\s*(?<minutes>\d+(?:\.\d+)?)\s?['′]\s?(?<direction>[NSEW])?(?<remainder>.*)$/i;
|
||
const dPattern = /^(?<degrees>-?\d+(?:\.\d+)?)\s*(?:°|DEG)\s?(?<direction>[NSEW])?(?<remainder>.*)$/i;
|
||
const trimmedInput = input.trimStart();
|
||
let match;
|
||
let format = null;
|
||
if ((match = trimmedInput.match(dmsPattern))) {
|
||
format = "DMS";
|
||
}
|
||
else if ((match = trimmedInput.match(dmPattern))) {
|
||
format = "DM";
|
||
}
|
||
else if ((match = trimmedInput.match(dPattern))) {
|
||
format = "D";
|
||
}
|
||
if (match == null ||
|
||
format == null ||
|
||
(!expectRemainders && !(0, String_1.blank)(match?.groups?.remainder))) {
|
||
throw new CoordinateParseError("Invalid coordinate format. Expected one of:\n" +
|
||
" DDD° MM' SS.S\" k (deg/min/sec)\n" +
|
||
" DDD° MM.MMM' k (deg/decimal minutes)\n" +
|
||
" DDD.DDDDD° (decimal degrees)\n" +
|
||
" (where k indicates direction: N, S, E, or W)");
|
||
}
|
||
if (!match.groups) {
|
||
throw new CoordinateParseError("Failed to parse coordinate components");
|
||
}
|
||
const { degrees: degreesStr, minutes: minutesStr, seconds: secondsStr, direction: directionStr, remainder, } = match.groups;
|
||
const direction = directionStr?.toUpperCase();
|
||
const degrees = parseFloat(degreesStr);
|
||
let minutes;
|
||
let seconds;
|
||
if (format === "DMS") {
|
||
minutes = parseInt(minutesStr, 10);
|
||
seconds = parseFloat(secondsStr);
|
||
if (minutes >= 60) {
|
||
throw new CoordinateParseError("Minutes must be between 0 and 59");
|
||
}
|
||
if (seconds >= 60) {
|
||
throw new CoordinateParseError("Seconds must be between 0 and 59.999...");
|
||
}
|
||
}
|
||
else if (format === "DM") {
|
||
minutes = parseFloat(minutesStr);
|
||
if (minutes >= 60) {
|
||
throw new CoordinateParseError("Minutes must be between 0 and 59.999...");
|
||
}
|
||
}
|
||
const maxDegrees = direction === "N" || direction === "S"
|
||
? MAX_LATITUDE_DEGREES
|
||
: MAX_LONGITUDE_DEGREES;
|
||
if (Math.abs(degrees) > maxDegrees) {
|
||
throw new CoordinateParseError(`Degrees must be between -${maxDegrees} and ${maxDegrees} for ${direction} direction`);
|
||
}
|
||
const coords = {
|
||
degrees,
|
||
minutes,
|
||
seconds,
|
||
direction,
|
||
format,
|
||
remainder: remainder?.trim(),
|
||
};
|
||
const decimal = toDecimalDegrees(coords);
|
||
return {
|
||
...coords,
|
||
decimal,
|
||
};
|
||
}
|
||
function toDecimalDegrees(coord) {
|
||
const degrees = (0, Number_1.toFloat)(coord.degrees) ?? 0;
|
||
const sign = Math.sign(degrees);
|
||
let decimal = Math.abs(degrees);
|
||
decimal += Math.abs((0, Number_1.toFloat)(coord.minutes) ?? 0) / 60.0;
|
||
decimal += Math.abs((0, Number_1.toFloat)(coord.seconds) ?? 0) / 3600.0;
|
||
if (coord.direction === "S" || coord.direction === "W" || sign < 0) {
|
||
decimal = -decimal;
|
||
}
|
||
const maxDegrees = coord.direction === "N" || coord.direction === "S"
|
||
? MAX_LATITUDE_DEGREES
|
||
: MAX_LONGITUDE_DEGREES;
|
||
const axis = coord.direction === "N" || coord.direction === "S"
|
||
? "latitude"
|
||
: "longitude";
|
||
if (Math.abs(decimal) > maxDegrees) {
|
||
throw new CoordinateParseError(`Degrees must be between -${maxDegrees} and ${maxDegrees} for ${axis}`);
|
||
}
|
||
// Round to 6 decimal places
|
||
// Most consumer devices can only resolve 4-5 decimal places (1m resolution)
|
||
return roundGpsDecimal(decimal);
|
||
}
|
||
const MAX_LAT_LON_DIFF = 1;
|
||
function roundGpsDecimal(decimal) {
|
||
return (0, Number_1.roundToDecimalPlaces)(decimal, 6);
|
||
}
|
||
function parsePosition(position) {
|
||
if ((0, String_1.blank)(position))
|
||
return;
|
||
const [lat, lon] = (0, String_1.toS)(position).split(/[, ]+/).map(Number_1.toFloat);
|
||
return lat != null && lon != null ? [lat, lon] : undefined;
|
||
}
|
||
function processCoordinate(config, warnings) {
|
||
let { value, ref } = config;
|
||
const { geoValue, coordinateType } = config;
|
||
const { expectedRefPositive, expectedRefNegative, max } = config;
|
||
let isInvalid = false;
|
||
// Validate ref is reasonable -- it should either start with
|
||
// expectedRefPositive or expectedRefNegative:
|
||
ref = (0, String_1.toS)(ref).trim().toUpperCase().slice(0, 1);
|
||
if (!(0, String_1.blank)(config.ref) &&
|
||
ref !== expectedRefPositive &&
|
||
ref !== expectedRefNegative) {
|
||
warnings.push(`Invalid GPS${coordinateType}Ref: ${JSON.stringify(config.ref)}.`);
|
||
ref = value < 0 ? expectedRefNegative : expectedRefPositive;
|
||
}
|
||
// Check range
|
||
if (Math.abs(value) > max) {
|
||
isInvalid = true;
|
||
warnings.push(`Invalid GPS${coordinateType}: ${value} is out of range`);
|
||
return { value, ref, isInvalid };
|
||
}
|
||
// Apply hemisphere reference
|
||
if (ref === expectedRefNegative) {
|
||
value = -Math.abs(value);
|
||
}
|
||
// Check for mismatched signs with GeolocationPosition
|
||
if (geoValue != null &&
|
||
Math.abs(Math.abs(geoValue) - Math.abs(value)) < MAX_LAT_LON_DIFF) {
|
||
if (Math.sign(geoValue) !== Math.sign(value)) {
|
||
value = -value;
|
||
warnings.push(`Corrected GPS${coordinateType} sign based on GeolocationPosition`);
|
||
}
|
||
// Force ref to correct value
|
||
const expectedRef = geoValue < 0 ? expectedRefNegative : expectedRefPositive;
|
||
if (ref !== expectedRef) {
|
||
ref = expectedRef;
|
||
if (!(0, String_1.blank)(config.ref)) {
|
||
warnings.push(`Corrected GPS${coordinateType}Ref to ${expectedRef} based on GeolocationPosition`);
|
||
}
|
||
}
|
||
}
|
||
// Ensure ref matches coordinate sign
|
||
const expectedRef = value < 0 ? expectedRefNegative : expectedRefPositive;
|
||
if (ref != null && ref !== expectedRef && !(0, String_1.blank)(config.ref)) {
|
||
warnings.push(`Corrected GPS${coordinateType}Ref to ${ref} to match coordinate sign`);
|
||
}
|
||
ref = expectedRef;
|
||
return { value: roundGpsDecimal(value), ref, isInvalid };
|
||
}
|
||
//# sourceMappingURL=CoordinateParser.js.map
|