import { Rowing } from '@suid/icons-material'; import { cloneDeep, isEmpty } from 'lodash'; import { PureComponent } from 'react'; import { $DEVCOMP } from 'solid-js'; import { Point, Rectangle } from '../components/map/types'; import { state } from '../db-admin/health-legacy'; import { lat2tile, lon2tile, rectanglesIntersect } from '../lib/geo'; import { findStartTime } from '../lib/gpx'; import getUri, { intToGpxId, intToTrkptId } from '../lib/ids'; import { get, getDocsByType, getFamily, put, putAll } from './lib'; export const emptyGpx: Gpx = { $: { version: '1.1', creator: 'dyomedea version 0.000002', xmlns: 'http://www.topografix.com/GPX/1/1', 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation': 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/WaypointExtension/v1 http://www8.garmin.com/xmlschemas/WaypointExtensionv1.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd', 'xmlns:gpxx': 'http://www.garmin.com/xmlschemas/GpxExtensions/v3', 'xmlns:wptx1': 'http://www.garmin.com/xmlschemas/WaypointExtension/v1', 'xmlns:gpxtpx': 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1', 'xmlns:dyo': 'http://xmlns.dyomedea.com/', }, metadata: { name: undefined, desc: undefined, author: undefined, copyright: undefined, link: undefined, time: undefined, keywords: undefined, bounds: undefined, extensions: undefined, }, wpt: undefined, rte: undefined, trk: undefined, extensions: undefined, }; export const newEmptyGpx = () => cloneDeep(emptyGpx); export const putNewGpx = async ( id: IdGpx = { gpx: intToGpxId(Date.now()) } ) => { const uri = getUri('gpx', id); await put( uri, 'gpx', (gpx) => { (gpx.metadata ??= {}).time = new Date(Date.now()).toISOString(); return gpx; }, emptyGpx ); return id; }; export const existsGpx = async (id: IdGpx) => { const uri = getUri('gpx', id); try { await get(uri); return true; } catch { return false; } }; const prune = ( id: any, object: any, previousIds: { rte: number; wpt: number; trk: number; rtept: number; trkseg: number; trkpt: number; }, extensions: any, docs: any[], shared: string[] ) => { if (typeof object === 'object') { for (const key in object) { if ( key === 'wpt' || key === 'rte' || // key === 'rtept' || key === 'trk' || key === 'trkseg' // key === 'trkpt' ) { const subObjects = object[key]; for (const index in subObjects) { const subId = { ...id }; /* if (key === 'trkpt') { // fix buggy times in GPX tracks const normalId = intToTrkptId( new Date(object[key][index].time).valueOf() ); const id = normalId > previousIds.trk ? normalId : previousIds.trk + 1; subId[key] = id; previousIds.trk = id; } else { */ previousIds[key] = previousIds[key] + 1; subId[key] = previousIds[key]; /* } */ // console.log({ // caller: 'prune', // id, // subId, // key, // object: object[key][index], // time: object[key][index].time, // }); subObjects[index].extensions = { ...extensions, ...subObjects[index].extensions, }; if (isEmpty(subObjects[index].extensions)) { subObjects[index].extensions = undefined; } docs.push({ _id: getUri(key, subId), type: key, shared, origin: state().remoteUrl, doc: subObjects[index], }); prune(subId, subObjects[index], previousIds, {}, docs, shared); } object[key] = undefined; } else prune(id, object[key], previousIds, extensions, docs, shared); } } }; const hasMissingTimestamps = (trk: Trk) => { for (const trkseg of trk.trkseg!) { for (const trkpt of trkseg.trkpt!) { if (trkpt.time === undefined) { return true; } } } return false; }; const convertTrkToRteWhenNeeded = (gpx: Gpx) => { if (gpx.trk === undefined) { return; } const newTrks: Trk[] = []; for (const trk of gpx.trk) { if (hasMissingTimestamps(trk)) { const rte = { ...trk, trkseg: undefined, rtept: [] }; for (const trkseg of trk.trkseg!) { rte.rtept = rte.rtept.concat(trkseg.trkpt); } if (gpx.rte === undefined) { gpx.rte = []; } gpx.rte.push(rte); } else { newTrks.push(trk); } } gpx.trk = newTrks; }; export const pruneAndSaveImportedGpx = async (params: any) => { console.log({ caller: 'pruneAndSaveImportedGpx', params }); const { id, gpx, tech } = params; let gpxId: IdGpx; let docs: any[] = []; const extensions = { ...tech, ...gpx.extensions, gpx: { creator: gpx?.$?.creator, metadata: gpx.metadata, extensions: gpx.extensions, }, }; convertTrkToRteWhenNeeded(gpx); let previousGpx: Gpx | null = null; let shared: string[]; if (id === 'new') { const currentDateTime = new Date().toISOString(); const startTime = new Date(findStartTime(gpx, currentDateTime)); if (gpx.metadata === undefined) { gpx.metadata = {}; } if (gpx.metadata.time === undefined) { gpx.metadata.time = startTime.toISOString(); } gpxId = { gpx: intToGpxId(startTime.valueOf()) }; docs = [ { _id: getUri('gpx', gpxId), type: 'gpx', origin: state().remoteUrl, doc: gpx, // a new GPX isn't shared... }, { _id: getUri('extensions', gpxId), type: 'extensions', origin: state().remoteUrl, doc: {}, }, ]; } else { gpxId = getUri('gpx', id); previousGpx = (await getGpx({ id })) ?? null; if (previousGpx?.extensions?.shared) { shared = previousGpx?.extensions?.shared.split(/\s*,\s*/); } console.log({ caller: 'pruneAndSaveImportedGpx / previousGpx', id, previousGpx, gpxId, }); } let previousIds = { rte: -1, wpt: -1, trk: -1, rtept: -1, trkseg: -1, trkpt: -1, }; if (!!previousGpx?.wpt) { const wptUri = previousGpx.wpt.at(-1); const wptId = getUri('wpt', wptUri); previousIds.wpt = wptId.wpt; } if (!!previousGpx?.rte) { const rteUri = previousGpx.rte.at(-1); const rteId = getUri('rte', rteUri); previousIds.rte = rteId.rte; } if (!!previousGpx?.trk) { const trkUri = previousGpx.trk.at(-1); const trkId = getUri('trk', trkUri); previousIds.trk = trkId.trk; } prune(gpxId, gpx, previousIds, extensions, docs, shared); console.log({ caller: 'pruneAndSaveImportedGpx / pruned', docs }); try { const result = await putAll(docs); console.log(JSON.stringify(result)); if (id !== 'new') { put(id, 'gpx', (doc) => doc, {}); } } catch (err) { console.error(`error: ${err}`); } }; export const getAllGpxes = async () => { try { return (await get('gpx')).doc; } catch { return []; } }; export const getAllGpxesWithSummary = async () => { const allGpxes = await getAllGpxes(); const result = await Promise.all( allGpxes.map(async (id: string) => { const gpxDoc = await get(id); // console.log({ caller: 'getAllGpxesWithSummary', gpx: gpxDoc }); return { id, name: gpxDoc.doc.metadata?.name, creator: gpxDoc.doc.$?.creator, }; }) ); console.log({ caller: 'getAllGpxesWithSummary', result }); return result; }; export const appendToArray = (target: any, key: string, value: any) => { if (!(key in target)) { target[key] = []; } target[key].push(value); }; export const getFullGpx = async (params: any) => { const { id } = params; const docs = await getFamily(id, { include_docs: true }); let target: any[]; let gpx: Gpx | undefined = undefined; docs.rows.forEach((row: any) => { // level 0 if (row.doc.type === 'gpx') { target = [row.doc.doc]; gpx = row.doc.doc; } //level 1 if ( row.doc.type === 'wpt' || row.doc.type === 'rte' || row.doc.type === 'trk' || row.doc.type === 'extensions' ) { target.splice(1); appendToArray(target.at(-1), row.doc.type, row.doc.doc); target.push(row.doc.doc); } // level 2 if (row.doc.type === 'rtept' || row.doc.type === 'trkseg') { target.splice(2); appendToArray(target.at(-1), row.doc.type, row.doc.doc); target.push(row.doc.doc); } // level 3 if (row.doc.type === 'trkpt') { appendToArray(target.at(-1), row.doc.type, row.doc.doc); } }); return gpx; }; export const getGpx = async (params: any) => { const { id } = params; if (id === 'new') { const newGpx = cloneDeep(emptyGpx); newGpx.metadata!.time = new Date().toISOString(); return newGpx; } const docs = await getFamily(id, { include_docs: true }); let target: any[]; let gpx: Gpx | undefined = undefined; docs.rows.every((row: any) => { // level 0 if (row.doc.type === 'gpx') { if (!!gpx) { console.error({ caller: 'getGpx', id, row, target, gpx, }); return false; // Hack to stop if getFamily fails } target = [row.doc.doc]; gpx = row.doc.doc; } //level 1 (extensions) if (target !== undefined && row.doc.type === 'extensions') { console.log({ caller: 'getGpx / extensions', gpx, row, doc: row.doc.doc, gpx_extensions: gpx?.extensions, target, }); gpx.extensions = row.doc.doc; // target.splice(1); // appendToArray(target.at(-1), row.doc.type, row.doc.doc); // target.push(row.doc.doc); } //level 1 (others) if ( target !== undefined && (row.doc.type === 'wpt' || row.doc.type === 'rte' || row.doc.type === 'trk') ) { target.splice(1); appendToArray(target.at(-1), row.doc.type, row.doc._id); target.push(row.doc.doc); } return true; }); return gpx; }; export const getNextGpxTrkId = async (gpxId: string) => { const docs = await getFamily(gpxId, { include_docs: true }); const gpxIdObj = getUri('gpx', gpxId); console.log({ caller: 'getNextGpxTrkId', gpxId, gpxIdObj, docs }); let trkId = ''; docs.rows.forEach((row: any) => { if (row.doc.type === 'trk') { trkId = row.doc._id; } }); if (trkId !== '') { const trkIdObj = getUri('trk', trkId); return getUri('trk', { gpx: gpxIdObj.gpx, trk: trkIdObj.trk + 1 }); } return getUri('trk', { gpx: gpxIdObj.gpx, trk: 0 }); }; export const appendTrk = async (params: any) => { const { gpxId, trk } = params; const trkId = await getNextGpxTrkId(gpxId); console.log({ caller: 'appendTrk', gpxId, trk, trkId }); await put(trkId, 'trk', (doc) => trk, {}); return trkId; }; export const putGpx = async (params: any) => { let { id, gpx } = params; try { if (id === 'new') { const date = !!gpx.metadata.time ? new Date(gpx.metadata.time) : new Date(); id = getUri('gpx', { gpx: intToGpxId(date.valueOf()), }); } let previousShared; try { previousShared = (await get(`${id}/4extensions`)).doc?.shared; } catch (error) {} console.log({ caller: 'putGpx', params, id, gpx, previousShared }); const extensions = gpx?.extensions; gpx.extensions = undefined; gpx.wpt = undefined; gpx.trk = undefined; gpx.rte = undefined; await put(id, 'gpx', (doc) => gpx, gpx); if (extensions !== undefined) { try { await put( `${id}/4extensions`, 'extensions', (doc) => extensions, extensions ); } catch (error) { console.error({ caller: 'putGpx / extensions', error, }); } } if (previousShared !== extensions?.shared) { const shared = extensions?.shared.split(/\s*,\s*/); const family = (await getFamily(id, { include_docs: true })).rows.map( (row: any) => { return { ...row.doc, shared }; } ); console.log({ caller: 'putGpx / updateShared', params, id, gpx, previousShared, family, }); await db.bulkDocs(family); } } catch (error) { console.error({ caller: 'putGpx', params, id, gpx, error, }); } return id; };