import GPX from "$lib/models/gpx/gpx"; import { Trail } from "$lib/models/trail"; import { Waypoint } from "$lib/models/waypoint"; import { currentUser } from "$lib/stores/user_store"; import { gpx, kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON"; import cryptoRandomString from "crypto-random-string"; import { get } from "svelte/store"; //@ts-ignore import { browser } from "$app/environment"; import Track from "$lib/models/gpx/track"; import TrackSegment from "$lib/models/gpx/track-segment"; import GPXWaypoint from "$lib/models/gpx/waypoint"; import EasyFit from "$lib/vendor/easy-fit/easy-fit"; import type { GeoJSON, Feature, FeatureCollection, GeoJsonProperties, Position } from 'geojson'; import * as xmldom from 'xmldom'; import { bbox } from "./geojson_util"; export async function gpx2trail(gpxString: string, fallbackName?: string) { const gpx = await GPX.parse(gpxString); if (gpx instanceof Error) { throw gpx; } const trail = new Trail(""); trail.name = gpx.metadata?.name || gpx.trk?.at(0)?.name || gpx.rte?.at(0)?.name || fallbackName || `trail-${new Date().toISOString()}`; trail.description = gpx.metadata?.desc; for (const wpt of gpx.wpt ?? []) { const wp = new Waypoint(wpt.$.lat ?? 0, wpt.$.lon ?? 0); wp.id = cryptoRandomString({ length: 15 }); wp.name = wpt.name ?? "" wp.description = wpt.desc; trail.expand!.waypoints.push(wp); } const totals = gpx.getTotals() const trackPoints = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt const routePoints = gpx.rte?.at(0)?.rtept; const startPoint = trackPoints?.at(0) ?? routePoints?.at(0); if (startPoint) { trail.lat = startPoint.$.lat trail.lon = startPoint.$.lon } const startTime = trackPoints?.at(0)?.time; const endTime = trackPoints?.at((trackPoints?.length ?? 1) - 1)?.time if (startTime && endTime) { trail.date = startTime.toISOString() .substring(0, 10); } trail.duration = totals.duration / 1000 / 60 trail.elevation_gain = totals.elevationGain; trail.elevation_loss = totals.elevationLoss; trail.distance = totals.distance return { gpx: gpx, trail: trail } } export async function trail2gpx(trail: Trail) { if (!trail.expand?.gpx_data) { throw Error("Trail has no GPX data") } const gpx = await GPX.parse(trail.expand.gpx_data) as GPX; if (gpx instanceof Error) { throw gpx; } gpx.metadata = { name: trail.name, desc: trail.description ?? "", time: trail.date ? new Date(trail.date) : new Date(), keywords: `${trail.category ?? ""}, ${trail.location ?? ""}`, author: { name: trail.author ?? "", email: get(currentUser)?.email ?? "" } } if (!gpx.wpt) { gpx.wpt = []; } for (const wp of trail.expand.waypoints ?? []) { const gpxWpt = gpx.wpt.find((w) => w.$.lat == wp.lat && w.$.lon == wp.lon) if (!gpxWpt) { gpx.wpt.push({ $: { lat: wp.lat, lon: wp.lon } }) } } return gpx.toString(); } export async function fromFile(file: File | Blob) { let gpxData = ""; let gpxFile: Blob; const fileContent = await file.text(); const fileBuffer = await file.arrayBuffer(); if (!isFITFile(fileBuffer)) { if (fileContent.includes("http://www.opengis.net/kml")) { gpxData = fromKML(fileContent); gpxFile = new Blob([gpxData], { type: "application/gpx+xml", }); } else if (fileContent.includes("TrainingCenterDatabase")) { gpxData = fromTCX(fileContent); gpxFile = new Blob([gpxData], { type: "application/gpx+xml", }); } else { gpxData = fileContent; gpxFile = file; } } else { gpxData = await fromFIT(fileBuffer); gpxFile = new Blob([gpxData], { type: "application/gpx+xml", }); } return {gpxData, gpxFile}; } export function fromKML(kmlData: string) { const parser = browser ? new DOMParser() : new xmldom.DOMParser(); const nodes = parser.parseFromString(kmlData, "text/xml") const geojson = kml(nodes) as Feature | FeatureCollection const gpx = fromGeoJSON(geojson); return gpx } export function fromTCX(tcxData: string) { const parser = browser ? new DOMParser() : new xmldom.DOMParser(); const nodes = parser.parseFromString(tcxData, "text/xml") const geojson = tcx(nodes) as Feature | FeatureCollection const gpx = fromGeoJSON(geojson); return gpx } export async function fromFIT(fitData: ArrayBuffer) { const easyFit = new EasyFit(); return new Promise((resolve, reject) => easyFit.parse(fitData, function (error, data) { if (error) { console.log(error); reject(error) } const gpx = new GPX({ metadata: { name: "", desc: "", time: data.activity?.timestamp ?? new Date(), keywords: "wanderer" }, trk: [ new Track({ trkseg: new TrackSegment({ trkpt: data.records.flatMap(((d: { position_lat: any; position_long: any; timestamp: any; altitude: any; }) => { return (d.position_lat && d.position_long) ? new GPXWaypoint({ $: { lat: d.position_lat, lon: d.position_long }, time: d.timestamp, ele: d.altitude }) : [] })) }) }) ] }); resolve(gpx.toString()); })); } export function fromGeoJSON(geoJson: Feature | FeatureCollection) { const gpx = new GPX({ metadata: { name: "", desc: "", time: new Date(), keywords: "wanderer" }, trk: [], wpt: [] }); const type = geoJson.type; function addSupportedPropertiesFromObject(el: any, supports: string[], properties: GeoJsonProperties) { if (properties && typeof properties === 'object') { supports.forEach(function (key: any) { var value = properties[key]; if (value && typeof value === 'string' && supports.includes(key)) { el[key] = value; } }); } } function createTrk(properties: GeoJsonProperties) { var trk = new Track({ trkseg: [] }); var supports = ['name', 'desc', 'src', 'type']; addSupportedPropertiesFromObject(trk, supports, properties); return trk; } function createPt(position: Position, properties?: GeoJsonProperties) { const pt = new GPXWaypoint({ $: { lat: position[1], lon: position[0] }, ele: position[2], time: position[3] as any }); const supports = ['name', 'desc', 'src', 'type']; if (properties) { addSupportedPropertiesFromObject(pt, supports, properties); } return pt; } function createTrkSeg(coordinates: Position[]) { var trkSeg = new TrackSegment({ trkpt: [] }); coordinates.forEach(function (point) { trkSeg.trkpt!.push(createPt(point)); }); return trkSeg; } function interpretFeature(feature: Feature) { const geometry = feature.geometry if (!geometry) { return } const properties = feature.properties; const type = geometry.type; switch (type) { case 'Polygon': break; case 'Point': { gpx.wpt!.push(createPt(geometry.coordinates, properties)); break; } case 'MultiPoint': { geometry.coordinates.forEach(function (coord) { gpx.wpt!.push(createPt(coord, properties)); }); break; } case 'LineString': { var lineTrk = createTrk(properties); var trkseg = createTrkSeg(geometry.coordinates); lineTrk.trkseg!.push(trkseg); gpx.trk!.push(lineTrk); break; } case 'MultiLineString': { var trk_1 = createTrk(properties); geometry.coordinates.forEach(function (pos) { var trkseg = createTrkSeg(pos); trk_1.trkseg!.push(trkseg); }); gpx.trk!.push(trk_1); break; } default: break; } } switch (type) { case 'Feature': { interpretFeature(geoJson); break; } case 'FeatureCollection': { var features = geoJson.features; features.forEach(function (feature) { interpretFeature(feature); }); break; } default: break; } return gpx.toString() } export function isFITFile(buffer: ArrayBuffer) { var blob = new Uint8Array(buffer); if (blob.length < 12) { return false } var headerLength = blob[0]; if (headerLength !== 14 && headerLength !== 12) { return false; } var fileTypeString = ''; for (var i = 8; i < 12; i++) { fileTypeString += String.fromCharCode(blob[i]); } if (fileTypeString !== '.FIT') { return false } return true; } export function toGeoJson(gpxData: string) { const parser = browser ? new DOMParser() : new xmldom.DOMParser(); const geojson = gpx( parser.parseFromString(gpxData, "text/xml"), ) as GeoJSON; geojson.bbox = bbox(geojson) return geojson }