• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bpatrik / pigallery2 / 6488973310

11 Oct 2023 11:05PM UTC coverage: 65.19% (+1.3%) from 63.854%
6488973310

push

github

bpatrik
tweak tests

1360 of 2403 branches covered (0.0%)

Branch coverage included in aggregate %.

4056 of 5905 relevant lines covered (68.69%)

14577.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

10.75
/src/backend/model/fileprocessing/GPXProcessing.ts
1
import * as path from 'path';
1✔
2
import {constants as fsConstants, promises as fsp} from 'fs';
1✔
3
import * as xml2js from 'xml2js';
1✔
4
import {ProjectPath} from '../../ProjectPath';
1✔
5
import {Config} from '../../../common/config/private/Config';
1✔
6
import {SupportedFormats} from '../../../common/SupportedFormats';
1✔
7

8
type gpxEntry = { '$': { lat: string, lon: string }, ele?: string[], time?: string[], extensions?: unknown };
9

10
export class GPXProcessing {
1✔
11
  private static readonly GPX_FLOAT_ACCURACY = 6;
1✔
12

13
  public static isMetaFile(fullPath: string): boolean {
14
    const extension = path.extname(fullPath).toLowerCase();
125✔
15
    return SupportedFormats.WithDots.MetaFiles.indexOf(extension) !== -1;
125✔
16
  }
17

18
  public static isGPXFile(fullPath: string): boolean {
19
    const extension = path.extname(fullPath).toLowerCase();
×
20
    return extension === '.gpx';
×
21
  }
22

23
  public static generateConvertedPath(filePath: string): string {
24
    return path.join(
×
25
        ProjectPath.TranscodedFolder,
26
        ProjectPath.getRelativePathToImages(path.dirname(filePath)),
27
        path.basename(filePath)
28
        + '_' + Config.MetaFile.GPXCompressing.minDistance + 'm' +
29
        Config.MetaFile.GPXCompressing.minTimeDistance + 'ms' +
30
        Config.MetaFile.GPXCompressing.maxMiddleDeviance + 'm' +
31
        path.extname(filePath));
32
  }
33

34
  public static async isValidConvertedPath(
35
      convertedPath: string
36
  ): Promise<boolean> {
37
    const origFilePath = path.join(
×
38
        ProjectPath.ImageFolder,
39
        path.relative(
40
            ProjectPath.TranscodedFolder,
41
            convertedPath.substring(0, convertedPath.lastIndexOf('_'))
42
        )
43
    );
44

45

46
    try {
×
47
      await fsp.access(origFilePath, fsConstants.R_OK);
×
48
    } catch (e) {
49
      return false;
×
50
    }
51

52
    return true;
×
53
  }
54

55

56
  static async compressedGPXExist(
57
      filePath: string
58
  ): Promise<boolean> {
59
    // compressed gpx path
60
    const outPath = GPXProcessing.generateConvertedPath(filePath);
×
61

62
    // check if file already exist
63
    try {
×
64
      await fsp.access(outPath, fsConstants.R_OK);
×
65
      return true;
×
66
    } catch (e) {
67
      // ignoring errors
68
    }
69
    return false;
×
70
  }
71

72
  public static async compressGPX(
73
      filePath: string,
74
  ): Promise<string> {
75
    // generate compressed gpx path
76
    const outPath = GPXProcessing.generateConvertedPath(filePath);
×
77

78
    // check if file already exist
79
    try {
×
80
      await fsp.access(outPath, fsConstants.R_OK);
×
81
      return outPath;
×
82
    } catch (e) {
83
      // ignoring errors
84
    }
85

86

87
    const outDir = path.dirname(outPath);
×
88

89
    await fsp.mkdir(outDir, {recursive: true});
×
90
    const gpxStr = await fsp.readFile(filePath);
×
91
    const gpxObj = await (new xml2js.Parser()).parseStringPromise(gpxStr);
×
92

93
    if (gpxObj.gpx?.trk?.[0].trkseg[0]) { // only compress paths if there is any
×
94
      const distance = (entry1: gpxEntry, entry2: gpxEntry) => {
×
95
        const lat1 = parseFloat(entry1.$.lat);
×
96
        const lon1 = parseFloat(entry1.$.lon);
×
97
        const lat2 = parseFloat(entry2.$.lat);
×
98
        const lon2 = parseFloat(entry2.$.lon);
×
99

100
        // credits to: https://www.movable-type.co.uk/scripts/latlong.html
101
        const R = 6371e3; // metres
×
102
        const φ1 = lat1 * Math.PI / 180; // φ, λ in radians
×
103
        const φ2 = lat2 * Math.PI / 180;
×
104
        const Δφ = (lat2 - lat1) * Math.PI / 180;
×
105
        const Δλ = (lon2 - lon1) * Math.PI / 180;
×
106

107
        const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
×
108
            Math.cos(φ1) * Math.cos(φ2) *
109
            Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
110
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
×
111

112
        const d = R * c; // in metres
×
113
        return d;
×
114
      };
115
      const gpxEntryFilter = (value: gpxEntry, i: number, list: gpxEntry[]) => {
×
116
        if (i === 0 || i >= list.length - 1) { // always keep the first and last items
×
117
          return true;
×
118
        }
119
        const timeDelta = (Date.parse(list[i]?.time?.[0]) - Date.parse(list[i - 1]?.time?.[0])); // mill sec.
×
120
        const dist = distance(list[i - 1], list[i]); // meters
×
121

122
        // if time is not available, consider it as all points are created the same time
123
        return !((isNaN(timeDelta) || timeDelta < Config.MetaFile.GPXCompressing.minTimeDistance) &&
×
124
            dist < Config.MetaFile.GPXCompressing.minDistance);
125
      };
126

127
      const postFilter = (i: number, list: gpxEntry[]) => {
×
128
        if (i === 0 || i >= list.length - 1) { // always keep the first and last items
×
129
          return true;
×
130
        }
131
        /* if point on the same line that the next and prev point would draw, lets skip it*/
132
        const avg = (a: string, b: string) => ((parseFloat(a) + parseFloat(b)) / 2).toFixed(this.GPX_FLOAT_ACCURACY);
×
133
        const modPoint: gpxEntry = {
×
134
          $: {
135
            lat: avg(list[i - 1].$.lat, list[i + 1].$.lat),
136
            lon: avg(list[i - 1].$.lon, list[i + 1].$.lon)
137
          }
138
        };
139
        if (list[i].time) {
×
140
          modPoint.time = list[i].time;
×
141
        }
142

143
        const deviation = distance(modPoint, list[i]); // meters
×
144
        return !(deviation < Config.MetaFile.GPXCompressing.maxMiddleDeviance); // keep if deviation is too big
×
145
      };
146

147
      for (let i = 0; i < gpxObj.gpx.trk.length; ++i) {
×
148
        for (let j = 0; j < gpxObj.gpx.trk[0].trkseg.length; ++j) {
×
149
          const trkseg: { trkpt: gpxEntry[] } = gpxObj.gpx.trk[i].trkseg[j];
×
150

151
          trkseg.trkpt = trkseg.trkpt.filter(gpxEntryFilter).map((v) => {
×
152
            v.$.lon = parseFloat(v.$.lon).toFixed(this.GPX_FLOAT_ACCURACY);
×
153
            v.$.lat = parseFloat(v.$.lat).toFixed(this.GPX_FLOAT_ACCURACY);
×
154
            delete v.ele;
×
155
            delete v.extensions;
×
156
            return v;
×
157
          });
158

159
          for (let i = 0; i < trkseg.trkpt.length; ++i) {
×
160
            if (!postFilter(i, trkseg.trkpt)) {
×
161
              trkseg.trkpt.splice(i, 1);
×
162
              --i;
×
163
            }
164
          }
165
        }
166
      }
167
    }
168
    await fsp.writeFile(outPath, (new xml2js.Builder({renderOpts: {pretty: false}})).buildObject(gpxObj));
×
169

170
    return outPath;
×
171
  }
172

173
}
174

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc