import { db } from './db.js' import { getActiveMasterKey } from './auth.js' import { getLogbookKey } from './logbookKeys.js' import { encryptJson, decryptJson } from './crypto.js' import { syncLogbook } from './sync.js' export interface TrackWaypoint { timestamp: number lat: number lng: number speedKnots?: number heading?: number } export interface SavedTrack { waypoints: TrackWaypoint[] gpxContent: string filename: string fileType: string } export async function getDecryptedTrack(entryId: string): Promise { const record = await db.gpsTracks.get(entryId) if (!record) return null const logbookId = record.logbookId const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) { throw new Error('Encryption key not found. Please log in.') } try { const decrypted = await decryptJson(record.encryptedData, record.iv, record.tag, masterKey) if (Array.isArray(decrypted)) { return { waypoints: decrypted, gpxContent: buildLegacyGpx(decrypted, 'legacy'), filename: 'track_legacy.gpx', fileType: 'gpx' } } return decrypted as SavedTrack } catch (err) { console.error('Failed to decrypt track file:', err) return null } } export async function saveUploadedTrack( logbookId: string, entryId: string, fileContent: string, waypoints: TrackWaypoint[], filename: string, fileType: string ): Promise { const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') const trackData: SavedTrack = { waypoints, gpxContent: fileContent, filename, fileType } const encrypted = await encryptJson(trackData, masterKey) const now = new Date().toISOString() await db.gpsTracks.put({ entryId, logbookId, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: now }) await db.syncQueue.put({ action: 'create', type: 'gpsTrack', payloadId: entryId, logbookId, data: JSON.stringify(encrypted), updatedAt: now }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } export async function deleteTrack(logbookId: string, entryId: string): Promise { const now = new Date().toISOString() await db.gpsTracks.delete(entryId) await db.syncQueue.put({ action: 'delete', type: 'gpsTrack', payloadId: entryId, logbookId, data: '', updatedAt: now }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } export function downloadTrackFile(track: SavedTrack): void { const blob = new Blob([track.gpxContent], { type: 'text/plain;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = track.filename || 'track.gpx' document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } export function parseTrackFile(text: string, filename: string): { waypoints: TrackWaypoint[]; type: string } { const lowerName = filename.toLowerCase() if (lowerName.endsWith('.kml') || text.includes('= 2) { const lon = parseFloat(parts[0]) const lat = parseFloat(parts[1]) if (!isNaN(lat) && !isNaN(lon)) { waypoints.push({ timestamp: Date.now(), lat: Number(lat.toFixed(6)), lng: Number(lon.toFixed(6)) }) } } } } const gxCoords = xmlDoc.getElementsByTagName('gx:coord') for (let i = 0; i < gxCoords.length; i++) { const parts = (gxCoords[i].textContent || '').trim().split(/\s+/) if (parts.length >= 2) { const lon = parseFloat(parts[0]) const lat = parseFloat(parts[1]) if (!isNaN(lat) && !isNaN(lon)) { waypoints.push({ timestamp: Date.now(), lat: Number(lat.toFixed(6)), lng: Number(lon.toFixed(6)) }) } } } return waypoints } function parseGeoJsonFile(geoJsonText: string): TrackWaypoint[] { const waypoints: TrackWaypoint[] = [] try { const data = JSON.parse(geoJsonText) const processGeometry = (geom: any) => { if (!geom) return if (geom.type === 'LineString' && Array.isArray(geom.coordinates)) { for (const coord of geom.coordinates) { pushCoord(waypoints, coord) } } else if (geom.type === 'MultiLineString' && Array.isArray(geom.coordinates)) { for (const line of geom.coordinates) { if (Array.isArray(line)) { for (const coord of line) { pushCoord(waypoints, coord) } } } } } if (data.type === 'FeatureCollection' && Array.isArray(data.features)) { for (const feature of data.features) { if (feature?.geometry) processGeometry(feature.geometry) } } else if (data.type === 'Feature' && data.geometry) { processGeometry(data.geometry) } else if (data.type === 'LineString' || data.type === 'MultiLineString') { processGeometry(data) } } catch (err) { console.error('Failed to parse GeoJSON track:', err) } return waypoints } function pushCoord(waypoints: TrackWaypoint[], coord: number[]) { const lon = coord[0] const lat = coord[1] if (typeof lat === 'number' && typeof lon === 'number') { waypoints.push({ timestamp: Date.now(), lat: Number(lat.toFixed(6)), lng: Number(lon.toFixed(6)) }) } } function buildLegacyGpx(waypoints: TrackWaypoint[], dateStr: string): string { const trkpts = waypoints .map((wp) => { const timeISO = new Date(wp.timestamp).toISOString() return ` ` }) .join('\n') return ` Track Log ${dateStr} ${trkpts} ` }