feat: GPS-Track-Statistiken automatisch ins Logbuch übernehmen.
Strecke, Max- und Durchschnittsgeschwindigkeit werden beim Track-Upload berechnet, gespeichert und in PDF/CSV exportiert. Test-GPX für die Kieler Förde (5 sm) hinzugefügt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1980,6 +1980,11 @@ body:has(.theme-cupertino) {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-stats-grid {
|
||||||
|
margin-top: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
.signature-grid {
|
.signature-grid {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
parseTrackFile,
|
parseTrackFile,
|
||||||
type SavedTrack
|
type SavedTrack
|
||||||
} from '../services/trackUpload.js'
|
} from '../services/trackUpload.js'
|
||||||
|
import { computeTrackStats, formatTrackStats } from '../utils/trackStats.js'
|
||||||
|
|
||||||
interface LogEntryEditorProps {
|
interface LogEntryEditorProps {
|
||||||
entryId: string
|
entryId: string
|
||||||
@@ -86,6 +87,11 @@ export default function LogEntryEditor({
|
|||||||
const [signSkipper, setSignSkipper] = useState('')
|
const [signSkipper, setSignSkipper] = useState('')
|
||||||
const [signCrew, setSignCrew] = useState('')
|
const [signCrew, setSignCrew] = useState('')
|
||||||
|
|
||||||
|
// GPS track stats (from uploaded track)
|
||||||
|
const [trackDistanceNm, setTrackDistanceNm] = useState('')
|
||||||
|
const [trackSpeedMaxKn, setTrackSpeedMaxKn] = useState('')
|
||||||
|
const [trackSpeedAvgKn, setTrackSpeedAvgKn] = useState('')
|
||||||
|
|
||||||
// Events list state
|
// Events list state
|
||||||
const [events, setEvents] = useState<LogEvent[]>([])
|
const [events, setEvents] = useState<LogEvent[]>([])
|
||||||
|
|
||||||
@@ -121,6 +127,27 @@ export default function LogEntryEditor({
|
|||||||
const [uploadError, setUploadError] = useState<string | null>(null)
|
const [uploadError, setUploadError] = useState<string | null>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const applyTrackStats = (waypoints: SavedTrack['waypoints']) => {
|
||||||
|
const stats = computeTrackStats(waypoints)
|
||||||
|
if (!stats) return
|
||||||
|
const formatted = formatTrackStats(stats)
|
||||||
|
setTrackDistanceNm(formatted.distanceNm)
|
||||||
|
setTrackSpeedMaxKn(formatted.speedMaxKn)
|
||||||
|
setTrackSpeedAvgKn(formatted.speedAvgKn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTrackStatsFromEntry = (entry: any) => {
|
||||||
|
if (entry?.trackDistanceNm != null && entry.trackDistanceNm !== '') {
|
||||||
|
setTrackDistanceNm(String(entry.trackDistanceNm))
|
||||||
|
}
|
||||||
|
if (entry?.trackSpeedMaxKn != null && entry.trackSpeedMaxKn !== '') {
|
||||||
|
setTrackSpeedMaxKn(String(entry.trackSpeedMaxKn))
|
||||||
|
}
|
||||||
|
if (entry?.trackSpeedAvgKn != null && entry.trackSpeedAvgKn !== '') {
|
||||||
|
setTrackSpeedAvgKn(String(entry.trackSpeedAvgKn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-calculate Freshwater Consumption
|
// Auto-calculate Freshwater Consumption
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const morning = parseFloat(fwMorning) || 0
|
const morning = parseFloat(fwMorning) || 0
|
||||||
@@ -189,6 +216,7 @@ export default function LogEntryEditor({
|
|||||||
|
|
||||||
setSignSkipper(preloadedEntry.signSkipper || '')
|
setSignSkipper(preloadedEntry.signSkipper || '')
|
||||||
setSignCrew(preloadedEntry.signCrew || '')
|
setSignCrew(preloadedEntry.signCrew || '')
|
||||||
|
loadTrackStatsFromEntry(preloadedEntry)
|
||||||
setEvents(preloadedEntry.events || [])
|
setEvents(preloadedEntry.events || [])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -218,6 +246,7 @@ export default function LogEntryEditor({
|
|||||||
|
|
||||||
setSignSkipper(decrypted.signSkipper || '')
|
setSignSkipper(decrypted.signSkipper || '')
|
||||||
setSignCrew(decrypted.signCrew || '')
|
setSignCrew(decrypted.signCrew || '')
|
||||||
|
loadTrackStatsFromEntry(decrypted)
|
||||||
setEvents(decrypted.events || [])
|
setEvents(decrypted.events || [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,6 +278,12 @@ export default function LogEntryEditor({
|
|||||||
loadTrack()
|
loadTrack()
|
||||||
}, [entryId, preloadedTrack])
|
}, [entryId, preloadedTrack])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!savedTrack || savedTrack.waypoints.length < 2) return
|
||||||
|
if (trackDistanceNm || trackSpeedMaxKn || trackSpeedAvgKn) return
|
||||||
|
applyTrackStats(savedTrack.waypoints)
|
||||||
|
}, [savedTrack, trackDistanceNm, trackSpeedMaxKn, trackSpeedAvgKn])
|
||||||
|
|
||||||
// Track file upload handlers
|
// Track file upload handlers
|
||||||
const handleFileUpload = async (file: File) => {
|
const handleFileUpload = async (file: File) => {
|
||||||
if (readOnly) return
|
if (readOnly) return
|
||||||
@@ -268,6 +303,7 @@ export default function LogEntryEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await saveUploadedTrack(logbookId, entryId, text, parsedWps, file.name, fileType)
|
await saveUploadedTrack(logbookId, entryId, text, parsedWps, file.name, fileType)
|
||||||
|
applyTrackStats(parsedWps)
|
||||||
await loadTrack()
|
await loadTrack()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('File parsing failed:', err)
|
console.error('File parsing failed:', err)
|
||||||
@@ -311,6 +347,9 @@ export default function LogEntryEditor({
|
|||||||
try {
|
try {
|
||||||
await deleteTrack(logbookId, entryId)
|
await deleteTrack(logbookId, entryId)
|
||||||
setSavedTrack(null)
|
setSavedTrack(null)
|
||||||
|
setTrackDistanceNm('')
|
||||||
|
setTrackSpeedMaxKn('')
|
||||||
|
setTrackSpeedAvgKn('')
|
||||||
setUploadError(null)
|
setUploadError(null)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showAlert(err.message || 'Failed to delete track')
|
showAlert(err.message || 'Failed to delete track')
|
||||||
@@ -570,6 +609,9 @@ export default function LogEntryEditor({
|
|||||||
},
|
},
|
||||||
signSkipper: isSignatureImage(signSkipper) ? signSkipper : signSkipper.trim(),
|
signSkipper: isSignatureImage(signSkipper) ? signSkipper : signSkipper.trim(),
|
||||||
signCrew: isSignatureImage(signCrew) ? signCrew : signCrew.trim(),
|
signCrew: isSignatureImage(signCrew) ? signCrew : signCrew.trim(),
|
||||||
|
trackDistanceNm: trackDistanceNm.trim() ? parseFloat(trackDistanceNm) : undefined,
|
||||||
|
trackSpeedMaxKn: trackSpeedMaxKn.trim() ? parseFloat(trackSpeedMaxKn) : undefined,
|
||||||
|
trackSpeedAvgKn: trackSpeedAvgKn.trim() ? parseFloat(trackSpeedAvgKn) : undefined,
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,6 +1220,15 @@ export default function LogEntryEditor({
|
|||||||
{savedTrack.waypoints.length > 0 && (
|
{savedTrack.waypoints.length > 0 && (
|
||||||
<> · {savedTrack.waypoints.length} {t('logs.track_upload_points')}</>
|
<> · {savedTrack.waypoints.length} {t('logs.track_upload_points')}</>
|
||||||
)}
|
)}
|
||||||
|
{trackDistanceNm && (
|
||||||
|
<> · {trackDistanceNm} sm</>
|
||||||
|
)}
|
||||||
|
{trackSpeedMaxKn && (
|
||||||
|
<> · max {trackSpeedMaxKn} kn</>
|
||||||
|
)}
|
||||||
|
{trackSpeedAvgKn && (
|
||||||
|
<> · Ø {trackSpeedAvgKn} kn</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
@@ -1204,6 +1255,47 @@ export default function LogEntryEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(savedTrack || trackDistanceNm || trackSpeedMaxKn || trackSpeedAvgKn) && (
|
||||||
|
<div className="form-grid track-stats-grid">
|
||||||
|
<div className="input-group">
|
||||||
|
<label>{t('logs.track_distance')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
placeholder="e.g. 5.0"
|
||||||
|
className="input-text"
|
||||||
|
value={trackDistanceNm}
|
||||||
|
onChange={(e) => setTrackDistanceNm(e.target.value)}
|
||||||
|
disabled={saving || readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="input-group">
|
||||||
|
<label>{t('logs.track_speed_max')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
placeholder="e.g. 7.8"
|
||||||
|
className="input-text"
|
||||||
|
value={trackSpeedMaxKn}
|
||||||
|
onChange={(e) => setTrackSpeedMaxKn(e.target.value)}
|
||||||
|
disabled={saving || readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="input-group">
|
||||||
|
<label>{t('logs.track_speed_avg')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
placeholder="e.g. 4.6"
|
||||||
|
className="input-text"
|
||||||
|
value={trackSpeedAvgKn}
|
||||||
|
onChange={(e) => setTrackSpeedAvgKn(e.target.value)}
|
||||||
|
disabled={saving || readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PhotoCapture entryId={entryId} logbookId={logbookId} readOnly={readOnly} preloadedPhotos={preloadedPhotos} />
|
<PhotoCapture entryId={entryId} logbookId={logbookId} readOnly={readOnly} preloadedPhotos={preloadedPhotos} />
|
||||||
|
|||||||
@@ -166,6 +166,9 @@
|
|||||||
"gps_track_upload_btn": "GPS-Track hochladen",
|
"gps_track_upload_btn": "GPS-Track hochladen",
|
||||||
"gps_track_delete": "Track-Datei löschen",
|
"gps_track_delete": "Track-Datei löschen",
|
||||||
"gps_track_delete_confirm": "Sind Sie sicher, dass Sie diese Track-Datei dauerhaft löschen möchten?",
|
"gps_track_delete_confirm": "Sind Sie sicher, dass Sie diese Track-Datei dauerhaft löschen möchten?",
|
||||||
|
"track_distance": "GPS-Strecke (sm)",
|
||||||
|
"track_speed_max": "Max. Geschwindigkeit (kn)",
|
||||||
|
"track_speed_avg": "Ø Geschwindigkeit (kn)",
|
||||||
"exporting": "Exportiere...",
|
"exporting": "Exportiere...",
|
||||||
"share_unsupported": "Teilen wird auf diesem Gerät nicht unterstützt. Datei wurde stattdessen heruntergeladen.",
|
"share_unsupported": "Teilen wird auf diesem Gerät nicht unterstützt. Datei wurde stattdessen heruntergeladen.",
|
||||||
"invite_crew": "Crew einladen",
|
"invite_crew": "Crew einladen",
|
||||||
|
|||||||
@@ -166,6 +166,9 @@
|
|||||||
"gps_track_upload_btn": "Upload GPS Track File",
|
"gps_track_upload_btn": "Upload GPS Track File",
|
||||||
"gps_track_delete": "Delete Track File",
|
"gps_track_delete": "Delete Track File",
|
||||||
"gps_track_delete_confirm": "Are you sure you want to permanently delete this track file?",
|
"gps_track_delete_confirm": "Are you sure you want to permanently delete this track file?",
|
||||||
|
"track_distance": "GPS distance (nm)",
|
||||||
|
"track_speed_max": "Max speed (kn)",
|
||||||
|
"track_speed_avg": "Avg speed (kn)",
|
||||||
"exporting": "Exporting...",
|
"exporting": "Exporting...",
|
||||||
"share_unsupported": "Web sharing is not supported on this device. File downloaded instead.",
|
"share_unsupported": "Web sharing is not supported on this device. File downloaded instead.",
|
||||||
"invite_crew": "Invite Crew",
|
"invite_crew": "Invite Crew",
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
|||||||
const headers = [
|
const headers = [
|
||||||
'Date', 'Day of Travel', 'Departure Port', 'Destination Port',
|
'Date', 'Day of Travel', 'Departure Port', 'Destination Port',
|
||||||
'Skipper Signature', 'Crew Signature',
|
'Skipper Signature', 'Crew Signature',
|
||||||
|
'Track Distance (nm)', 'Track Max Speed (kn)', 'Track Avg Speed (kn)',
|
||||||
'Event Time', 'MgK Course', 'RwK Course',
|
'Event Time', 'MgK Course', 'RwK Course',
|
||||||
'Wind Dir', 'Wind Str', 'Barometer (hPa)', 'Sea State',
|
'Wind Dir', 'Wind Str', 'Barometer (hPa)', 'Sea State',
|
||||||
'Current', 'Heel Angle', 'Sails/Motor', 'Log (nm)', 'Distance (nm)',
|
'Current', 'Heel Angle', 'Sails/Motor', 'Log (nm)', 'Distance (nm)',
|
||||||
@@ -96,6 +97,9 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
|||||||
const dest = entry.destination || '';
|
const dest = entry.destination || '';
|
||||||
const signS = formatSignatureForExport(entry.signSkipper);
|
const signS = formatSignatureForExport(entry.signSkipper);
|
||||||
const signC = formatSignatureForExport(entry.signCrew);
|
const signC = formatSignatureForExport(entry.signCrew);
|
||||||
|
const trackDist = entry.trackDistanceNm ?? '';
|
||||||
|
const trackMax = entry.trackSpeedMaxKn ?? '';
|
||||||
|
const trackAvg = entry.trackSpeedAvgKn ?? '';
|
||||||
const fwM = entry.freshwater?.morning ?? '';
|
const fwM = entry.freshwater?.morning ?? '';
|
||||||
const fwR = entry.freshwater?.refilled ?? '';
|
const fwR = entry.freshwater?.refilled ?? '';
|
||||||
const fwE = entry.freshwater?.evening ?? '';
|
const fwE = entry.freshwater?.evening ?? '';
|
||||||
@@ -111,6 +115,7 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
|||||||
rows.push([
|
rows.push([
|
||||||
dateVal, travelDay, dep, dest,
|
dateVal, travelDay, dep, dest,
|
||||||
signS, signC,
|
signS, signC,
|
||||||
|
trackDist, trackMax, trackAvg,
|
||||||
'', '', '',
|
'', '', '',
|
||||||
'', '', '', '',
|
'', '', '', '',
|
||||||
'', '', '', '', '',
|
'', '', '', '', '',
|
||||||
@@ -126,6 +131,7 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
|||||||
rows.push([
|
rows.push([
|
||||||
dateVal, travelDay, dep, dest,
|
dateVal, travelDay, dep, dest,
|
||||||
signS, signC,
|
signS, signC,
|
||||||
|
trackDist, trackMax, trackAvg,
|
||||||
ev.time || '', ev.mgk || '', ev.rwk || '',
|
ev.time || '', ev.mgk || '', ev.rwk || '',
|
||||||
ev.windDirection || '', ev.windStrength || '', ev.windPressure || '', ev.seaState || '',
|
ev.windDirection || '', ev.windStrength || '', ev.windPressure || '', ev.seaState || '',
|
||||||
ev.current || '', ev.heel || '', ev.sailsOrMotor || '', ev.logReading || '', ev.distance || '',
|
ev.current || '', ev.heel || '', ev.sailsOrMotor || '', ev.logReading || '', ev.distance || '',
|
||||||
|
|||||||
@@ -78,10 +78,19 @@ export async function generateLogbookPagePdf(logbookId: string, entryId: string,
|
|||||||
doc.text(`ATIS: ${atis || '—'}`, 210, 21);
|
doc.text(`ATIS: ${atis || '—'}`, 210, 21);
|
||||||
doc.text(`MMSI: ${mmsi || '—'}`, 250, 21);
|
doc.text(`MMSI: ${mmsi || '—'}`, 250, 21);
|
||||||
|
|
||||||
doc.text(`Datum: ${entry.date || '—'}`, 10, 26);
|
doc.text(`Datum: ${entry.date || '—'}`, 10, 23);
|
||||||
doc.text(`Reisetag: ${entry.dayOfTravel || '—'}`, 60, 26);
|
doc.text(`Reisetag: ${entry.dayOfTravel || '—'}`, 60, 23);
|
||||||
doc.text(`Reise von (Departure): ${entry.departure || '—'}`, 110, 26);
|
doc.text(`Reise von (Departure): ${entry.departure || '—'}`, 110, 23);
|
||||||
doc.text(`nach (Destination): ${entry.destination || '—'}`, 200, 26);
|
doc.text(`nach (Destination): ${entry.destination || '—'}`, 200, 23);
|
||||||
|
|
||||||
|
if (entry.trackDistanceNm) {
|
||||||
|
doc.setFont('Helvetica', 'normal');
|
||||||
|
doc.text(
|
||||||
|
`GPS-Track: ${entry.trackDistanceNm} sm · max. ${entry.trackSpeedMaxKn ?? '—'} kn · Ø ${entry.trackSpeedAvgKn ?? '—'} kn`,
|
||||||
|
10,
|
||||||
|
27
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Divider line
|
// Divider line
|
||||||
doc.setLineWidth(0.3);
|
doc.setLineWidth(0.3);
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import type { TrackWaypoint } from '../services/trackUpload.js'
|
||||||
|
|
||||||
|
const NM_IN_METERS = 1852
|
||||||
|
const MAX_PLAUSIBLE_KNOTS = 50
|
||||||
|
|
||||||
|
export interface TrackStats {
|
||||||
|
distanceNm: number
|
||||||
|
speedMaxKn: number
|
||||||
|
speedAvgKn: number
|
||||||
|
durationMinutes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function haversineMeters(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||||
|
const R = 6371000
|
||||||
|
const p1 = (lat1 * Math.PI) / 180
|
||||||
|
const p2 = (lat2 * Math.PI) / 180
|
||||||
|
const dLat = ((lat2 - lat1) * Math.PI) / 180
|
||||||
|
const dLon = ((lon2 - lon1) * Math.PI) / 180
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) ** 2 +
|
||||||
|
Math.cos(p1) * Math.cos(p2) * Math.sin(dLon / 2) ** 2
|
||||||
|
return 2 * R * Math.asin(Math.sqrt(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMeaningfulTimestamps(waypoints: TrackWaypoint[]): boolean {
|
||||||
|
if (waypoints.length < 2) return false
|
||||||
|
const first = waypoints[0].timestamp
|
||||||
|
const last = waypoints[waypoints.length - 1].timestamp
|
||||||
|
return last > first + 60_000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeTrackStats(waypoints: TrackWaypoint[]): TrackStats | null {
|
||||||
|
if (waypoints.length < 2) return null
|
||||||
|
|
||||||
|
let totalMeters = 0
|
||||||
|
let maxSegmentKn = 0
|
||||||
|
let maxTaggedKn = 0
|
||||||
|
let hasTaggedSpeed = false
|
||||||
|
|
||||||
|
const timed = hasMeaningfulTimestamps(waypoints)
|
||||||
|
const firstTs = waypoints[0].timestamp
|
||||||
|
const lastTs = waypoints[waypoints.length - 1].timestamp
|
||||||
|
|
||||||
|
for (let i = 1; i < waypoints.length; i++) {
|
||||||
|
const prev = waypoints[i - 1]
|
||||||
|
const curr = waypoints[i]
|
||||||
|
const segmentM = haversineMeters(prev.lat, prev.lng, curr.lat, curr.lng)
|
||||||
|
totalMeters += segmentM
|
||||||
|
|
||||||
|
if (timed) {
|
||||||
|
const dtMs = curr.timestamp - prev.timestamp
|
||||||
|
if (dtMs > 0 && segmentM > 0) {
|
||||||
|
const segmentKn = (segmentM / NM_IN_METERS) / (dtMs / 3_600_000)
|
||||||
|
if (segmentKn <= MAX_PLAUSIBLE_KNOTS) {
|
||||||
|
maxSegmentKn = Math.max(maxSegmentKn, segmentKn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curr.speedKnots != null && curr.speedKnots > 0) {
|
||||||
|
hasTaggedSpeed = true
|
||||||
|
maxTaggedKn = Math.max(maxTaggedKn, curr.speedKnots)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const distanceNm = totalMeters / NM_IN_METERS
|
||||||
|
if (distanceNm <= 0) return null
|
||||||
|
|
||||||
|
let speedMaxKn = 0
|
||||||
|
let speedAvgKn = 0
|
||||||
|
let durationMinutes = 0
|
||||||
|
|
||||||
|
if (timed) {
|
||||||
|
const durationHours = (lastTs - firstTs) / 3_600_000
|
||||||
|
durationMinutes = Math.round((lastTs - firstTs) / 60_000)
|
||||||
|
speedAvgKn = durationHours > 0 ? distanceNm / durationHours : 0
|
||||||
|
speedMaxKn = Math.max(maxSegmentKn, hasTaggedSpeed ? maxTaggedKn : 0)
|
||||||
|
} else if (hasTaggedSpeed) {
|
||||||
|
const taggedSpeeds = waypoints
|
||||||
|
.map((wp) => wp.speedKnots)
|
||||||
|
.filter((speed): speed is number => speed != null && speed > 0)
|
||||||
|
speedMaxKn = maxTaggedKn
|
||||||
|
speedAvgKn =
|
||||||
|
taggedSpeeds.length > 0
|
||||||
|
? taggedSpeeds.reduce((sum, speed) => sum + speed, 0) / taggedSpeeds.length
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
distanceNm: Number(distanceNm.toFixed(2)),
|
||||||
|
speedMaxKn: Number(speedMaxKn.toFixed(1)),
|
||||||
|
speedAvgKn: Number(speedAvgKn.toFixed(1)),
|
||||||
|
durationMinutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTrackStats(stats: TrackStats): {
|
||||||
|
distanceNm: string
|
||||||
|
speedMaxKn: string
|
||||||
|
speedAvgKn: string
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
distanceNm: stats.distanceNm.toFixed(2),
|
||||||
|
speedMaxKn: stats.speedMaxKn > 0 ? stats.speedMaxKn.toFixed(1) : '',
|
||||||
|
speedAvgKn: stats.speedAvgKn > 0 ? stats.speedAvgKn.toFixed(1) : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
+1274
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user