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.
- package/dist/ElevationProfile.d.ts +1 -0
- package/dist/ElevationProfile.d.ts.map +1 -0
- package/dist/ElevationProfile.js +15 -24
- package/dist/ElevationProfile.js.map +1 -1
- package/dist/FeatureType.d.ts +1 -0
- package/dist/FeatureType.d.ts.map +1 -0
- package/dist/FeatureType.js +2 -5
- package/dist/FeatureType.js.map +1 -1
- package/dist/Lift.d.ts +10 -8
- package/dist/Lift.d.ts.map +1 -0
- package/dist/Lift.js +22 -29
- package/dist/Lift.js.map +1 -1
- package/dist/LiftNameFormatter.d.ts +2 -1
- package/dist/LiftNameFormatter.d.ts.map +1 -0
- package/dist/LiftNameFormatter.js +11 -14
- package/dist/LiftNameFormatter.js.map +1 -1
- package/dist/Place.d.ts +1 -0
- package/dist/Place.d.ts.map +1 -0
- package/dist/Place.js +1 -2
- package/dist/Run.d.ts +10 -9
- package/dist/Run.d.ts.map +1 -0
- package/dist/Run.js +23 -30
- package/dist/Run.js.map +1 -1
- package/dist/RunDifficultyConvention.d.ts +2 -1
- package/dist/RunDifficultyConvention.d.ts.map +1 -0
- package/dist/RunDifficultyConvention.js +10 -15
- package/dist/RunDifficultyConvention.js.map +1 -1
- package/dist/SkiArea.d.ts +12 -10
- package/dist/SkiArea.d.ts.map +1 -0
- package/dist/SkiArea.js +2 -5
- package/dist/SkiArea.js.map +1 -1
- package/dist/SkiPass.d.ts +32 -7
- package/dist/SkiPass.d.ts.map +1 -0
- package/dist/SkiPass.js +1 -2
- package/dist/SlopeGradingScale.d.ts +3 -2
- package/dist/SlopeGradingScale.d.ts.map +1 -0
- package/dist/SlopeGradingScale.js +21 -26
- package/dist/SlopeGradingScale.js.map +1 -1
- package/dist/SnowCoverHistory.d.ts +1 -0
- package/dist/SnowCoverHistory.d.ts.map +1 -0
- package/dist/SnowCoverHistory.js +9 -8
- package/dist/SnowCoverHistory.js.map +1 -1
- package/dist/Source.d.ts +1 -0
- package/dist/Source.d.ts.map +1 -0
- package/dist/Source.js +11 -17
- package/dist/Source.js.map +1 -1
- package/dist/Spot.d.ts +6 -5
- package/dist/Spot.d.ts.map +1 -0
- package/dist/Spot.js +6 -9
- package/dist/Spot.js.map +1 -1
- package/dist/Status.d.ts +1 -0
- package/dist/Status.d.ts.map +1 -0
- package/dist/Status.js +2 -5
- package/dist/Status.js.map +1 -1
- package/dist/ViewportHint.d.ts +1 -0
- package/dist/ViewportHint.d.ts.map +1 -0
- package/dist/ViewportHint.js +4 -10
- package/dist/ViewportHint.js.map +1 -1
- package/dist/index.d.ts +16 -15
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -18
- package/dist/index.js.map +1 -1
- package/dist/util/exhaustiveMatchingGuard.d.ts +1 -0
- package/dist/util/exhaustiveMatchingGuard.d.ts.map +1 -0
- package/dist/util/exhaustiveMatchingGuard.js +1 -5
- package/dist/util/exhaustiveMatchingGuard.js.map +1 -1
- package/package.json +35 -23
- package/dist/testUtils.d.ts +0 -2
- package/dist/testUtils.js +0 -7
- package/dist/testUtils.js.map +0 -1
- package/src/ElevationProfile.test.ts +0 -738
- package/src/ElevationProfile.ts +0 -452
- package/src/FeatureType.ts +0 -6
- package/src/Lift.test.ts +0 -195
- package/src/Lift.ts +0 -219
- package/src/LiftNameFormatter.ts +0 -116
- package/src/Place.ts +0 -11
- package/src/Run.ts +0 -242
- package/src/RunDifficultyConvention.test.ts +0 -79
- package/src/RunDifficultyConvention.ts +0 -335
- package/src/SkiArea.ts +0 -128
- package/src/SkiPass.ts +0 -62
- package/src/SlopeGradingScale.test.ts +0 -151
- package/src/SlopeGradingScale.ts +0 -96
- package/src/SnowCoverHistory.test.ts +0 -103
- package/src/SnowCoverHistory.ts +0 -107
- package/src/Source.test.ts +0 -50
- package/src/Source.ts +0 -72
- package/src/Spot.ts +0 -140
- package/src/Status.ts +0 -20
- package/src/ViewportHint.ts +0 -208
- package/src/index.ts +0 -15
- package/src/testUtils.ts +0 -5
- 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
|
-
})
|