template-project/node_modules/exiftool-vendored/dist/CoordinateParser.js
2025-05-30 18:13:30 +08:00

286 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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