aboutsummaryrefslogtreecommitdiff
blob: 40a71a453cfed8dff482a4880a9aeb53cb90c5db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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 _ from 'lodash';

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(':', '-'),
  };
}

/**
 * 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
      return {
        name: _.kebabCase(p),
        src: require(`trips/${dir}/${p}`).src,
        thumbnail: require(`trips/${dir}/${p}?resize&sizes[]=36`).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;
  });
}