'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 };