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.
- package/dist/ElevationProfile.d.ts +27 -3
- package/dist/ElevationProfile.js +66 -24
- package/dist/ElevationProfile.js.map +1 -1
- package/package.json +1 -1
- package/src/ElevationProfile.test.ts +278 -27
- package/src/ElevationProfile.ts +106 -30
|
@@ -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,
|
|
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,
|
|
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 {};
|
package/dist/ElevationProfile.js
CHANGED
|
@@ -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,
|
|
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 <
|
|
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
|
|
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:
|
|
84
|
+
pitchCalculationResolutionInMeters: actualResolutionInMeters,
|
|
94
85
|
};
|
|
95
86
|
}
|
|
96
87
|
function getProfileGeometry(geometry, elevationProfile) {
|
|
97
|
-
const profileLine = extractPointsForElevationProfile(geometry, elevationProfile.
|
|
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,
|
|
112
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
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":";;
|
|
1
|
+
{"version":3,"file":"ElevationProfile.js","sourceRoot":"","sources":["../src/ElevationProfile.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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.
|
|
79
|
-
expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(20.
|
|
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
|
|
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
|
|
123
|
-
expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(44.
|
|
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
|
|
139
|
-
expect(result.pitchCalculationResolutionInMeters).toBeCloseTo(11.
|
|
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.
|
|
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
|
|
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.
|
|
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(
|
|
219
|
-
|
|
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: [
|
|
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:
|
|
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
|
|
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
|
})
|
package/src/ElevationProfile.ts
CHANGED
|
@@ -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
|
-
|
|
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 <
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
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 =
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
182
|
-
): GeoJSON.LineString {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
|
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
|