import { cloneDeep } from 'lodash'; import { PureComponent } from 'react'; import { $DEVCOMP } from 'solid-js'; import { Point, Rectangle } from '../components/map/types'; 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 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, previousGpx: Gpx | null, extensions: any, docs: any[] ) => { 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]; let previousId = 0; let nbPreviousObjects = 0; if (previousGpx !== null && key in previousGpx) { nbPreviousObjects = previousGpx[key].length; } 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 > previousId ? normalId : previousId + 1; subId[key] = id; previousId = id; } else { subId[key] = index + nbPreviousObjects; } // console.log({ // caller: 'prune', // id, // subId, // key, // object: object[key][index], // time: object[key][index].time, // }); subObjects[index].extensions = { ...extensions, ...subObjects[index].extensions, }; docs.push({ _id: getUri(key, subId), type: key, doc: subObjects[index], }); prune(subId, subObjects[index], previousGpx, extensions, docs); } object[key] = undefined; } else prune(id, object[key], previousGpx, extensions, docs); } } }; // const extensionsFromObject = ( // object: any, // extensions = { // viewport: { topLeft: {}, bottomRight: {} }, // bbox: { // minLon: undefined, // minLat: undefined, // maxLon: undefined, // maxLat: undefined, // }, // } // ) => { // if (typeof object === 'object') { // if ('$' in object) { // const attributes = object.$; // if ('lat' in attributes) { // const lat = +attributes.lat; // if ( // extensions.bbox.minLat === undefined || // lat < extensions.bbox.minLat // ) { // extensions.bbox.minLat = lat; // } // if ( // extensions.bbox.maxLat === undefined || // lat > extensions.bbox.maxLat // ) { // extensions.bbox.maxLat = lat; // } // } // if ('lon' in attributes) { // const lon = +attributes.lon; // if ( // extensions.bbox.minLon === undefined || // lon < extensions.bbox.minLon // ) { // extensions.bbox.minLon = lon; // } // if ( // extensions.bbox.maxLon === undefined || // lon > extensions.bbox.maxLon // ) { // extensions.bbox.maxLon = lon; // } // } // } // for (const key in object) { // extensionsFromObject(object[key], extensions); // } // } // return extensions; // }; // const extensionsFromGpx = (gpx: Gpx) => { // const extensions = { ...gpx.extensions, ...extensionsFromObject(gpx) }; // gpx.extensions = undefined; // if ( // extensions.bbox.maxLat !== undefined && // extensions.bbox.minLon !== undefined // ) { // extensions.viewport.topLeft = { // x: lon2tile(extensions.bbox.minLon, 0), // y: lat2tile(extensions.bbox.maxLat, 0), // }; // } // if ( // extensions.bbox.minLat !== undefined && // extensions.bbox.maxLon !== undefined // ) { // extensions.viewport.bottomRight = { // x: lon2tile(extensions.bbox.maxLon, 0), // y: lat2tile(extensions.bbox.minLat, 0), // }; // } // return extensions; // }; 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; 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', doc: gpx }, { _id: getUri('extensions', gpxId), type: 'extensions', doc: {}, }, ]; } else { gpxId = getUri('gpx', id); previousGpx = (await getGpx(id)) ?? null; } prune(gpxId, gpx, previousGpx, extensions, docs); 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.forEach((row: any) => { // level 0 if (row.doc.type === 'gpx') { target = [row.doc.doc]; gpx = row.doc.doc; } //level 1 (extensions) if (row.doc.type === 'extensions') { target.splice(1); appendToArray(target.at(-1), row.doc.type, row.doc.doc); target.push(row.doc.doc); } //level 1 (others) if ( 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 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; if (id === 'new') { id = getUri('gpx', { gpx: intToGpxId(new Date(gpx.metadata.time).valueOf()), }); } 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) { await put( `${id}/4extensions`, 'extensions', (doc) => extensions, extensions ); } return id; };