119 lines
4.2 KiB
JavaScript
119 lines
4.2 KiB
JavaScript
'use strict';
|
||
|
||
const moment = require('moment-timezone');
|
||
const tzLookup = require('tz-lookup');
|
||
|
||
function isFiniteNumber(value) {
|
||
return typeof value === 'number' && Number.isFinite(value);
|
||
}
|
||
|
||
function parseCompactDateTime(dateTimeString) {
|
||
const match = /^([0-9]{8})T([0-9]{6})$/.exec(dateTimeString || '');
|
||
if (!match) return null;
|
||
|
||
return {
|
||
datePart: match[1],
|
||
timePart: match[2]
|
||
};
|
||
}
|
||
|
||
function resolveTimezoneName(lat, lon) {
|
||
if (!isFiniteNumber(lat) || !isFiniteNumber(lon)) return 'UTC';
|
||
|
||
try {
|
||
return tzLookup(lat, lon) || 'UTC';
|
||
} catch (err) {
|
||
return 'UTC';
|
||
}
|
||
}
|
||
|
||
function getUtcOffsetMinutesFromLocation(lat, lon, dateLabel) {
|
||
const timezoneName = resolveTimezoneName(lat, lon);
|
||
if (timezoneName === 'UTC') return 0;
|
||
|
||
const referenceDate = dateLabel
|
||
? moment.utc(dateLabel, ['YYYYMMDD', 'YYYY-MM-DD'], true).add(12, 'hours')
|
||
: moment.utc();
|
||
|
||
if (!referenceDate.isValid()) return 0;
|
||
|
||
return moment.tz(referenceDate.toDate(), timezoneName).utcOffset();
|
||
}
|
||
|
||
function getDateLabelFromDateTime(dateTimeString) {
|
||
if (!dateTimeString) return null;
|
||
|
||
const compact = parseCompactDateTime(dateTimeString);
|
||
if (compact) return compact.datePart;
|
||
|
||
const isoMoment = moment.utc(dateTimeString, moment.ISO_8601, true);
|
||
if (isoMoment.isValid()) return isoMoment.format('YYYYMMDD');
|
||
|
||
return null;
|
||
}
|
||
|
||
function toUtcDateFromAppDateTime(dateTimeString, utcOffsetMinutes = 0) {
|
||
if (!dateTimeString) return null;
|
||
|
||
const compact = parseCompactDateTime(dateTimeString);
|
||
if (!compact) {
|
||
const isoMoment = moment.utc(dateTimeString, moment.ISO_8601, true);
|
||
if (isoMoment.isValid()) return isoMoment.toDate();
|
||
return null;
|
||
}
|
||
|
||
const hours = Number(compact.timePart.slice(0, 2));
|
||
const minutes = Number(compact.timePart.slice(2, 4));
|
||
const seconds = Number(compact.timePart.slice(4, 6));
|
||
const utcSecondsOfDay = (hours * 3600) + (minutes * 60) + seconds;
|
||
|
||
// AgNav hybrid format:
|
||
// datePart = LOCAL mission date assigned by the device (not derived from GPS)
|
||
// timePart = GPS UTC time-of-day (already UTC, no conversion needed)
|
||
//
|
||
// timePart is placed directly onto a UTC date. The only question is: which UTC
|
||
// calendar date? Compute how timePart translates to local time; if the result
|
||
// crosses midnight the UTC date must be shifted by ±1 day relative to datePart:
|
||
//
|
||
// localSecondsOfDay = utcSecondsOfDay + offsetSeconds
|
||
// < 0 → local rolled back to previous day → UTC date = datePart − 1 day
|
||
// ≥ 86400 → local rolled forward to next day → UTC date = datePart + 1 day
|
||
// otherwise→ UTC and local calendar dates align → no shift
|
||
const offsetSeconds = utcOffsetMinutes * 60;
|
||
const localSecondsOfDay = utcSecondsOfDay + offsetSeconds;
|
||
|
||
let dateShift = 0;
|
||
if (localSecondsOfDay < 0) dateShift = -1; // local rolled back → UTC one day earlier
|
||
if (localSecondsOfDay >= 86400) dateShift = +1; // local rolled forward → UTC one day later
|
||
|
||
return moment.utc(compact.datePart, 'YYYYMMDD', true)
|
||
.add(dateShift, 'days')
|
||
.add(utcSecondsOfDay, 'seconds')
|
||
.toDate();
|
||
}
|
||
|
||
function buildApplicationDateFields({ startDateTime, endDateTime, latitude, longitude }) {
|
||
const dateLabel = getDateLabelFromDateTime(startDateTime || endDateTime);
|
||
const utcOffset = getUtcOffsetMinutesFromLocation(latitude, longitude, dateLabel);
|
||
|
||
let startDateTimeUTC = toUtcDateFromAppDateTime(startDateTime, utcOffset);
|
||
let endDateTimeUTC = toUtcDateFromAppDateTime(endDateTime, utcOffset);
|
||
|
||
// Sanity guard: startDateTimeUTC must never exceed endDateTimeUTC.
|
||
// With the correct dateShift logic above this should not occur for valid data.
|
||
// Kept as a safety net for genuinely corrupt input (e.g. file contains timestamps
|
||
// from two different days with the start record from a later day than the end record).
|
||
if (startDateTimeUTC && endDateTimeUTC && startDateTimeUTC > endDateTimeUTC) {
|
||
startDateTimeUTC = new Date(startDateTimeUTC.getTime() - 86400000);
|
||
}
|
||
|
||
return { utcOffset, startDateTimeUTC, endDateTimeUTC };
|
||
}
|
||
|
||
module.exports = {
|
||
resolveTimezoneName,
|
||
getUtcOffsetMinutesFromLocation,
|
||
getDateLabelFromDateTime,
|
||
toUtcDateFromAppDateTime,
|
||
buildApplicationDateFields
|
||
}; |