openskidata-format 13.0.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/ElevationProfile.d.ts +1 -0
  2. package/dist/ElevationProfile.d.ts.map +1 -0
  3. package/dist/ElevationProfile.js +15 -24
  4. package/dist/ElevationProfile.js.map +1 -1
  5. package/dist/FeatureType.d.ts +1 -0
  6. package/dist/FeatureType.d.ts.map +1 -0
  7. package/dist/FeatureType.js +2 -5
  8. package/dist/FeatureType.js.map +1 -1
  9. package/dist/Lift.d.ts +10 -8
  10. package/dist/Lift.d.ts.map +1 -0
  11. package/dist/Lift.js +22 -29
  12. package/dist/Lift.js.map +1 -1
  13. package/dist/LiftNameFormatter.d.ts +2 -1
  14. package/dist/LiftNameFormatter.d.ts.map +1 -0
  15. package/dist/LiftNameFormatter.js +11 -14
  16. package/dist/LiftNameFormatter.js.map +1 -1
  17. package/dist/Place.d.ts +1 -0
  18. package/dist/Place.d.ts.map +1 -0
  19. package/dist/Place.js +1 -2
  20. package/dist/Run.d.ts +10 -9
  21. package/dist/Run.d.ts.map +1 -0
  22. package/dist/Run.js +23 -30
  23. package/dist/Run.js.map +1 -1
  24. package/dist/RunDifficultyConvention.d.ts +2 -1
  25. package/dist/RunDifficultyConvention.d.ts.map +1 -0
  26. package/dist/RunDifficultyConvention.js +10 -15
  27. package/dist/RunDifficultyConvention.js.map +1 -1
  28. package/dist/SkiArea.d.ts +12 -10
  29. package/dist/SkiArea.d.ts.map +1 -0
  30. package/dist/SkiArea.js +2 -5
  31. package/dist/SkiArea.js.map +1 -1
  32. package/dist/SkiPass.d.ts +32 -7
  33. package/dist/SkiPass.d.ts.map +1 -0
  34. package/dist/SkiPass.js +1 -2
  35. package/dist/SlopeGradingScale.d.ts +3 -2
  36. package/dist/SlopeGradingScale.d.ts.map +1 -0
  37. package/dist/SlopeGradingScale.js +21 -26
  38. package/dist/SlopeGradingScale.js.map +1 -1
  39. package/dist/SnowCoverHistory.d.ts +1 -0
  40. package/dist/SnowCoverHistory.d.ts.map +1 -0
  41. package/dist/SnowCoverHistory.js +9 -8
  42. package/dist/SnowCoverHistory.js.map +1 -1
  43. package/dist/Source.d.ts +1 -0
  44. package/dist/Source.d.ts.map +1 -0
  45. package/dist/Source.js +11 -17
  46. package/dist/Source.js.map +1 -1
  47. package/dist/Spot.d.ts +6 -5
  48. package/dist/Spot.d.ts.map +1 -0
  49. package/dist/Spot.js +6 -9
  50. package/dist/Spot.js.map +1 -1
  51. package/dist/Status.d.ts +1 -0
  52. package/dist/Status.d.ts.map +1 -0
  53. package/dist/Status.js +2 -5
  54. package/dist/Status.js.map +1 -1
  55. package/dist/ViewportHint.d.ts +1 -0
  56. package/dist/ViewportHint.d.ts.map +1 -0
  57. package/dist/ViewportHint.js +4 -10
  58. package/dist/ViewportHint.js.map +1 -1
  59. package/dist/index.d.ts +16 -15
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +15 -18
  62. package/dist/index.js.map +1 -1
  63. package/dist/util/exhaustiveMatchingGuard.d.ts +1 -0
  64. package/dist/util/exhaustiveMatchingGuard.d.ts.map +1 -0
  65. package/dist/util/exhaustiveMatchingGuard.js +1 -5
  66. package/dist/util/exhaustiveMatchingGuard.js.map +1 -1
  67. package/package.json +35 -23
  68. package/dist/testUtils.d.ts +0 -2
  69. package/dist/testUtils.js +0 -7
  70. package/dist/testUtils.js.map +0 -1
  71. package/src/ElevationProfile.test.ts +0 -738
  72. package/src/ElevationProfile.ts +0 -452
  73. package/src/FeatureType.ts +0 -6
  74. package/src/Lift.test.ts +0 -195
  75. package/src/Lift.ts +0 -219
  76. package/src/LiftNameFormatter.ts +0 -116
  77. package/src/Place.ts +0 -11
  78. package/src/Run.ts +0 -242
  79. package/src/RunDifficultyConvention.test.ts +0 -79
  80. package/src/RunDifficultyConvention.ts +0 -335
  81. package/src/SkiArea.ts +0 -128
  82. package/src/SkiPass.ts +0 -62
  83. package/src/SlopeGradingScale.test.ts +0 -151
  84. package/src/SlopeGradingScale.ts +0 -96
  85. package/src/SnowCoverHistory.test.ts +0 -103
  86. package/src/SnowCoverHistory.ts +0 -107
  87. package/src/Source.test.ts +0 -50
  88. package/src/Source.ts +0 -72
  89. package/src/Spot.ts +0 -140
  90. package/src/Status.ts +0 -20
  91. package/src/ViewportHint.ts +0 -208
  92. package/src/index.ts +0 -15
  93. package/src/testUtils.ts +0 -5
  94. package/src/util/exhaustiveMatchingGuard.ts +0 -3
@@ -1,738 +0,0 @@
1
- import length from '@turf/length'
2
- import {
3
- extractPointsForElevationProfile,
4
- getAscentAndDescent,
5
- getPitchData,
6
- lineChunkPatched,
7
- } from './ElevationProfile'
8
- import { FeatureType } from './FeatureType'
9
- import { getLiftElevationData, LiftFeature, LiftType } from './Lift'
10
- import { getRunElevationData, RunDifficulty, RunFeature, RunUse } from './Run'
11
- import { RunDifficultyConvention } from './RunDifficultyConvention'
12
- import { Status } from './Status'
13
- import { mockViewportHint } from './testUtils'
14
-
15
- describe('ElevationProfile', () => {
16
- describe('getAscentAndDescent', () => {
17
- it('calculates ascent and descent correctly from elevation profile', () => {
18
- const profileGeometry: GeoJSON.LineString = {
19
- type: 'LineString',
20
- coordinates: [
21
- [0, 0, 100], // start at 100m
22
- [1, 0, 150], // climb to 150m (+50m)
23
- [2, 0, 130], // descend to 130m (-20m)
24
- [3, 0, 180], // climb to 180m (+50m)
25
- ],
26
- }
27
-
28
- const result = getAscentAndDescent(profileGeometry)
29
-
30
- expect(result.ascentInMeters).toBe(100) // 50 + 50 = 100m of climbing
31
- expect(result.descentInMeters).toBe(20) // 20m of descent
32
- expect(result.minElevationInMeters).toBe(100)
33
- expect(result.maxElevationInMeters).toBe(180)
34
- })
35
-
36
- it('throws error when coordinates are empty', () => {
37
- const profileGeometry: GeoJSON.LineString = {
38
- type: 'LineString',
39
- coordinates: [],
40
- }
41
-
42
- expect(() => getAscentAndDescent(profileGeometry)).toThrow(
43
- 'Empty coordinates are not supported for elevation data analysis',
44
- )
45
- })
46
-
47
- it('throws error when elevation data is missing', () => {
48
- const profileGeometry: GeoJSON.LineString = {
49
- type: 'LineString',
50
- coordinates: [
51
- [0, 0],
52
- [1, 1],
53
- ],
54
- }
55
-
56
- expect(() => getAscentAndDescent(profileGeometry)).toThrow(
57
- 'Elevation data is required for elevation data analysis',
58
- )
59
- })
60
- })
61
-
62
- describe('getPitchData', () => {
63
- it('calculates pitch data correctly', () => {
64
- const profileGeometry: GeoJSON.LineString = {
65
- type: 'LineString',
66
- coordinates: [
67
- [11.177425600412874, 47.31265682344346, 100], // start
68
- [11.177224899122194, 47.312533118812354, 110], // 10m rise over ~20.45m distance = ~49% slope / 22.76m inclined
69
- [11.176823496540862, 47.31229807921545, 105], // -5m fall over ~39.99m distance = ~-13% slope / 40.3m inclined
70
- ],
71
- }
72
-
73
- const result = getPitchData(profileGeometry)
74
-
75
- // With the new resolution calculation, the actual resolution divides the distance evenly
76
- // Total length is ~60.44m, with default resolution of 25m: numSegments = ceil(60.44/25) = 3, actualResolution = 60.44/3 ≈ 20.14m
77
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(20.14)
78
- expect(result.pitchCalculationResolutionInMeters).toBeLessThanOrEqual(25)
79
- expect(result.maxPitchInPercent).toBeCloseTo(0.489)
80
- expect(result.averagePitchInPercent).toBeCloseTo(0.2482)
81
- expect(result.inclinedLengthInMeters).toBeCloseTo(63.0597)
82
- })
83
-
84
- it('calculates pitch correctly on a profile with problematic short segments', () => {
85
- // Create a geometry with artificially short segments and unrealistic elevation changes
86
- const profileGeometry: GeoJSON.LineString = {
87
- type: 'LineString',
88
- coordinates: [
89
- [0, 0, 100],
90
- [0, 0.00000001, 101], // Very short distance with 1m elevation gain (unrealistically steep)
91
- [0, 0.001, 102], // Normal segment
92
- [0, 0.002, 103], // Normal segment
93
- ],
94
- }
95
-
96
- // Using fixed-length segments should smooth out the unrealistic spike
97
- const result = getPitchData(profileGeometry, 50)
98
-
99
- // The overall change should be more reasonable when calculated over fixed distances
100
- expect(result.maxPitchInPercent).toBeLessThan(0.5) // Should be much less than the individual segment pitch
101
- })
102
-
103
- it('calculates resolution that divides distance evenly', () => {
104
- const profileGeometry: GeoJSON.LineString = {
105
- type: 'LineString',
106
- coordinates: [
107
- [0, 0, 100],
108
- [0, 0.001, 110], // ~111m distance
109
- [0, 0.002, 105], // ~111m distance
110
- ],
111
- }
112
-
113
- const result = getPitchData(profileGeometry, 50)
114
-
115
- // Resolution should be <= 50
116
- expect(result.pitchCalculationResolutionInMeters).toBeLessThanOrEqual(50)
117
-
118
- // With ~222m total length and 50m input resolution:
119
- // numSegments = ceil(222/50) = 5
120
- // actualResolution = 222/5 ≈ 44.48m
121
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(44.48)
122
- })
123
-
124
- it('uses total length as resolution when line is shorter than input resolution', () => {
125
- const profileGeometry: GeoJSON.LineString = {
126
- type: 'LineString',
127
- coordinates: [
128
- [0, 0, 100],
129
- [0, 0.0001, 110], // ~11m distance
130
- ],
131
- }
132
-
133
- const result = getPitchData(profileGeometry, 50)
134
-
135
- // Resolution should equal the total length since numSegments = ceil(11/50) = 1
136
- // actualResolution = 11/1 ≈ 11.12m
137
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(11.12)
138
- expect(result.pitchCalculationResolutionInMeters).toBeLessThanOrEqual(50)
139
- })
140
-
141
- it('ensures all chunks are processed without skipping short chunks', () => {
142
- // Create a line that would previously have a short final chunk
143
- const profileGeometry: GeoJSON.LineString = {
144
- type: 'LineString',
145
- coordinates: [
146
- [11.177425600412874, 47.31265682344346, 100],
147
- [11.177224899122194, 47.312533118812354, 120], // ~20m, steep
148
- [11.176823496540862, 47.31229807921545, 105], // ~40m, gentle descent
149
- ],
150
- }
151
-
152
- const result = getPitchData(profileGeometry, 25)
153
-
154
- // With evenly divided chunks, no chunks should be skipped
155
- // The max pitch should be calculated from all segments
156
- expect(result.maxPitchInPercent).toBeGreaterThan(0)
157
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(20.14)
158
- })
159
-
160
- it('returns null pitch data for very short lines', () => {
161
- const profileGeometry: GeoJSON.LineString = {
162
- type: 'LineString',
163
- coordinates: [
164
- [0, 0, 100],
165
- [0, 0.00001, 101], // ~1.11m distance
166
- ],
167
- }
168
-
169
- const result = getPitchData(profileGeometry, 25)
170
-
171
- // Lines shorter than half the resolution should return null pitch data
172
- // because elevation data resolution makes pitch calculations unreliable
173
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(1.11)
174
- expect(result.maxPitchInPercent).toBeNull()
175
- expect(result.averagePitchInPercent).toBeNull()
176
- expect(result.overallPitchInPercent).toBeNull()
177
- expect(result.inclinedLengthInMeters).toBeGreaterThan(0) // This should still be calculated
178
- })
179
-
180
- it('calculates pitch data for lines just above the minimum threshold', () => {
181
- const profileGeometry: GeoJSON.LineString = {
182
- type: 'LineString',
183
- coordinates: [
184
- [0, 0, 100],
185
- [0, 0.00015, 105], // ~16.7m distance, just above half of 25m
186
- ],
187
- }
188
-
189
- const result = getPitchData(profileGeometry, 25)
190
-
191
- // Lines at or above half the resolution should calculate pitch data
192
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(16.68)
193
- expect(result.maxPitchInPercent).not.toBeNull()
194
- expect(result.averagePitchInPercent).not.toBeNull()
195
- expect(result.overallPitchInPercent).not.toBeNull()
196
- })
197
- })
198
-
199
- describe('extractPointsForElevationProfile', () => {
200
- it('extracts points at regular intervals', () => {
201
- const geometry: GeoJSON.LineString = {
202
- type: 'LineString',
203
- coordinates: [
204
- [11.177452968770694, 47.312650638218656],
205
- [11.175409464719593, 47.31138883724759],
206
- ],
207
- } // 208m distance
208
-
209
- // With 20m resolution, there should be 12 points
210
- const result = extractPointsForElevationProfile(geometry, 20)
211
-
212
- expect(result.geometry.coordinates.length).toEqual(12)
213
- expect(result.geometry.coordinates[0]).toEqual([
214
- 11.177452968770694, 47.312650638218656,
215
- ])
216
- expect(
217
- result.geometry.coordinates[result.geometry.coordinates.length - 1],
218
- ).toEqual([11.175409464719593, 47.31138883724759])
219
- })
220
-
221
- it('handles points that already have elevation', () => {
222
- const geometry: GeoJSON.LineString = {
223
- type: 'LineString',
224
- coordinates: [
225
- [11.177452968770694, 47.312650638218656, 1500], // With elevation
226
- [11.175409464719593, 47.31138883724759, 1450], // With elevation
227
- ],
228
- }
229
-
230
- const result = extractPointsForElevationProfile(geometry, 20)
231
-
232
- // Should still work properly, returning 2D points regardless of input dimension
233
- expect(result.geometry.coordinates.length).toBeGreaterThan(1)
234
- expect(result.geometry.coordinates[0].length).toBe(2) // Should always return 2D points
235
- expect(result.geometry.coordinates[0]).toEqual([
236
- 11.177452968770694, 47.312650638218656,
237
- ])
238
- })
239
- })
240
-
241
- describe('getRunElevationData', () => {
242
- it('returns null for non-LineString geometry', () => {
243
- const runFeature: RunFeature = {
244
- type: 'Feature',
245
- geometry: {
246
- type: 'Polygon',
247
- coordinates: [
248
- [
249
- [0, 0],
250
- [0, 1],
251
- [1, 1],
252
- [1, 0],
253
- [0, 0],
254
- ],
255
- ],
256
- },
257
- properties: {
258
- type: FeatureType.Run,
259
- id: '123',
260
- ref: null,
261
- uses: [RunUse.Downhill],
262
- difficulty: RunDifficulty.EASY,
263
- status: Status.Operating,
264
- name: 'Test Run',
265
- elevationProfile: null,
266
- skiAreas: [],
267
- sources: [],
268
- description: null,
269
- difficultyConvention: RunDifficultyConvention.EUROPE,
270
- oneway: null,
271
- lit: null,
272
- gladed: null,
273
- patrolled: null,
274
- snowmaking: null,
275
- snowfarming: null,
276
- grooming: null,
277
- websites: [],
278
- wikidataID: null,
279
- tunnel: null,
280
- places: [],
281
- viewportHint: mockViewportHint(),
282
- },
283
- }
284
-
285
- expect(getRunElevationData(runFeature)).toBeNull()
286
- })
287
-
288
- it('returns null for missing elevation profile', () => {
289
- const runFeature: RunFeature = {
290
- type: 'Feature',
291
- geometry: {
292
- type: 'LineString',
293
- coordinates: [
294
- [0, 0, 0],
295
- [1, 1, 0],
296
- ],
297
- },
298
- properties: {
299
- type: FeatureType.Run,
300
- id: '123',
301
- difficulty: RunDifficulty.EASY,
302
- status: Status.Operating,
303
- name: 'Test Run',
304
- elevationProfile: null,
305
- skiAreas: [],
306
- sources: [],
307
- uses: [],
308
- ref: null,
309
- description: null,
310
- difficultyConvention: RunDifficultyConvention.EUROPE,
311
- oneway: null,
312
- lit: null,
313
- gladed: null,
314
- patrolled: null,
315
- snowmaking: null,
316
- snowfarming: null,
317
- grooming: null,
318
- websites: [],
319
- wikidataID: null,
320
- tunnel: null,
321
- places: [],
322
- viewportHint: mockViewportHint(),
323
- },
324
- }
325
-
326
- expect(getRunElevationData(runFeature)).toBeNull()
327
- })
328
- })
329
-
330
- describe('getLiftElevationData', () => {
331
- it('returns null for non-LineString geometry', () => {
332
- const liftFeature: LiftFeature = {
333
- type: 'Feature',
334
- geometry: {
335
- type: 'MultiLineString',
336
- coordinates: [
337
- [
338
- [0, 0, 100],
339
- [1, 1, 200],
340
- ],
341
- ],
342
- },
343
- properties: {
344
- type: FeatureType.Lift,
345
- id: '123',
346
- liftType: LiftType.ChairLift,
347
- status: Status.Operating,
348
- access: null,
349
- name: 'Test Lift',
350
- skiAreas: [],
351
- sources: [],
352
- ref: null,
353
- refFRCAIRN: null,
354
- description: null,
355
- oneway: null,
356
- occupancy: null,
357
- capacity: null,
358
- duration: null,
359
- detachable: null,
360
- bubble: null,
361
- heating: null,
362
- stations: [],
363
- websites: [],
364
- wikidataID: null,
365
- tunnel: null,
366
- places: [],
367
- viewportHint: mockViewportHint(),
368
- },
369
- }
370
-
371
- expect(getLiftElevationData(liftFeature)).toBeNull()
372
- })
373
-
374
- it('get max aspect', () => {
375
- const runFeature = {
376
- type: 'Feature',
377
- properties: {
378
- type: 'run',
379
- id: 'c8c91edb337438a2cc8b96f3e3cc42887384de3b',
380
- uses: ['nordic'],
381
- name: 'City-Trail Davos',
382
- ref: '3',
383
- description: 'Golfplatz – Bünda',
384
- difficulty: 'novice',
385
- difficultyConvention: 'europe',
386
- status: 'operating',
387
- oneway: false,
388
- lit: null,
389
- gladed: null,
390
- patrolled: null,
391
- snowmaking: null,
392
- snowfarming: null,
393
- grooming: 'classic+skating',
394
- skiAreas: [],
395
- elevationProfile: {
396
- heights: [
397
- 1546, 1546, 1546, 1546, 1546, 1545, 1546, 1546, 1546, 1546, 1546,
398
- 1545, 1544, 1543, 1543, 1543, 1543, 1543, 1543, 1543, 1544, 1544,
399
- 1545, 1546, 1547, 1547, 1547, 1547, 1547, 1547, 1547, 1547, 1547,
400
- 1547, 1547, 1547, 1547, 1547, 1548, 1549, 1549, 1549, 1549, 1549,
401
- 1550, 1550, 1551, 1552, 1552, 1552, 1552, 1552, 1552, 1552, 1553,
402
- 1554, 1554, 1554, 1555, 1556, 1557, 1557, 1557, 1557, 1558, 1560,
403
- 1563, 1564, 1567, 1569, 1570, 1570, 1572, 1572, 1571, 1570, 1572,
404
- 1574, 1572, 1571, 1571, 1572, 1572, 1573, 1574, 1573, 1572, 1572,
405
- 1573, 1575, 1576, 1577, 1580, 1583, 1584, 1586,
406
- ],
407
- resolution: 24.737575778032642,
408
- targetResolution: 25,
409
- },
410
- sources: [],
411
- websites: [],
412
- wikidataID: null,
413
- tunnel: null,
414
- places: [],
415
- viewportHint: mockViewportHint(),
416
- },
417
- geometry: {
418
- type: 'LineString',
419
- coordinates: [
420
- [9.8353072, 46.797782200000015, 1546],
421
- [9.8353827, 46.797835800000016, 1546],
422
- [9.8356987, 46.798060000000014, 1546],
423
- [9.8360226, 46.79806910000001, 1546],
424
- [9.8380422, 46.79899450000001, 1546],
425
- [9.837935, 46.79945900000001, 1544],
426
- [9.8376614, 46.79963530000002, 1543],
427
- [9.8372698, 46.7996886, 1543],
428
- [9.8370767, 46.79981160000001, 1543],
429
- [9.8370096, 46.79993550000002, 1544],
430
- [9.8371799, 46.8000539, 1543],
431
- [9.837685500000003, 46.8002724, 1543],
432
- [9.8379698, 46.8005066, 1543],
433
- [9.8380396, 46.8006763, 1544],
434
- [9.8379765, 46.8008132, 1544],
435
- [9.837833, 46.80095, 1545],
436
- [9.8378196, 46.8011363, 1546],
437
- [9.8378488, 46.8013207, 1547],
438
- [9.837881, 46.8014058, 1547],
439
- [9.837895, 46.8014252, 1547],
440
- [9.8382166, 46.801688099999986, 1547],
441
- [9.8383936, 46.8019258, 1547],
442
- [9.8386216, 46.8029164, 1547],
443
- [9.8387771, 46.8031256, 1547],
444
- [9.8394209, 46.8034763, 1547],
445
- [9.8397186, 46.8037939, 1547],
446
- [9.8398018, 46.804254799999995, 1548],
447
- [9.8399613, 46.8044383, 1549],
448
- [9.8401469, 46.80448349999998, 1549],
449
- [9.8402551, 46.80451270000001, 1549],
450
- [9.8405166, 46.80462749999998, 1549],
451
- [9.8422614, 46.804981799999986, 1552],
452
- [9.842446900000004, 46.805034, 1552],
453
- [9.8425913, 46.805118600000014, 1552],
454
- [9.8426637, 46.8053545, 1552],
455
- [9.842754900000003, 46.80561510000001, 1552],
456
- [9.8429278, 46.805750300000014, 1552],
457
- [9.8434727, 46.80615580000001, 1553],
458
- [9.8437722, 46.806829199999996, 1554],
459
- [9.8437728, 46.807147199999996, 1555],
460
- [9.8442519, 46.8075484, 1557],
461
- [9.8446648, 46.807873199999996, 1557],
462
- [9.8448358, 46.80802230000001, 1557],
463
- [9.8453494, 46.80833250000001, 1562],
464
- [9.8457157, 46.808526, 1564],
465
- [9.8463504, 46.8087692, 1569],
466
- [9.8467433, 46.8090115, 1570],
467
- [9.8471886, 46.8092391, 1571],
468
- [9.8474304, 46.8093297, 1572],
469
- [9.847923499999998, 46.8095145, 1570],
470
- [9.849087, 46.80979280000001, 1573],
471
- [9.8495311, 46.809877599999986, 1571],
472
- [9.850192000000003, 46.8099654, 1572],
473
- [9.8513658, 46.81011090000001, 1572],
474
- [9.852096400000004, 46.810099599999994, 1573],
475
- [9.852562200000007, 46.810143499999995, 1575],
476
- [9.852670200000004, 46.8103734, 1576],
477
- [9.852746100000006, 46.8105425, 1577],
478
- [9.852914200000006, 46.81067139999999, 1580],
479
- [9.853248800000003, 46.810729599999995, 1583],
480
- [9.853487800000007, 46.810662799999996, 1584],
481
- [9.853535100000004, 46.81066409999998, 1586],
482
- ],
483
- },
484
- } as RunFeature
485
-
486
- const result = getRunElevationData(runFeature)
487
- expect(result?.maxPitchInPercent).toBeCloseTo(0.12)
488
- })
489
- })
490
-
491
- describe('lineChunkPatched', () => {
492
- it('chunks a line into evenly spaced segments', () => {
493
- const geometry: GeoJSON.LineString = {
494
- type: 'LineString',
495
- coordinates: [
496
- [0, 0, 100],
497
- [0, 0.001, 110], // ~111m distance
498
- [0, 0.002, 105], // ~111m distance
499
- ],
500
- }
501
-
502
- const result = lineChunkPatched(geometry, 50)
503
-
504
- // Total length ~222m, with 50m input resolution:
505
- // numSegments = ceil(222/50) = 5, actualResolution = 222/5 ≈ 44.48m
506
- expect(result.resolutionInMeters).toBeCloseTo(44.48)
507
- expect(result.geometry.length).toBe(5)
508
- })
509
-
510
- it('drops very short last segment caused by floating point precision issues', () => {
511
- // This geometry used to produce a 0-length last segment
512
- const geometry: GeoJSON.LineString = {
513
- type: 'LineString',
514
- coordinates: [
515
- [11.0796655, 47.4527256, 1692.2],
516
- [11.079720619903608, 47.45294351099136, 1681.8],
517
- [11.079741829533312, 47.4531647479081, 1668.6],
518
- [11.079727503541202, 47.453383892073894, 1656.7],
519
- [11.07965005139028, 47.453599318647775, 1644.3],
520
- [11.079572598604779, 47.4538147451695, 1630.9],
521
- [11.07942834590789, 47.45401183160125, 1618.7],
522
- [11.079261771477254, 47.45420278959679, 1607.3],
523
- [11.079114389973567, 47.454399266146474, 1594.7],
524
- [11.0790705, 47.45461691834462, 1583.1],
525
- [11.079181898893397, 47.45481861566543, 1576.6],
526
- [11.079441578857713, 47.45494597233321, 1569.5],
527
- [11.079759061257176, 47.454999538815876, 1562],
528
- [11.080078284221905, 47.4550501436389, 1556.1],
529
- [11.080397507801036, 47.45510074757592, 1552.5],
530
- [11.080713989344984, 47.45515789593482, 1548.1],
531
- [11.081008027508169, 47.45525121278351, 1543.2],
532
- [11.081212874246143, 47.455416412781105, 1540.9],
533
- [11.081238270576348, 47.455636585144894, 1535.5],
534
- [11.081208499760809, 47.45585723881769, 1526.8],
535
- [11.081175991164459, 47.456077847750485, 1510.4],
536
- [11.081099444790018, 47.456292146692356, 1497.9],
537
- [11.080996819007606, 47.45650270883295, 1484.3],
538
- [11.080897169545302, 47.45671391641284, 1472.4],
539
- [11.080799550285963, 47.45692556428702, 1456.4],
540
- [11.080701930240755, 47.457137212078344, 1443.9],
541
- [11.08059047871202, 47.4573452579998, 1434.7],
542
- [11.080452900000004, 47.45754649999998, 1427.4],
543
- ],
544
- }
545
-
546
- const result = lineChunkPatched(geometry, 25)
547
-
548
- // Verify that all segments have correct length
549
- for (const chunk of result.geometry) {
550
- const chunkLength = length(
551
- { type: 'Feature', geometry: chunk, properties: {} },
552
- { units: 'meters' },
553
- )
554
- expect(chunkLength).toBeCloseTo(result.resolutionInMeters)
555
- }
556
- })
557
-
558
- it('preserves the exact last point from the original geometry', () => {
559
- // This geometry had a split last point that was different from the input last point
560
- const geometry: GeoJSON.LineString = {
561
- type: 'LineString',
562
- coordinates: [
563
- [11.1215994, 47.466431, 886.6],
564
- [11.118092699999998, 47.4702057, 768.5],
565
- ],
566
- }
567
-
568
- const result = lineChunkPatched(geometry, 25)
569
-
570
- // The last point of the last chunk should be exactly the same as the original last point
571
- const lastChunk = result.geometry[result.geometry.length - 1]
572
- const lastPoint = lastChunk.coordinates[lastChunk.coordinates.length - 1]
573
- const originalLastPoint =
574
- geometry.coordinates[geometry.coordinates.length - 1]
575
-
576
- expect(lastPoint[0]).toBe(originalLastPoint[0])
577
- expect(lastPoint[1]).toBe(originalLastPoint[1])
578
- expect(lastPoint[2]).toBe(originalLastPoint[2])
579
- })
580
-
581
- it('strips elevation from non-original points', () => {
582
- const geometry: GeoJSON.LineString = {
583
- type: 'LineString',
584
- coordinates: [
585
- [0, 0, 100],
586
- [0, 0.001, 110], // ~111m distance
587
- [0, 0.002, 120], // ~111m distance
588
- ],
589
- }
590
-
591
- const result = lineChunkPatched(geometry, 50)
592
-
593
- // Check that interpolated points (not from original geometry) don't have elevation
594
- for (const chunk of result.geometry) {
595
- for (const point of chunk.coordinates) {
596
- const key = `${point[0]},${point[1]}`
597
- const originalKey = geometry.coordinates.some(
598
- (c) => `${c[0]},${c[1]}` === key,
599
- )
600
-
601
- if (!originalKey) {
602
- // Non-original points should not have elevation (should be 2D)
603
- expect(point.length).toBe(2)
604
- } else {
605
- // Original points should still have elevation
606
- expect(point.length).toBe(3)
607
- }
608
- }
609
- }
610
- })
611
-
612
- it('handles very short lines', () => {
613
- const geometry: GeoJSON.LineString = {
614
- type: 'LineString',
615
- coordinates: [
616
- [0, 0, 100],
617
- [0, 0.00001, 101], // ~1.11m distance
618
- ],
619
- }
620
-
621
- const result = lineChunkPatched(geometry, 25)
622
-
623
- // Should create a single chunk for very short lines
624
- expect(result.geometry.length).toBe(1)
625
- expect(result.resolutionInMeters).toBeCloseTo(1.11)
626
- })
627
-
628
- it('handles lines with exact multiples of resolution', () => {
629
- const geometry: GeoJSON.LineString = {
630
- type: 'LineString',
631
- coordinates: [
632
- [0, 0, 100],
633
- [0, 0.0009, 110],
634
- ],
635
- }
636
-
637
- const result = lineChunkPatched(geometry, 50)
638
-
639
- // The actual distance is calculated and divided evenly
640
- // Resolution should be <= 50m
641
- expect(result.resolutionInMeters).toBeLessThanOrEqual(50)
642
- expect(result.geometry.length).toBeGreaterThanOrEqual(1)
643
- })
644
-
645
- it('returns first and last points matching original geometry', () => {
646
- const geometry: GeoJSON.LineString = {
647
- type: 'LineString',
648
- coordinates: [
649
- [11.177425600412874, 47.31265682344346, 100],
650
- [11.177224899122194, 47.312533118812354, 110],
651
- [11.176823496540862, 47.31229807921545, 105],
652
- ],
653
- }
654
-
655
- const result = lineChunkPatched(geometry, 25)
656
-
657
- // First point of first chunk should match original first point
658
- const firstPoint = result.geometry[0].coordinates[0]
659
- expect(firstPoint[0]).toBe(geometry.coordinates[0][0])
660
- expect(firstPoint[1]).toBe(geometry.coordinates[0][1])
661
- expect(firstPoint[2]).toBe(geometry.coordinates[0][2])
662
-
663
- // Last point of last chunk should match original last point
664
- const lastChunk = result.geometry[result.geometry.length - 1]
665
- const lastPoint = lastChunk.coordinates[lastChunk.coordinates.length - 1]
666
- const originalLastPoint =
667
- geometry.coordinates[geometry.coordinates.length - 1]
668
- expect(lastPoint[0]).toBe(originalLastPoint[0])
669
- expect(lastPoint[1]).toBe(originalLastPoint[1])
670
- expect(lastPoint[2]).toBe(originalLastPoint[2])
671
- })
672
-
673
- it('calculates resolution that is less than or equal to requested resolution', () => {
674
- const geometry: GeoJSON.LineString = {
675
- type: 'LineString',
676
- coordinates: [
677
- [0, 0, 100],
678
- [0, 0.0015, 110], // ~166.7m distance
679
- ],
680
- }
681
-
682
- const result = lineChunkPatched(geometry, 50)
683
-
684
- // Resolution should never exceed the requested minimum resolution
685
- expect(result.resolutionInMeters).toBeLessThanOrEqual(50)
686
- })
687
-
688
- it('handles zero-length segments in original geometry', () => {
689
- const geometry: GeoJSON.LineString = {
690
- type: 'LineString',
691
- coordinates: [
692
- [0, 0, 100],
693
- [0, 0, 100], // Duplicate point (0 length segment)
694
- [0, 0.001, 110],
695
- ],
696
- }
697
-
698
- const result = lineChunkPatched(geometry, 25)
699
-
700
- // Should still work and produce valid chunks
701
- expect(result.geometry.length).toBeGreaterThan(0)
702
- expect(result.resolutionInMeters).toBeGreaterThan(0)
703
- })
704
-
705
- it('preserves all original points that fall on chunk boundaries', () => {
706
- const geometry: GeoJSON.LineString = {
707
- type: 'LineString',
708
- coordinates: [
709
- [0, 0, 100],
710
- [0, 0.0005, 105], // ~55.6m - should be close to a chunk boundary with 50m resolution
711
- [0, 0.001, 110],
712
- ],
713
- }
714
-
715
- const result = lineChunkPatched(geometry, 50)
716
-
717
- // Count how many original points are preserved in the chunks
718
- let preservedOriginalPoints = 0
719
- for (const chunk of result.geometry) {
720
- for (const point of chunk.coordinates) {
721
- if (
722
- geometry.coordinates.some(
723
- (c) => c[0] === point[0] && c[1] === point[1],
724
- )
725
- ) {
726
- preservedOriginalPoints++
727
- }
728
- }
729
- }
730
-
731
- // All original points should be present somewhere in the chunks
732
- // (Note: points may appear multiple times if they're at chunk boundaries)
733
- expect(preservedOriginalPoints).toBeGreaterThanOrEqual(
734
- geometry.coordinates.length,
735
- )
736
- })
737
- })
738
- })