diff options
author | Marcin Zelent <marcin@zelent.net> | 2021-01-08 19:47:37 +0100 |
---|---|---|
committer | Marcin Zelent <marcin@zelent.net> | 2021-01-08 19:47:37 +0100 |
commit | 628632ff5092f1e3cf6c968d9bdfbd9a24f59541 (patch) | |
tree | feddb4e82267bba700a15a6a7091f922f48cae40 /lib |
Initial commit
Diffstat (limited to 'lib')
-rw-r--r-- | lib/trips.ts | 115 | ||||
-rw-r--r-- | lib/useEvent.ts | 20 | ||||
-rw-r--r-- | lib/util.ts | 30 |
3 files changed, 165 insertions, 0 deletions
diff --git a/lib/trips.ts b/lib/trips.ts new file mode 100644 index 0000000..6a58d34 --- /dev/null +++ b/lib/trips.ts @@ -0,0 +1,115 @@ +import path from 'path'; +import fs from 'fs'; +import { DOMParser } from 'xmldom'; +import { FeatureCollection, LineString } from 'geojson'; +import { gpx } from '@tmcw/togeojson'; +import gb from 'geojson-bounds'; +import * as ExifReader from 'exifreader'; + +import { Trip, Photo } from 'models'; +import { distanceBetween } from 'lib/util'; + +const tripsDirectory = path.join(process.cwd(), 'trips'); + +// extract photo metadata from EXIF +function getPhotoMetadata(filePath: string) { + const buffer = fs.readFileSync(filePath); + const tags = ExifReader.load(buffer, { expanded: true }); + + return { + latitude: tags.gps.Latitude, + longitude: tags.gps.Longitude, + time: tags.exif.DateTime.description.replace(':', '-').replace(':', '-'), + thumbnail: tags.Thumbnail.base64, + }; +} + +/** + * Reads GPX files and photos from trips folder. + */ +export default function getTripsData(): Trip[] { + // get folder names under /trips + const dirs = fs + .readdirSync(tripsDirectory, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + const allTripsData = dirs.map((dir: string) => { + const dirPath = path.join(tripsDirectory, dir); + const files = fs.readdirSync(dirPath); + + const gpxFiles = files.filter((f) => f.endsWith('.gpx')); + + // read GPX file as string + const fullPath = path.join(dirPath, gpxFiles[0]); + const fileContents = fs.readFileSync(fullPath, 'utf8'); + const contentsWithoutNS = fileContents.replace(/\sxmlns[^"]+"[^"]+"/g, ''); + + // create DOM from string + const doc = new DOMParser().parseFromString(contentsWithoutNS); + + // convert GPX to GeoJSON + const track: FeatureCollection = gpx(doc); + + // add bounding box + track.bbox = [gb.xMin(track), gb.yMin(track), gb.xMax(track), gb.yMax(track)]; + + const { coordTimes } = track.features[0].properties; + + // time of the first point + const start = coordTimes[0]; + + // time of the last point + const end = coordTimes[coordTimes.length - 1]; + + // total duration in seconds + const duration = (new Date(end).getTime() - new Date(start).getTime()) / 1000; + + // distance and speed + let totalDistance = 0; + const speeds = []; + const coords = (track.features[0].geometry as LineString).coordinates; + + for (let i = 0; i < coords.length - 1; i += 1) { + const a = [coords[i][1], coords[i][0]]; + const b = [coords[i + 1][1], coords[i + 1][0]]; + const distance = distanceBetween(a, b); + if (distance > 0) { + totalDistance += distance; + const timeBetween = + new Date(coordTimes[i + 1]).getTime() - new Date(coordTimes[i]).getTime(); + speeds.push(distance / timeBetween); + } + } + + // total distance in km + const distance = Math.floor(totalDistance / 10) / 100; + + // average speed in km/h + const speedMps = speeds.reduce((acc, val) => acc + val) / speeds.length; + const speed = Math.floor(speedMps * 3600 * 100) / 100; + + // photos + const photoFiles = files.filter((f) => f.endsWith('.jpg')); + const photos: Photo[] = photoFiles.map((p) => { + // eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires + const { src } = require(`trips/${dir}/${p}`); + return { + name: p, + src, + ...getPhotoMetadata(path.join(dirPath, p)), + }; + }); + + const trip: Trip = { name: dir, track, distance, start, end, duration, speed, photos }; + + return trip; + }); + + // sort trips by name + return allTripsData.sort((a, b) => { + if (a.name < b.name) { + return 1; + } + return -1; + }); +} diff --git a/lib/useEvent.ts b/lib/useEvent.ts new file mode 100644 index 0000000..70d51b1 --- /dev/null +++ b/lib/useEvent.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; + +/** + * A hook for creating event handlers. + */ +export default function useEvent( + event: string, + handler: (e: KeyboardEvent | MouseEvent) => void, + passive = false, +): void { + useEffect(() => { + // initiate the event handler + window.addEventListener(event, handler, passive); + + // this will clean up the event every time the component is re-rendered + return function cleanup() { + window.removeEventListener(event, handler); + }; + }); +} diff --git a/lib/util.ts b/lib/util.ts new file mode 100644 index 0000000..5a00aed --- /dev/null +++ b/lib/util.ts @@ -0,0 +1,30 @@ +/** + * Converts time in seconds to HH:mm format. + * @param time Time to convert in seconds. + */ +export function secondsToTimeString(time: number): string { + const h = Math.floor(time / 3600); + const m = Math.floor((time % 3600) / 60); + + return `${h < 10 ? `0${h}` : h}:${m < 10 ? `0${m}` : m}`; +} + +/** + * Calculates distance between two geographical points. + * @param latlng1 Coordinates of the first point. + * @param latlng2 Coordinates of the second point. + */ +export function distanceBetween(latlng1: number[], latlng2: number[]): number { + const R = 6371000; + const rad = Math.PI / 180; + const lat1 = latlng1[0] * rad; + const lat2 = latlng2[0] * rad; + const sinDLat = Math.sin(((latlng2[0] - latlng1[0]) * rad) / 2); + const sinDLon = Math.sin(((latlng2[1] - latlng1[1]) * rad) / 2); + const a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon; + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; +} + +export default { secondsToTimeString, distanceBetween }; |