3cab735754
Updated various components to utilize parseAppDecimal and formatAppDecimal for consistent decimal parsing and formatting. This change enhances the handling of numeric inputs across the application, ensuring better accuracy and user experience in forms and displays.
217 lines
7.6 KiB
TypeScript
217 lines
7.6 KiB
TypeScript
import { formatAppDecimal } from '../../utils/numberFormat.js'
|
|
import type { NmeaChangeEvent, NmeaDetectionConfig, NmeaTimePoint } from './nmeaTypes.js'
|
|
import { DEFAULT_NMEA_DETECTION_CONFIG } from './nmeaTypes.js'
|
|
import { angularDelta } from './nmeaTimeSeries.js'
|
|
|
|
function formatNmeaDecimal(value: number): string {
|
|
return formatAppDecimal(value, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
|
|
}
|
|
|
|
function pushUnique(events: NmeaChangeEvent[], event: NmeaChangeEvent, minGapMs: number) {
|
|
const last = events[events.length - 1]
|
|
if (last && last.type === event.type && event.timestamp - last.timestamp < minGapMs) return
|
|
events.push(event)
|
|
}
|
|
|
|
export function detectNmeaChanges(
|
|
points: NmeaTimePoint[],
|
|
config: NmeaDetectionConfig = DEFAULT_NMEA_DETECTION_CONFIG
|
|
): NmeaChangeEvent[] {
|
|
const events: NmeaChangeEvent[] = []
|
|
if (points.length < 2) return events
|
|
|
|
let lastCourse: number | undefined
|
|
let lastWindDir: number | undefined
|
|
let lastWindSpeed: number | undefined
|
|
let lastPressure: number | undefined
|
|
let lastDepth: number | undefined
|
|
let lastWaterTemp: number | undefined
|
|
let lastFix: boolean | undefined
|
|
let engineRunning = false
|
|
let autopilot: boolean | undefined
|
|
let underWay = false
|
|
let stoppedSince: number | null = null
|
|
let lastSog: number | undefined
|
|
|
|
for (const p of points) {
|
|
const course = p.cog ?? p.hdt ?? p.hdm
|
|
if (course != null && lastCourse != null && (p.sog ?? 0) > 1) {
|
|
if (angularDelta(course, lastCourse) >= config.courseDeltaDeg) {
|
|
pushUnique(events, {
|
|
type: 'course',
|
|
timestamp: p.timestamp,
|
|
confidence: 'high',
|
|
summaryKey: 'logs.nmea_change_course',
|
|
summaryParams: { from: Math.round(lastCourse), to: Math.round(course) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
}
|
|
if (course != null) lastCourse = course
|
|
|
|
if (p.windDir != null && lastWindDir != null) {
|
|
if (angularDelta(p.windDir, lastWindDir) >= config.windDirDeltaDeg) {
|
|
pushUnique(events, {
|
|
type: 'wind',
|
|
timestamp: p.timestamp,
|
|
confidence: 'high',
|
|
summaryKey: 'logs.nmea_change_wind',
|
|
summaryParams: { from: Math.round(lastWindDir), to: Math.round(p.windDir) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
} else if (
|
|
p.windSpeedKnots != null &&
|
|
lastWindSpeed != null &&
|
|
Math.abs(p.windSpeedKnots - lastWindSpeed) >= config.windSpeedDeltaKnots
|
|
) {
|
|
pushUnique(events, {
|
|
type: 'wind',
|
|
timestamp: p.timestamp,
|
|
confidence: 'medium',
|
|
summaryKey: 'logs.nmea_change_wind_speed',
|
|
summaryParams: { from: formatNmeaDecimal(lastWindSpeed), to: formatNmeaDecimal(p.windSpeedKnots) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
}
|
|
if (p.windDir != null) lastWindDir = p.windDir
|
|
if (p.windSpeedKnots != null) lastWindSpeed = p.windSpeedKnots
|
|
|
|
if (p.pressureHpa != null && lastPressure != null) {
|
|
if (Math.abs(p.pressureHpa - lastPressure) >= config.pressureDeltaHpa) {
|
|
pushUnique(events, {
|
|
type: 'pressure',
|
|
timestamp: p.timestamp,
|
|
confidence: 'medium',
|
|
summaryKey: 'logs.nmea_change_pressure',
|
|
summaryParams: { from: formatNmeaDecimal(lastPressure), to: formatNmeaDecimal(p.pressureHpa) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
}
|
|
if (p.pressureHpa != null) lastPressure = p.pressureHpa
|
|
|
|
if (p.depthM != null && lastDepth != null) {
|
|
const delta = Math.abs(p.depthM - lastDepth)
|
|
const rel = lastDepth > 0 ? (delta / lastDepth) * 100 : 100
|
|
if (delta >= config.depthDeltaM || rel >= config.depthDeltaPercent) {
|
|
pushUnique(events, {
|
|
type: 'depth',
|
|
timestamp: p.timestamp,
|
|
confidence: 'high',
|
|
summaryKey: 'logs.nmea_change_depth',
|
|
summaryParams: { from: formatNmeaDecimal(lastDepth), to: formatNmeaDecimal(p.depthM) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
}
|
|
if (p.depthM != null) lastDepth = p.depthM
|
|
|
|
if (p.rpm != null) {
|
|
const running = p.rpm >= config.rpmRunning
|
|
const idle = p.rpm <= config.rpmIdle
|
|
if (running && !engineRunning) {
|
|
pushUnique(events, {
|
|
type: 'engine_start',
|
|
timestamp: p.timestamp,
|
|
confidence: 'high',
|
|
summaryKey: 'logs.nmea_change_engine_start',
|
|
summaryParams: { rpm: Math.round(p.rpm) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
engineRunning = true
|
|
} else if (idle && engineRunning) {
|
|
pushUnique(events, {
|
|
type: 'engine_stop',
|
|
timestamp: p.timestamp,
|
|
confidence: 'high',
|
|
summaryKey: 'logs.nmea_change_engine_stop',
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
engineRunning = false
|
|
}
|
|
}
|
|
|
|
if (p.autopilotEngaged != null && autopilot != null && p.autopilotEngaged !== autopilot) {
|
|
pushUnique(events, {
|
|
type: p.autopilotEngaged ? 'autopilot_on' : 'autopilot_off',
|
|
timestamp: p.timestamp,
|
|
confidence: 'high',
|
|
summaryKey: p.autopilotEngaged ? 'logs.nmea_change_autopilot_on' : 'logs.nmea_change_autopilot_off',
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
if (p.autopilotEngaged != null) autopilot = p.autopilotEngaged
|
|
|
|
if (p.fixValid != null && lastFix != null && p.fixValid !== lastFix) {
|
|
pushUnique(events, {
|
|
type: p.fixValid ? 'gps_fix_regained' : 'gps_fix_lost',
|
|
timestamp: p.timestamp,
|
|
confidence: 'high',
|
|
summaryKey: p.fixValid ? 'logs.nmea_change_gps_regained' : 'logs.nmea_change_gps_lost',
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
if (p.fixValid != null) lastFix = p.fixValid
|
|
|
|
if (p.waterTempC != null && lastWaterTemp != null) {
|
|
if (Math.abs(p.waterTempC - lastWaterTemp) >= 2) {
|
|
pushUnique(events, {
|
|
type: 'water_temp',
|
|
timestamp: p.timestamp,
|
|
confidence: 'medium',
|
|
summaryKey: 'logs.nmea_change_water_temp',
|
|
summaryParams: { from: formatNmeaDecimal(lastWaterTemp), to: formatNmeaDecimal(p.waterTempC) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
}
|
|
if (p.waterTempC != null) lastWaterTemp = p.waterTempC
|
|
|
|
const sog = p.sog ?? 0
|
|
if (sog >= config.sogUnderWayKn && !underWay) {
|
|
if (stoppedSince != null && p.timestamp - stoppedSince >= config.anchorMinutes * 60 * 1000) {
|
|
pushUnique(events, {
|
|
type: 'departure',
|
|
timestamp: p.timestamp,
|
|
confidence: 'medium',
|
|
summaryKey: 'logs.nmea_change_departure',
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
underWay = true
|
|
stoppedSince = null
|
|
}
|
|
if (sog <= config.sogStoppedKn && underWay) {
|
|
underWay = false
|
|
stoppedSince = p.timestamp
|
|
}
|
|
if (sog <= config.sogStoppedKn && stoppedSince != null && !underWay) {
|
|
if (p.timestamp - stoppedSince >= config.anchorMinutes * 60 * 1000) {
|
|
pushUnique(events, {
|
|
type: 'anchor',
|
|
timestamp: p.timestamp,
|
|
confidence: 'medium',
|
|
summaryKey: 'logs.nmea_change_anchor',
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
stoppedSince = null
|
|
}
|
|
}
|
|
|
|
if (lastSog != null && Math.abs(sog - lastSog) >= config.speedDeltaKn) {
|
|
pushUnique(events, {
|
|
type: 'speed',
|
|
timestamp: p.timestamp,
|
|
confidence: 'low',
|
|
summaryKey: 'logs.nmea_change_speed',
|
|
summaryParams: { from: formatNmeaDecimal(lastSog), to: formatNmeaDecimal(sog) },
|
|
data: p
|
|
}, config.dedupeWindowMs)
|
|
}
|
|
lastSog = sog
|
|
}
|
|
|
|
return events.sort((a, b) => a.timestamp - b.timestamp)
|
|
}
|