diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx index 68670e4..ac1e4f5 100644 --- a/client/src/components/LiveLogView.tsx +++ b/client/src/components/LiveLogView.tsx @@ -73,6 +73,7 @@ import { type GeolocationErrorReason, type GpsSignalQuality } from '../utils/geolocation.js' +import { formatCourseAngle } from '../utils/courseAngle.js' import { sortLogEventsByTime, type LogEventPayload } from '../utils/logEntryPayload.js' import { dedupeSailNames, @@ -633,16 +634,18 @@ export default function LiveLogView({ setModal(type) } + const liveLogGpsPrefillOptions = { + timeoutMs: 8000, + enableHighAccuracy: false, + maximumAge: 60_000 + } as const + const openSogModal = async () => { let prefill = '' try { const permission = await queryGeolocationPermission() if (permission === 'granted') { - const pos = await getCurrentPosition({ - timeoutMs: 8000, - enableHighAccuracy: false, - maximumAge: 60_000 - }) + const pos = await getCurrentPosition(liveLogGpsPrefillOptions) if (pos.speedKn != null) prefill = String(pos.speedKn) } } catch { @@ -651,6 +654,20 @@ export default function LiveLogView({ openValueModal('sog', prefill) } + const openCourseModal = async () => { + let prefill = lastCourseFromEvents(events) + try { + const permission = await queryGeolocationPermission() + if (permission === 'granted') { + const pos = await getCurrentPosition(liveLogGpsPrefillOptions) + if (pos.headingDeg != null) prefill = formatCourseAngle(pos.headingDeg, true) + } + } catch { + // Fall back to last logged course or manual entry + } + openValueModal('course', prefill) + } + const handleMotorToggle = () => { hapticPulse() const starting = !motorRunning @@ -1433,7 +1450,7 @@ export default function LiveLogView({ onCastOff={handleCastOff} onMoor={handleMoor} onOpenSails={() => { setSelectedSails([]); setModal('sails') }} - onOpenCourse={() => openValueModal('course', lastCourseFromEvents(events))} + onOpenCourse={() => void openCourseModal()} onOpenSog={() => void openSogModal()} onOpenStw={() => openValueModal('stw')} onOpenFuel={() => openValueModal('fuel')} @@ -1796,6 +1813,7 @@ export default function LiveLogView({
setModal('none')}>
e.stopPropagation()}>

{t('logs.live_course_btn')}

+

{t('logs.live_course_hint')}

{ lat: '59.910000', lng: '10.750000', speedKn: 4.9, + headingDeg: null, accuracyM: 12, signalQuality: 'excellent' }) }) + it('resolves heading from getCurrentPosition when reported', async () => { + vi.stubGlobal('navigator', { + geolocation: { + getCurrentPosition: (success: PositionCallback) => { + success({ + coords: { latitude: 59.91, longitude: 10.75, heading: 245.6, accuracy: 8 } + } as GeolocationPosition) + } + } + }) + + await expect(getCurrentPosition({ timeoutMs: 1000, enableHighAccuracy: false })).resolves.toMatchObject({ + headingDeg: 246, + signalQuality: 'excellent' + }) + }) + it('formats GPS accuracy for display', () => { expect(formatGpsAccuracyMeters(12.4)).toBe('12') expect(formatGpsAccuracyMeters(87)).toBe('87') diff --git a/client/src/utils/geolocation.ts b/client/src/utils/geolocation.ts index 0709c88..6f7f616 100644 --- a/client/src/utils/geolocation.ts +++ b/client/src/utils/geolocation.ts @@ -18,6 +18,8 @@ export interface GeoCoordinates { lng: string /** SOG from GPS when available (kn), otherwise null. */ speedKn: number | null + /** COG from GPS when available (degrees, 0–359), otherwise null. */ + headingDeg: number | null /** Estimated horizontal accuracy in metres, when reported by the browser. */ accuracyM: number | null /** Derived signal quality indicator for UI hints. */ @@ -142,10 +144,16 @@ function normalizeGetPositionOptions( } } +function normalizeHeadingDeg(heading: number | null | undefined): number | null { + if (heading == null || !Number.isFinite(heading)) return null + return Math.round(((heading % 360) + 360) % 360) +} + function positionFromGeolocationPosition(pos: GeolocationPosition): GeoCoordinates { const speedKn = pos.coords.speed != null && Number.isFinite(pos.coords.speed) ? Number((pos.coords.speed * MPS_TO_KNOTS).toFixed(1)) : null + const headingDeg = normalizeHeadingDeg(pos.coords.heading) const accuracyM = pos.coords.accuracy != null && Number.isFinite(pos.coords.accuracy) ? pos.coords.accuracy : null @@ -153,6 +161,7 @@ function positionFromGeolocationPosition(pos: GeolocationPosition): GeoCoordinat lat: formatAppCoordinate(pos.coords.latitude), lng: formatAppCoordinate(pos.coords.longitude), speedKn, + headingDeg, accuracyM, signalQuality: classifyGpsAccuracyMeters(accuracyM) }