openskidata-format 9.0.0 → 10.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.
@@ -6,10 +6,12 @@ import { LineString } from 'geojson';
6
6
  * except for the final height which corresponds to the LineString endpoint and may have a different spacing.
7
7
  * Height values can be mapped to geographical coordinates using Turf.js: `turf.lineChunk(geometry, resolution, {units: 'meters'})`.
8
8
  * @property {number} resolution - The horizontal sampling distance in meters between consecutive height measurements.
9
+ * @property {number} targetResolution - The horizontal sampling distance in meters that was requested for the elevation profile. The actual resolution may be less to ensure an integer number of segments along the line. This is used for reconstructing the profile geometry from the feature geometry.
9
10
  */
10
11
  export type ElevationProfile = {
11
12
  heights: number[];
12
13
  resolution: number;
14
+ targetResolution: number;
13
15
  };
14
16
  /**
15
17
  * Elevation data that can be computed on demand fom a Run or Lift feature.
@@ -32,11 +34,33 @@ type PitchData = {
32
34
  pitchCalculationResolutionInMeters: number;
33
35
  };
34
36
  export declare function getElevationData(profileGeometry: GeoJSON.LineString): ElevationData;
35
- export declare function getPitchData(profileGeometry: GeoJSON.LineString, resolutionInMeters?: number): PitchData;
37
+ export declare function getPitchData(profileGeometry: GeoJSON.LineString, minResolutionInMeters?: number): PitchData;
36
38
  export declare function getProfileGeometry(geometry: GeoJSON.LineString, elevationProfile: ElevationProfile): GeoJSON.LineString;
37
39
  /**
38
- * Determines the points to use for an elevation profile.
40
+ * Determines the points to use for an elevation profile, given a minimum horizontal resolution. The resolution is selected to have evenly spaced points along the line.
39
41
  */
40
- export declare function extractPointsForElevationProfile(geometry: LineString, resolution: number): GeoJSON.LineString;
42
+ export declare function extractPointsForElevationProfile(geometry: LineString, minResolutionInMeters: number): {
43
+ resolutionInMeters: number;
44
+ geometry: GeoJSON.LineString;
45
+ };
41
46
  export declare function getAscentAndDescent(profileGeometry: GeoJSON.LineString): AscentDescentData;
47
+ /**
48
+ * Turf's lineChunk has several issues:
49
+ * - it doesn't allow for a custom resolution that divides the line evenly, which is important for consistent pitch calculations
50
+ * change: calculate a resolution that divides the line into an integer number of segments, with a maximum of the provided resolution
51
+ * - it includes elevations from the original geometry on newly created points which are wrong (https://github.com/Turfjs/turf/issues/3007)
52
+ * fix: strip Z coordinates from non-original points
53
+ * - it can produce a last point that is very close but not exactly the same as the original endpoint
54
+ * fix: set the last point to be exactly the original endpoint
55
+ * - it can produce two points very close to the end of line due to floating point precision issues
56
+ * fix: drop the second to last point if it's very close to the end
57
+ *
58
+ * @param geometry
59
+ * @param minResolutionInMeters
60
+ * @returns
61
+ */
62
+ export declare function lineChunkPatched(geometry: LineString, minResolutionInMeters: number): {
63
+ resolutionInMeters: number;
64
+ geometry: GeoJSON.LineString[];
65
+ };
42
66
  export {};
@@ -5,6 +5,7 @@ exports.getPitchData = getPitchData;
5
5
  exports.getProfileGeometry = getProfileGeometry;
6
6
  exports.extractPointsForElevationProfile = extractPointsForElevationProfile;
7
7
  exports.getAscentAndDescent = getAscentAndDescent;
8
+ exports.lineChunkPatched = lineChunkPatched;
8
9
  const tslib_1 = require("tslib");
9
10
  const distance_1 = tslib_1.__importDefault(require("@turf/distance"));
10
11
  const length_1 = tslib_1.__importDefault(require("@turf/length"));
@@ -16,7 +17,7 @@ function getElevationData(profileGeometry) {
16
17
  profileGeometry,
17
18
  };
18
19
  }
19
- function getPitchData(profileGeometry, resolutionInMeters = 25) {
20
+ function getPitchData(profileGeometry, minResolutionInMeters = 25) {
20
21
  const coordinates = profileGeometry.coordinates;
21
22
  if (coordinates[0].length < 3) {
22
23
  throw 'Elevation data is required for slope analysis';
@@ -47,7 +48,7 @@ function getPitchData(profileGeometry, resolutionInMeters = 25) {
47
48
  // If the line is too short relative to the resolution, pitch calculations are unreliable
48
49
  // due to elevation data resolution. A 1m elevation change over 1m distance would be a 45 degree slope,
49
50
  // which can easily happen due to elevation data precision issues.
50
- if (totalLength < resolutionInMeters / 2) {
51
+ if (totalLength < minResolutionInMeters / 2) {
51
52
  return {
52
53
  averagePitchInPercent: null,
53
54
  maxPitchInPercent: null,
@@ -56,18 +57,8 @@ function getPitchData(profileGeometry, resolutionInMeters = 25) {
56
57
  pitchCalculationResolutionInMeters: totalLength,
57
58
  };
58
59
  }
59
- // Calculate a resolution that divides the distance evenly, with the constraint that it must be <= resolutionInMeters
60
- const numSegments = Math.ceil(totalLength / resolutionInMeters);
61
- const actualResolution = totalLength / numSegments;
62
60
  // Chunk the line into segments using the calculated resolution for max pitch calculation
63
- const chunkedGeometries = (0, line_chunk_1.lineChunk)(profileGeometry, actualResolution, {
64
- units: 'meters',
65
- }).features.map((feature) => feature.geometry);
66
- // turf 7.3+ adds Z coordinates to newly created points using segment-end elevation,
67
- // but we need distance-based interpolation. Strip Z from non-original vertices so
68
- // interpolateElevation can add them correctly.
69
- // https://github.com/Turfjs/turf/issues/3007
70
- stripNonOriginalElevations(chunkedGeometries, coordinates);
61
+ const { geometry: chunkedGeometries, resolutionInMeters: actualResolutionInMeters, } = lineChunkPatched(profileGeometry, minResolutionInMeters);
71
62
  // Mutates the geometries in place to add elevation data
72
63
  interpolateElevation(chunkedGeometries);
73
64
  // Calculate max pitch over the evenly-divided chunks
@@ -90,14 +81,18 @@ function getPitchData(profileGeometry, resolutionInMeters = 25) {
90
81
  maxPitchInPercent: maxPitchValue,
91
82
  inclinedLengthInMeters: totalInclinedLength,
92
83
  overallPitchInPercent: overallElevationChange / totalLength,
93
- pitchCalculationResolutionInMeters: actualResolution,
84
+ pitchCalculationResolutionInMeters: actualResolutionInMeters,
94
85
  };
95
86
  }
96
87
  function getProfileGeometry(geometry, elevationProfile) {
97
- const profileLine = extractPointsForElevationProfile(geometry, elevationProfile.resolution);
88
+ const { geometry: profileLine, resolutionInMeters: actualResolution } = extractPointsForElevationProfile(geometry, elevationProfile.targetResolution);
98
89
  if (profileLine.coordinates.length !== elevationProfile.heights.length) {
99
90
  throw `Mismatch of points & elevation profile`;
100
91
  }
92
+ if (Math.abs(actualResolution - elevationProfile.resolution) >
93
+ elevationProfile.resolution * 0.001) {
94
+ throw `Resolution mismatch between profile geometry (${actualResolution}) and elevation profile (${elevationProfile.resolution})`;
95
+ }
101
96
  for (let i = 0; i < profileLine.coordinates.length; i++) {
102
97
  const point = profileLine.coordinates[i];
103
98
  const height = elevationProfile.heights[i];
@@ -106,13 +101,10 @@ function getProfileGeometry(geometry, elevationProfile) {
106
101
  return profileLine;
107
102
  }
108
103
  /**
109
- * Determines the points to use for an elevation profile.
104
+ * Determines the points to use for an elevation profile, given a minimum horizontal resolution. The resolution is selected to have evenly spaced points along the line.
110
105
  */
111
- function extractPointsForElevationProfile(geometry, resolution) {
112
- // Important: Z coordinates in the output of lineChunk may be incorrect, ignore them (https://github.com/Turfjs/turf/issues/3007)
113
- const lineChunks = (0, line_chunk_1.lineChunk)(geometry, resolution, {
114
- units: 'meters',
115
- }).features.map((feature) => feature.geometry);
106
+ function extractPointsForElevationProfile(geometry, minResolutionInMeters) {
107
+ const { geometry: lineChunks, resolutionInMeters } = lineChunkPatched(geometry, minResolutionInMeters);
116
108
  const points = [];
117
109
  for (let subline of lineChunks) {
118
110
  const point = subline.coordinates[0];
@@ -127,8 +119,11 @@ function extractPointsForElevationProfile(geometry, resolution) {
127
119
  }
128
120
  }
129
121
  return {
130
- type: 'LineString',
131
- coordinates: points,
122
+ resolutionInMeters,
123
+ geometry: {
124
+ type: 'LineString',
125
+ coordinates: points,
126
+ },
132
127
  };
133
128
  }
134
129
  function getAscentAndDescent(profileGeometry) {
@@ -169,6 +164,53 @@ function getAscentAndDescent(profileGeometry) {
169
164
  verticalInMeters: result.maxElevation - result.minElevation,
170
165
  };
171
166
  }
167
+ /**
168
+ * Turf's lineChunk has several issues:
169
+ * - it doesn't allow for a custom resolution that divides the line evenly, which is important for consistent pitch calculations
170
+ * change: calculate a resolution that divides the line into an integer number of segments, with a maximum of the provided resolution
171
+ * - it includes elevations from the original geometry on newly created points which are wrong (https://github.com/Turfjs/turf/issues/3007)
172
+ * fix: strip Z coordinates from non-original points
173
+ * - it can produce a last point that is very close but not exactly the same as the original endpoint
174
+ * fix: set the last point to be exactly the original endpoint
175
+ * - it can produce two points very close to the end of line due to floating point precision issues
176
+ * fix: drop the second to last point if it's very close to the end
177
+ *
178
+ * @param geometry
179
+ * @param minResolutionInMeters
180
+ * @returns
181
+ */
182
+ function lineChunkPatched(geometry, minResolutionInMeters) {
183
+ const resolutionInMeters = horizontalResolutionFitting(geometry, minResolutionInMeters);
184
+ const lineChunks = (0, line_chunk_1.lineChunk)(geometry, resolutionInMeters, {
185
+ units: 'meters',
186
+ }).features.map((feature) => feature.geometry);
187
+ // Check the last segment has the expected length
188
+ const lastSegment = lineChunks[lineChunks.length - 1];
189
+ const lastSegmentLength = (0, length_1.default)({ type: 'Feature', geometry: lastSegment, properties: {} }, { units: 'meters' });
190
+ if (lastSegmentLength < resolutionInMeters * 0.01) {
191
+ // If the last segment is very short, which can happen due to floating point precision issues, drop it to avoid issues with elevation interpolation
192
+ lineChunks.pop();
193
+ }
194
+ // Set the last point from the original coordinates, turf line chunk can sometimes produce a last point that
195
+ // is very close but not exactly the same as the original endpoint, which can cause issues with elevation interpolation
196
+ const lastOriginalPoint = geometry.coordinates[geometry.coordinates.length - 1];
197
+ const lastChunkCoords = lineChunks[lineChunks.length - 1].coordinates;
198
+ lastChunkCoords[lastChunkCoords.length - 1] = lastOriginalPoint;
199
+ // turf 7.3+ adds Z coordinates to newly created points using segment-end elevation,
200
+ // but we need distance-based interpolation. Strip Z from non-original vertices so
201
+ // interpolateElevation can add them correctly.
202
+ // https://github.com/Turfjs/turf/issues/3007
203
+ stripNonOriginalElevations(lineChunks, geometry.coordinates);
204
+ return { resolutionInMeters, geometry: lineChunks };
205
+ }
206
+ function horizontalResolutionFitting(geometry, minResolution) {
207
+ const totalLength = (0, length_1.default)({ type: 'Feature', geometry, properties: {} }, { units: 'meters' });
208
+ if (totalLength === 0) {
209
+ return 0;
210
+ }
211
+ const numSegments = Math.ceil(totalLength / minResolution);
212
+ return totalLength / numSegments;
213
+ }
172
214
  /**
173
215
  * Strip Z coordinates from points that were created by lineChunk (not original vertices).
174
216
  * Original vertices are identified by matching lon/lat coordinates.
@@ -224,7 +266,7 @@ function interpolateElevation(geometries) {
224
266
  // Find elevation reference points
225
267
  const referencePoints = allPoints.filter((p) => p.hasElevation);
226
268
  if (referencePoints.length <= 1) {
227
- throw 'At least two points with elevation data are required';
269
+ throw `At least two points with elevation data are required ${JSON.stringify(allPoints)}`;
228
270
  }
229
271
  // Process segments between each pair of reference points
230
272
  for (let i = 0; i < referencePoints.length - 1; i++) {
@@ -1 +1 @@
1
- {"version":3,"file":"ElevationProfile.js","sourceRoot":"","sources":["../src/ElevationProfile.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ElevationProfile.js","sourceRoot":"","sources":["../src/ElevationProfile.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openskidata-format",
3
- "version": "9.0.0",
3
+ "version": "10.0.0",
4
4
  "description": "Data format for OpenSkiMap.org",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,16 +1,13 @@
1
+ import length from '@turf/length'
1
2
  import {
2
3
  extractPointsForElevationProfile,
3
4
  getAscentAndDescent,
4
5
  getPitchData,
6
+ lineChunkPatched,
5
7
  } from './ElevationProfile'
6
8
  import { FeatureType } from './FeatureType'
7
9
  import { getLiftElevationData, LiftFeature, LiftType } from './Lift'
8
- import {
9
- getRunElevationData,
10
- RunDifficulty,
11
- RunFeature,
12
- RunUse,
13
- } from './Run'
10
+ import { getRunElevationData, RunDifficulty, RunFeature, RunUse } from './Run'
14
11
  import { RunDifficultyConvention } from './RunDifficultyConvention'
15
12
  import { Status } from './Status'
16
13
 
@@ -75,10 +72,10 @@ describe('ElevationProfile', () => {
75
72
  const result = getPitchData(profileGeometry)
76
73
 
77
74
  // With the new resolution calculation, the actual resolution divides the distance evenly
78
- // Total length is ~60.44m, with default resolution of 25m: numSegments = ceil(60.44/25) = 3, actualResolution = 60.44/3 ≈ 20.15m
79
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(20.15, 1)
75
+ // Total length is ~60.44m, with default resolution of 25m: numSegments = ceil(60.44/25) = 3, actualResolution = 60.44/3 ≈ 20.14m
76
+ expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(20.14)
80
77
  expect(result.pitchCalculationResolutionInMeters).toBeLessThanOrEqual(25)
81
- expect(result.maxPitchInPercent).toBeCloseTo(0.489, 2)
78
+ expect(result.maxPitchInPercent).toBeCloseTo(0.489)
82
79
  expect(result.averagePitchInPercent).toBeCloseTo(0.2482)
83
80
  expect(result.inclinedLengthInMeters).toBeCloseTo(63.0597)
84
81
  })
@@ -119,8 +116,8 @@ describe('ElevationProfile', () => {
119
116
 
120
117
  // With ~222m total length and 50m input resolution:
121
118
  // numSegments = ceil(222/50) = 5
122
- // actualResolution = 222/5 = 44.4m
123
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(44.4, 0)
119
+ // actualResolution = 222/5 44.48m
120
+ expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(44.48)
124
121
  })
125
122
 
126
123
  it('uses total length as resolution when line is shorter than input resolution', () => {
@@ -135,8 +132,8 @@ describe('ElevationProfile', () => {
135
132
  const result = getPitchData(profileGeometry, 50)
136
133
 
137
134
  // Resolution should equal the total length since numSegments = ceil(11/50) = 1
138
- // actualResolution = 11/1 = 11m
139
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(11.1, 0)
135
+ // actualResolution = 11/1 11.12m
136
+ expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(11.12)
140
137
  expect(result.pitchCalculationResolutionInMeters).toBeLessThanOrEqual(50)
141
138
  })
142
139
 
@@ -156,7 +153,7 @@ describe('ElevationProfile', () => {
156
153
  // With evenly divided chunks, no chunks should be skipped
157
154
  // The max pitch should be calculated from all segments
158
155
  expect(result.maxPitchInPercent).toBeGreaterThan(0)
159
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(20.15, 1)
156
+ expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(20.14)
160
157
  })
161
158
 
162
159
  it('returns null pitch data for very short lines', () => {
@@ -172,7 +169,7 @@ describe('ElevationProfile', () => {
172
169
 
173
170
  // Lines shorter than half the resolution should return null pitch data
174
171
  // because elevation data resolution makes pitch calculations unreliable
175
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(1.11, 1)
172
+ expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(1.11)
176
173
  expect(result.maxPitchInPercent).toBeNull()
177
174
  expect(result.averagePitchInPercent).toBeNull()
178
175
  expect(result.overallPitchInPercent).toBeNull()
@@ -191,7 +188,7 @@ describe('ElevationProfile', () => {
191
188
  const result = getPitchData(profileGeometry, 25)
192
189
 
193
190
  // Lines at or above half the resolution should calculate pitch data
194
- expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(16.7, 0)
191
+ expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(16.68)
195
192
  expect(result.maxPitchInPercent).not.toBeNull()
196
193
  expect(result.averagePitchInPercent).not.toBeNull()
197
194
  expect(result.overallPitchInPercent).not.toBeNull()
@@ -211,13 +208,13 @@ describe('ElevationProfile', () => {
211
208
  // With 20m resolution, there should be 12 points
212
209
  const result = extractPointsForElevationProfile(geometry, 20)
213
210
 
214
- expect(result.coordinates.length).toEqual(12)
215
- expect(result.coordinates[0]).toEqual([
211
+ expect(result.geometry.coordinates.length).toEqual(12)
212
+ expect(result.geometry.coordinates[0]).toEqual([
216
213
  11.177452968770694, 47.312650638218656,
217
214
  ])
218
- expect(result.coordinates[result.coordinates.length - 1]).toEqual([
219
- 11.175409464719593, 47.31138883724759,
220
- ])
215
+ expect(
216
+ result.geometry.coordinates[result.geometry.coordinates.length - 1],
217
+ ).toEqual([11.175409464719593, 47.31138883724759])
221
218
  })
222
219
 
223
220
  it('handles points that already have elevation', () => {
@@ -232,9 +229,9 @@ describe('ElevationProfile', () => {
232
229
  const result = extractPointsForElevationProfile(geometry, 20)
233
230
 
234
231
  // Should still work properly, returning 2D points regardless of input dimension
235
- expect(result.coordinates.length).toBeGreaterThan(1)
236
- expect(result.coordinates[0].length).toBe(2) // Should always return 2D points
237
- expect(result.coordinates[0]).toEqual([
232
+ expect(result.geometry.coordinates.length).toBeGreaterThan(1)
233
+ expect(result.geometry.coordinates[0].length).toBe(2) // Should always return 2D points
234
+ expect(result.geometry.coordinates[0]).toEqual([
238
235
  11.177452968770694, 47.312650638218656,
239
236
  ])
240
237
  })
@@ -331,7 +328,12 @@ describe('ElevationProfile', () => {
331
328
  type: 'Feature',
332
329
  geometry: {
333
330
  type: 'MultiLineString',
334
- coordinates: [[[0, 0, 100], [1, 1, 200]]],
331
+ coordinates: [
332
+ [
333
+ [0, 0, 100],
334
+ [1, 1, 200],
335
+ ],
336
+ ],
335
337
  },
336
338
  properties: {
337
339
  type: FeatureType.Lift,
@@ -395,7 +397,8 @@ describe('ElevationProfile', () => {
395
397
  1574, 1572, 1571, 1571, 1572, 1572, 1573, 1574, 1573, 1572, 1572,
396
398
  1573, 1575, 1576, 1577, 1580, 1583, 1584, 1586,
397
399
  ],
398
- resolution: 25,
400
+ resolution: 24.737575778032642,
401
+ targetResolution: 25,
399
402
  },
400
403
  sources: [],
401
404
  websites: [],
@@ -472,7 +475,255 @@ describe('ElevationProfile', () => {
472
475
  } as RunFeature
473
476
 
474
477
  const result = getRunElevationData(runFeature)
475
- expect(result?.maxPitchInPercent).toBeCloseTo(0.12, 2)
478
+ expect(result?.maxPitchInPercent).toBeCloseTo(0.12)
479
+ })
480
+ })
481
+
482
+ describe('lineChunkPatched', () => {
483
+ it('chunks a line into evenly spaced segments', () => {
484
+ const geometry: GeoJSON.LineString = {
485
+ type: 'LineString',
486
+ coordinates: [
487
+ [0, 0, 100],
488
+ [0, 0.001, 110], // ~111m distance
489
+ [0, 0.002, 105], // ~111m distance
490
+ ],
491
+ }
492
+
493
+ const result = lineChunkPatched(geometry, 50)
494
+
495
+ // Total length ~222m, with 50m input resolution:
496
+ // numSegments = ceil(222/50) = 5, actualResolution = 222/5 ≈ 44.48m
497
+ expect(result.resolutionInMeters).toBeCloseTo(44.48)
498
+ expect(result.geometry.length).toBe(5)
499
+ })
500
+
501
+ it('drops very short last segment caused by floating point precision issues', () => {
502
+ // This geometry used to produce a 0-length last segment
503
+ const geometry: GeoJSON.LineString = {
504
+ type: 'LineString',
505
+ coordinates: [
506
+ [11.0796655, 47.4527256, 1692.2],
507
+ [11.079720619903608, 47.45294351099136, 1681.8],
508
+ [11.079741829533312, 47.4531647479081, 1668.6],
509
+ [11.079727503541202, 47.453383892073894, 1656.7],
510
+ [11.07965005139028, 47.453599318647775, 1644.3],
511
+ [11.079572598604779, 47.4538147451695, 1630.9],
512
+ [11.07942834590789, 47.45401183160125, 1618.7],
513
+ [11.079261771477254, 47.45420278959679, 1607.3],
514
+ [11.079114389973567, 47.454399266146474, 1594.7],
515
+ [11.0790705, 47.45461691834462, 1583.1],
516
+ [11.079181898893397, 47.45481861566543, 1576.6],
517
+ [11.079441578857713, 47.45494597233321, 1569.5],
518
+ [11.079759061257176, 47.454999538815876, 1562],
519
+ [11.080078284221905, 47.4550501436389, 1556.1],
520
+ [11.080397507801036, 47.45510074757592, 1552.5],
521
+ [11.080713989344984, 47.45515789593482, 1548.1],
522
+ [11.081008027508169, 47.45525121278351, 1543.2],
523
+ [11.081212874246143, 47.455416412781105, 1540.9],
524
+ [11.081238270576348, 47.455636585144894, 1535.5],
525
+ [11.081208499760809, 47.45585723881769, 1526.8],
526
+ [11.081175991164459, 47.456077847750485, 1510.4],
527
+ [11.081099444790018, 47.456292146692356, 1497.9],
528
+ [11.080996819007606, 47.45650270883295, 1484.3],
529
+ [11.080897169545302, 47.45671391641284, 1472.4],
530
+ [11.080799550285963, 47.45692556428702, 1456.4],
531
+ [11.080701930240755, 47.457137212078344, 1443.9],
532
+ [11.08059047871202, 47.4573452579998, 1434.7],
533
+ [11.080452900000004, 47.45754649999998, 1427.4],
534
+ ],
535
+ }
536
+
537
+ const result = lineChunkPatched(geometry, 25)
538
+
539
+ // Verify that all segments have correct length
540
+ for (const chunk of result.geometry) {
541
+ const chunkLength = length(
542
+ { type: 'Feature', geometry: chunk, properties: {} },
543
+ { units: 'meters' },
544
+ )
545
+ expect(chunkLength).toBeCloseTo(result.resolutionInMeters)
546
+ }
547
+ })
548
+
549
+ it('preserves the exact last point from the original geometry', () => {
550
+ // This geometry had a split last point that was different from the input last point
551
+ const geometry: GeoJSON.LineString = {
552
+ type: 'LineString',
553
+ coordinates: [
554
+ [11.1215994, 47.466431, 886.6],
555
+ [11.118092699999998, 47.4702057, 768.5],
556
+ ],
557
+ }
558
+
559
+ const result = lineChunkPatched(geometry, 25)
560
+
561
+ // The last point of the last chunk should be exactly the same as the original last point
562
+ const lastChunk = result.geometry[result.geometry.length - 1]
563
+ const lastPoint = lastChunk.coordinates[lastChunk.coordinates.length - 1]
564
+ const originalLastPoint =
565
+ geometry.coordinates[geometry.coordinates.length - 1]
566
+
567
+ expect(lastPoint[0]).toBe(originalLastPoint[0])
568
+ expect(lastPoint[1]).toBe(originalLastPoint[1])
569
+ expect(lastPoint[2]).toBe(originalLastPoint[2])
570
+ })
571
+
572
+ it('strips elevation from non-original points', () => {
573
+ const geometry: GeoJSON.LineString = {
574
+ type: 'LineString',
575
+ coordinates: [
576
+ [0, 0, 100],
577
+ [0, 0.001, 110], // ~111m distance
578
+ [0, 0.002, 120], // ~111m distance
579
+ ],
580
+ }
581
+
582
+ const result = lineChunkPatched(geometry, 50)
583
+
584
+ // Check that interpolated points (not from original geometry) don't have elevation
585
+ for (const chunk of result.geometry) {
586
+ for (const point of chunk.coordinates) {
587
+ const key = `${point[0]},${point[1]}`
588
+ const originalKey = geometry.coordinates.some(
589
+ (c) => `${c[0]},${c[1]}` === key,
590
+ )
591
+
592
+ if (!originalKey) {
593
+ // Non-original points should not have elevation (should be 2D)
594
+ expect(point.length).toBe(2)
595
+ } else {
596
+ // Original points should still have elevation
597
+ expect(point.length).toBe(3)
598
+ }
599
+ }
600
+ }
601
+ })
602
+
603
+ it('handles very short lines', () => {
604
+ const geometry: GeoJSON.LineString = {
605
+ type: 'LineString',
606
+ coordinates: [
607
+ [0, 0, 100],
608
+ [0, 0.00001, 101], // ~1.11m distance
609
+ ],
610
+ }
611
+
612
+ const result = lineChunkPatched(geometry, 25)
613
+
614
+ // Should create a single chunk for very short lines
615
+ expect(result.geometry.length).toBe(1)
616
+ expect(result.resolutionInMeters).toBeCloseTo(1.11)
617
+ })
618
+
619
+ it('handles lines with exact multiples of resolution', () => {
620
+ const geometry: GeoJSON.LineString = {
621
+ type: 'LineString',
622
+ coordinates: [
623
+ [0, 0, 100],
624
+ [0, 0.0009, 110],
625
+ ],
626
+ }
627
+
628
+ const result = lineChunkPatched(geometry, 50)
629
+
630
+ // The actual distance is calculated and divided evenly
631
+ // Resolution should be <= 50m
632
+ expect(result.resolutionInMeters).toBeLessThanOrEqual(50)
633
+ expect(result.geometry.length).toBeGreaterThanOrEqual(1)
634
+ })
635
+
636
+ it('returns first and last points matching original geometry', () => {
637
+ const geometry: GeoJSON.LineString = {
638
+ type: 'LineString',
639
+ coordinates: [
640
+ [11.177425600412874, 47.31265682344346, 100],
641
+ [11.177224899122194, 47.312533118812354, 110],
642
+ [11.176823496540862, 47.31229807921545, 105],
643
+ ],
644
+ }
645
+
646
+ const result = lineChunkPatched(geometry, 25)
647
+
648
+ // First point of first chunk should match original first point
649
+ const firstPoint = result.geometry[0].coordinates[0]
650
+ expect(firstPoint[0]).toBe(geometry.coordinates[0][0])
651
+ expect(firstPoint[1]).toBe(geometry.coordinates[0][1])
652
+ expect(firstPoint[2]).toBe(geometry.coordinates[0][2])
653
+
654
+ // Last point of last chunk should match original last point
655
+ const lastChunk = result.geometry[result.geometry.length - 1]
656
+ const lastPoint = lastChunk.coordinates[lastChunk.coordinates.length - 1]
657
+ const originalLastPoint =
658
+ geometry.coordinates[geometry.coordinates.length - 1]
659
+ expect(lastPoint[0]).toBe(originalLastPoint[0])
660
+ expect(lastPoint[1]).toBe(originalLastPoint[1])
661
+ expect(lastPoint[2]).toBe(originalLastPoint[2])
662
+ })
663
+
664
+ it('calculates resolution that is less than or equal to requested resolution', () => {
665
+ const geometry: GeoJSON.LineString = {
666
+ type: 'LineString',
667
+ coordinates: [
668
+ [0, 0, 100],
669
+ [0, 0.0015, 110], // ~166.7m distance
670
+ ],
671
+ }
672
+
673
+ const result = lineChunkPatched(geometry, 50)
674
+
675
+ // Resolution should never exceed the requested minimum resolution
676
+ expect(result.resolutionInMeters).toBeLessThanOrEqual(50)
677
+ })
678
+
679
+ it('handles zero-length segments in original geometry', () => {
680
+ const geometry: GeoJSON.LineString = {
681
+ type: 'LineString',
682
+ coordinates: [
683
+ [0, 0, 100],
684
+ [0, 0, 100], // Duplicate point (0 length segment)
685
+ [0, 0.001, 110],
686
+ ],
687
+ }
688
+
689
+ const result = lineChunkPatched(geometry, 25)
690
+
691
+ // Should still work and produce valid chunks
692
+ expect(result.geometry.length).toBeGreaterThan(0)
693
+ expect(result.resolutionInMeters).toBeGreaterThan(0)
694
+ })
695
+
696
+ it('preserves all original points that fall on chunk boundaries', () => {
697
+ const geometry: GeoJSON.LineString = {
698
+ type: 'LineString',
699
+ coordinates: [
700
+ [0, 0, 100],
701
+ [0, 0.0005, 105], // ~55.6m - should be close to a chunk boundary with 50m resolution
702
+ [0, 0.001, 110],
703
+ ],
704
+ }
705
+
706
+ const result = lineChunkPatched(geometry, 50)
707
+
708
+ // Count how many original points are preserved in the chunks
709
+ let preservedOriginalPoints = 0
710
+ for (const chunk of result.geometry) {
711
+ for (const point of chunk.coordinates) {
712
+ if (
713
+ geometry.coordinates.some(
714
+ (c) => c[0] === point[0] && c[1] === point[1],
715
+ )
716
+ ) {
717
+ preservedOriginalPoints++
718
+ }
719
+ }
720
+ }
721
+
722
+ // All original points should be present somewhere in the chunks
723
+ // (Note: points may appear multiple times if they're at chunk boundaries)
724
+ expect(preservedOriginalPoints).toBeGreaterThanOrEqual(
725
+ geometry.coordinates.length,
726
+ )
476
727
  })
477
728
  })
478
729
  })
@@ -10,10 +10,12 @@ import { LineString } from 'geojson'
10
10
  * except for the final height which corresponds to the LineString endpoint and may have a different spacing.
11
11
  * Height values can be mapped to geographical coordinates using Turf.js: `turf.lineChunk(geometry, resolution, {units: 'meters'})`.
12
12
  * @property {number} resolution - The horizontal sampling distance in meters between consecutive height measurements.
13
+ * @property {number} targetResolution - The horizontal sampling distance in meters that was requested for the elevation profile. The actual resolution may be less to ensure an integer number of segments along the line. This is used for reconstructing the profile geometry from the feature geometry.
13
14
  */
14
15
  export type ElevationProfile = {
15
16
  heights: number[]
16
17
  resolution: number
18
+ targetResolution: number
17
19
  }
18
20
 
19
21
  /**
@@ -52,7 +54,7 @@ export function getElevationData(
52
54
 
53
55
  export function getPitchData(
54
56
  profileGeometry: GeoJSON.LineString,
55
- resolutionInMeters: number = 25, // Default resolution of 25 meters for pitch calculation
57
+ minResolutionInMeters: number = 25, // Default resolution of 25 meters for pitch calculation
56
58
  ): PitchData {
57
59
  const coordinates = profileGeometry.coordinates
58
60
  if (coordinates[0].length < 3) {
@@ -90,7 +92,7 @@ export function getPitchData(
90
92
  // If the line is too short relative to the resolution, pitch calculations are unreliable
91
93
  // due to elevation data resolution. A 1m elevation change over 1m distance would be a 45 degree slope,
92
94
  // which can easily happen due to elevation data precision issues.
93
- if (totalLength < resolutionInMeters / 2) {
95
+ if (totalLength < minResolutionInMeters / 2) {
94
96
  return {
95
97
  averagePitchInPercent: null,
96
98
  maxPitchInPercent: null,
@@ -100,20 +102,11 @@ export function getPitchData(
100
102
  }
101
103
  }
102
104
 
103
- // Calculate a resolution that divides the distance evenly, with the constraint that it must be <= resolutionInMeters
104
- const numSegments = Math.ceil(totalLength / resolutionInMeters)
105
- const actualResolution = totalLength / numSegments
106
-
107
105
  // Chunk the line into segments using the calculated resolution for max pitch calculation
108
- const chunkedGeometries = lineChunk(profileGeometry, actualResolution, {
109
- units: 'meters',
110
- }).features.map((feature) => feature.geometry)
111
-
112
- // turf 7.3+ adds Z coordinates to newly created points using segment-end elevation,
113
- // but we need distance-based interpolation. Strip Z from non-original vertices so
114
- // interpolateElevation can add them correctly.
115
- // https://github.com/Turfjs/turf/issues/3007
116
- stripNonOriginalElevations(chunkedGeometries, coordinates)
106
+ const {
107
+ geometry: chunkedGeometries,
108
+ resolutionInMeters: actualResolutionInMeters,
109
+ } = lineChunkPatched(profileGeometry, minResolutionInMeters)
117
110
 
118
111
  // Mutates the geometries in place to add elevation data
119
112
  interpolateElevation(chunkedGeometries)
@@ -148,7 +141,7 @@ export function getPitchData(
148
141
  maxPitchInPercent: maxPitchValue,
149
142
  inclinedLengthInMeters: totalInclinedLength,
150
143
  overallPitchInPercent: overallElevationChange / totalLength,
151
- pitchCalculationResolutionInMeters: actualResolution,
144
+ pitchCalculationResolutionInMeters: actualResolutionInMeters,
152
145
  }
153
146
  }
154
147
 
@@ -156,14 +149,22 @@ export function getProfileGeometry(
156
149
  geometry: GeoJSON.LineString,
157
150
  elevationProfile: ElevationProfile,
158
151
  ): GeoJSON.LineString {
159
- const profileLine = extractPointsForElevationProfile(
160
- geometry,
161
- elevationProfile.resolution,
162
- )
152
+ const { geometry: profileLine, resolutionInMeters: actualResolution } =
153
+ extractPointsForElevationProfile(
154
+ geometry,
155
+ elevationProfile.targetResolution,
156
+ )
163
157
  if (profileLine.coordinates.length !== elevationProfile.heights.length) {
164
158
  throw `Mismatch of points & elevation profile`
165
159
  }
166
160
 
161
+ if (
162
+ Math.abs(actualResolution - elevationProfile.resolution) >
163
+ elevationProfile.resolution * 0.001
164
+ ) {
165
+ throw `Resolution mismatch between profile geometry (${actualResolution}) and elevation profile (${elevationProfile.resolution})`
166
+ }
167
+
167
168
  for (let i = 0; i < profileLine.coordinates.length; i++) {
168
169
  const point = profileLine.coordinates[i]
169
170
  const height = elevationProfile.heights[i]
@@ -174,16 +175,16 @@ export function getProfileGeometry(
174
175
  }
175
176
 
176
177
  /**
177
- * Determines the points to use for an elevation profile.
178
+ * Determines the points to use for an elevation profile, given a minimum horizontal resolution. The resolution is selected to have evenly spaced points along the line.
178
179
  */
179
180
  export function extractPointsForElevationProfile(
180
181
  geometry: LineString,
181
- resolution: number,
182
- ): GeoJSON.LineString {
183
- // Important: Z coordinates in the output of lineChunk may be incorrect, ignore them (https://github.com/Turfjs/turf/issues/3007)
184
- const lineChunks = lineChunk(geometry, resolution, {
185
- units: 'meters',
186
- }).features.map((feature) => feature.geometry)
182
+ minResolutionInMeters: number,
183
+ ): { resolutionInMeters: number; geometry: GeoJSON.LineString } {
184
+ const { geometry: lineChunks, resolutionInMeters } = lineChunkPatched(
185
+ geometry,
186
+ minResolutionInMeters,
187
+ )
187
188
 
188
189
  const points: GeoJSON.Position[] = []
189
190
  for (let subline of lineChunks) {
@@ -200,8 +201,11 @@ export function extractPointsForElevationProfile(
200
201
  }
201
202
 
202
203
  return {
203
- type: 'LineString',
204
- coordinates: points,
204
+ resolutionInMeters,
205
+ geometry: {
206
+ type: 'LineString',
207
+ coordinates: points,
208
+ },
205
209
  }
206
210
  }
207
211
 
@@ -256,6 +260,78 @@ export function getAscentAndDescent(
256
260
  }
257
261
  }
258
262
 
263
+ /**
264
+ * Turf's lineChunk has several issues:
265
+ * - it doesn't allow for a custom resolution that divides the line evenly, which is important for consistent pitch calculations
266
+ * change: calculate a resolution that divides the line into an integer number of segments, with a maximum of the provided resolution
267
+ * - it includes elevations from the original geometry on newly created points which are wrong (https://github.com/Turfjs/turf/issues/3007)
268
+ * fix: strip Z coordinates from non-original points
269
+ * - it can produce a last point that is very close but not exactly the same as the original endpoint
270
+ * fix: set the last point to be exactly the original endpoint
271
+ * - it can produce two points very close to the end of line due to floating point precision issues
272
+ * fix: drop the second to last point if it's very close to the end
273
+ *
274
+ * @param geometry
275
+ * @param minResolutionInMeters
276
+ * @returns
277
+ */
278
+ export function lineChunkPatched(
279
+ geometry: LineString,
280
+ minResolutionInMeters: number,
281
+ ): { resolutionInMeters: number; geometry: GeoJSON.LineString[] } {
282
+ const resolutionInMeters = horizontalResolutionFitting(
283
+ geometry,
284
+ minResolutionInMeters,
285
+ )
286
+
287
+ const lineChunks = lineChunk(geometry, resolutionInMeters, {
288
+ units: 'meters',
289
+ }).features.map((feature) => feature.geometry)
290
+
291
+ // Check the last segment has the expected length
292
+ const lastSegment = lineChunks[lineChunks.length - 1]
293
+ const lastSegmentLength = length(
294
+ { type: 'Feature', geometry: lastSegment, properties: {} },
295
+ { units: 'meters' },
296
+ )
297
+ if (lastSegmentLength < resolutionInMeters * 0.01) {
298
+ // If the last segment is very short, which can happen due to floating point precision issues, drop it to avoid issues with elevation interpolation
299
+ lineChunks.pop()
300
+ }
301
+
302
+ // Set the last point from the original coordinates, turf line chunk can sometimes produce a last point that
303
+ // is very close but not exactly the same as the original endpoint, which can cause issues with elevation interpolation
304
+ const lastOriginalPoint =
305
+ geometry.coordinates[geometry.coordinates.length - 1]
306
+ const lastChunkCoords = lineChunks[lineChunks.length - 1].coordinates
307
+ lastChunkCoords[lastChunkCoords.length - 1] = lastOriginalPoint
308
+
309
+ // turf 7.3+ adds Z coordinates to newly created points using segment-end elevation,
310
+ // but we need distance-based interpolation. Strip Z from non-original vertices so
311
+ // interpolateElevation can add them correctly.
312
+ // https://github.com/Turfjs/turf/issues/3007
313
+ stripNonOriginalElevations(lineChunks, geometry.coordinates)
314
+
315
+ return { resolutionInMeters, geometry: lineChunks }
316
+ }
317
+
318
+ function horizontalResolutionFitting(
319
+ geometry: LineString,
320
+ minResolution: number,
321
+ ): number {
322
+ const totalLength = length(
323
+ { type: 'Feature', geometry, properties: {} },
324
+ { units: 'meters' },
325
+ )
326
+
327
+ if (totalLength === 0) {
328
+ return 0
329
+ }
330
+
331
+ const numSegments = Math.ceil(totalLength / minResolution)
332
+ return totalLength / numSegments
333
+ }
334
+
259
335
  /**
260
336
  * Strip Z coordinates from points that were created by lineChunk (not original vertices).
261
337
  * Original vertices are identified by matching lon/lat coordinates.
@@ -328,7 +404,7 @@ function interpolateElevation(geometries: GeoJSON.LineString[]) {
328
404
  const referencePoints = allPoints.filter((p) => p.hasElevation)
329
405
 
330
406
  if (referencePoints.length <= 1) {
331
- throw 'At least two points with elevation data are required'
407
+ throw `At least two points with elevation data are required ${JSON.stringify(allPoints)}`
332
408
  }
333
409
 
334
410
  // Process segments between each pair of reference points