import { cloneDeep } from 'lodash'; import { PureComponent } from 'react'; import { Point, Rectangle } from '../components/map/types'; import { lat2tile, lon2tile, rectanglesIntersect } from '../lib/geo'; 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, 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; 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; } // console.log({ // caller: 'prune', // id, // subId, // key, // object: object[key][index], // time: object[key][index].time, // }); docs.push({ _id: getUri(key, subId), type: key, doc: subObjects[index], }); prune(subId, subObjects[index], docs); } object[key] = undefined; } else prune(id, object[key], 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; }; export const pruneAndSaveImportedGpx = async (params: any) => { console.log({ caller: 'pruneAndSaveImportedGpx', params }); const { id, gpx, extensions } = params; let docs: any[] = [ { _id: getUri('gpx', id), type: 'gpx', doc: gpx }, { _id: getUri('extensions', id), type: 'extensions', doc: { ...extensions, ...extensionsFromGpx(gpx) }, }, ]; prune(id, gpx, docs); console.log({ caller: 'pruneAndSaveImportedGpx / pruned', docs }); try { const result = await putAll(docs); console.log(JSON.stringify(result)); } 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 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; };