openskidata-format 2.1.0 → 2.2.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 +30 -1
- package/dist/ElevationProfile.js +139 -0
- package/dist/ElevationProfile.js.map +1 -1
- package/dist/Lift.d.ts +5 -0
- package/dist/Lift.js +16 -0
- package/dist/Lift.js.map +1 -1
- package/dist/Run.d.ts +2 -1
- package/dist/Run.js +12 -0
- package/dist/Run.js.map +1 -1
- package/package.json +10 -2
- package/src/ElevationProfile.test.ts +232 -0
- package/src/ElevationProfile.ts +198 -1
- package/src/Lift.test.ts +24 -0
- package/src/Lift.ts +24 -0
- package/src/Run.ts +18 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { LineString } from 'geojson';
|
|
1
2
|
/**
|
|
2
|
-
* Represents an elevation profile with
|
|
3
|
+
* Represents an elevation profile with height measurements along a LineString.
|
|
3
4
|
*
|
|
4
5
|
* @property {number[]} heights - Array of height measurements in meters. These heights are sampled at regular intervals along the LineString,
|
|
5
6
|
* except for the final height which corresponds to the LineString endpoint and may have a different spacing.
|
|
@@ -10,3 +11,31 @@ export type ElevationProfile = {
|
|
|
10
11
|
heights: number[];
|
|
11
12
|
resolution: number;
|
|
12
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Elevation data that can be computed on demand fom a Run or Lift feature.
|
|
16
|
+
*/
|
|
17
|
+
export type ElevationData = AscentDescentData & PitchData & {
|
|
18
|
+
profileGeometry: GeoJSON.LineString;
|
|
19
|
+
};
|
|
20
|
+
type AscentDescentData = {
|
|
21
|
+
ascentInMeters: number;
|
|
22
|
+
descentInMeters: number;
|
|
23
|
+
minElevationInMeters: number;
|
|
24
|
+
maxElevationInMeters: number;
|
|
25
|
+
verticalInMeters: number;
|
|
26
|
+
};
|
|
27
|
+
type PitchData = {
|
|
28
|
+
averagePitchInPercent: number;
|
|
29
|
+
maxPitchInPercent: number;
|
|
30
|
+
inclinedLengthInMeters: number;
|
|
31
|
+
overallPitchInPercent: number;
|
|
32
|
+
};
|
|
33
|
+
export declare function getElevationData(profileGeometry: GeoJSON.LineString): ElevationData;
|
|
34
|
+
export declare function getPitchData(profileGeometry: GeoJSON.LineString): PitchData;
|
|
35
|
+
export declare function getProfileGeometry(geometry: GeoJSON.LineString, elevationProfile: ElevationProfile): GeoJSON.LineString;
|
|
36
|
+
/**
|
|
37
|
+
* Determines the points to use for an elevation profile.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractPointsForElevationProfile(geometry: LineString, resolution: number): GeoJSON.LineString;
|
|
40
|
+
export declare function getAscentAndDescent(profileGeometry: GeoJSON.LineString): AscentDescentData;
|
|
41
|
+
export {};
|
package/dist/ElevationProfile.js
CHANGED
|
@@ -1,3 +1,142 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getElevationData = getElevationData;
|
|
4
|
+
exports.getPitchData = getPitchData;
|
|
5
|
+
exports.getProfileGeometry = getProfileGeometry;
|
|
6
|
+
exports.extractPointsForElevationProfile = extractPointsForElevationProfile;
|
|
7
|
+
exports.getAscentAndDescent = getAscentAndDescent;
|
|
8
|
+
const tslib_1 = require("tslib");
|
|
9
|
+
const length_1 = tslib_1.__importDefault(require("@turf/length"));
|
|
10
|
+
const line_chunk_1 = require("@turf/line-chunk");
|
|
11
|
+
function getElevationData(profileGeometry) {
|
|
12
|
+
return {
|
|
13
|
+
...getAscentAndDescent(profileGeometry),
|
|
14
|
+
...getPitchData(profileGeometry),
|
|
15
|
+
profileGeometry,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function getPitchData(profileGeometry) {
|
|
19
|
+
const coordinates = profileGeometry.coordinates;
|
|
20
|
+
if (coordinates[0].length < 3) {
|
|
21
|
+
throw 'Elevation data is required for slope analysis';
|
|
22
|
+
}
|
|
23
|
+
let totalLength = 0;
|
|
24
|
+
let totalInclinedLength = 0;
|
|
25
|
+
let totalElevationChange = 0;
|
|
26
|
+
let maxUphillPitch = Number.MIN_VALUE;
|
|
27
|
+
let maxDownhillPitch = Number.MAX_VALUE;
|
|
28
|
+
for (let i = 0; i < coordinates.length - 1; i++) {
|
|
29
|
+
const before = coordinates[i];
|
|
30
|
+
const after = coordinates[i + 1];
|
|
31
|
+
const elevation = after[2] - before[2];
|
|
32
|
+
const geometry = {
|
|
33
|
+
type: 'LineString',
|
|
34
|
+
coordinates: [before, after],
|
|
35
|
+
};
|
|
36
|
+
const feature = {
|
|
37
|
+
type: 'Feature',
|
|
38
|
+
geometry,
|
|
39
|
+
properties: {},
|
|
40
|
+
};
|
|
41
|
+
const lengthInMeters = (0, length_1.default)(feature, { units: 'meters' });
|
|
42
|
+
const percentSlope = elevation / lengthInMeters;
|
|
43
|
+
maxUphillPitch = Math.max(maxUphillPitch, percentSlope);
|
|
44
|
+
maxDownhillPitch = Math.min(maxDownhillPitch, percentSlope);
|
|
45
|
+
totalLength += lengthInMeters;
|
|
46
|
+
totalInclinedLength += Math.sqrt(Math.pow(lengthInMeters, 2) + Math.pow(elevation, 2));
|
|
47
|
+
totalElevationChange += Math.abs(elevation);
|
|
48
|
+
}
|
|
49
|
+
const maxPitch = Math.abs(maxUphillPitch) > Math.abs(maxDownhillPitch)
|
|
50
|
+
? maxUphillPitch
|
|
51
|
+
: maxDownhillPitch;
|
|
52
|
+
const averagePitch = totalElevationChange / totalLength;
|
|
53
|
+
const overallElevationChange = Math.abs(coordinates[coordinates.length - 1][2] - coordinates[0][2]);
|
|
54
|
+
return {
|
|
55
|
+
averagePitchInPercent: averagePitch,
|
|
56
|
+
maxPitchInPercent: maxPitch,
|
|
57
|
+
inclinedLengthInMeters: totalInclinedLength,
|
|
58
|
+
overallPitchInPercent: overallElevationChange / totalLength,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function getProfileGeometry(geometry, elevationProfile) {
|
|
62
|
+
const points = extractPointsForElevationProfile(geometry, elevationProfile.resolution).coordinates;
|
|
63
|
+
const heights = elevationProfile.heights;
|
|
64
|
+
if (points.length !== heights.length) {
|
|
65
|
+
throw 'Mismatch of points & elevation profile.';
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type: 'LineString',
|
|
69
|
+
coordinates: points.map((point, index) => {
|
|
70
|
+
return [point[0], point[1], heights[index]];
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Determines the points to use for an elevation profile.
|
|
76
|
+
*/
|
|
77
|
+
function extractPointsForElevationProfile(geometry, resolution) {
|
|
78
|
+
const subfeatures = (0, line_chunk_1.lineChunk)(geometry, resolution, {
|
|
79
|
+
units: 'meters',
|
|
80
|
+
}).features;
|
|
81
|
+
const points = [];
|
|
82
|
+
for (let subline of subfeatures) {
|
|
83
|
+
const geometry = subline.geometry;
|
|
84
|
+
if (geometry) {
|
|
85
|
+
const point = geometry.coordinates[0];
|
|
86
|
+
points.push([point[0], point[1]]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (subfeatures.length > 0) {
|
|
90
|
+
const geometry = subfeatures[subfeatures.length - 1].geometry;
|
|
91
|
+
if (geometry) {
|
|
92
|
+
const coords = geometry.coordinates;
|
|
93
|
+
if (coords.length > 1) {
|
|
94
|
+
const point = coords[coords.length - 1];
|
|
95
|
+
points.push([point[0], point[1]]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
type: 'LineString',
|
|
101
|
+
coordinates: points,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function getAscentAndDescent(profileGeometry) {
|
|
105
|
+
const coordinates = profileGeometry.coordinates;
|
|
106
|
+
if (coordinates.length === 0) {
|
|
107
|
+
throw 'Empty coordinates are not supported for elevation data analysis';
|
|
108
|
+
}
|
|
109
|
+
if (coordinates[0].length < 3) {
|
|
110
|
+
throw 'Elevation data is required for elevation data analysis';
|
|
111
|
+
}
|
|
112
|
+
const initialElevation = coordinates[0][2];
|
|
113
|
+
const result = coordinates
|
|
114
|
+
.map((point) => point[2])
|
|
115
|
+
.reduce((accumulated, currentElevation) => {
|
|
116
|
+
const ascent = currentElevation - accumulated.lastElevation;
|
|
117
|
+
if (ascent > 0) {
|
|
118
|
+
accumulated.ascent += ascent;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
accumulated.descent -= ascent;
|
|
122
|
+
}
|
|
123
|
+
accumulated.lastElevation = currentElevation;
|
|
124
|
+
accumulated.minElevation = Math.min(currentElevation, accumulated.minElevation);
|
|
125
|
+
accumulated.maxElevation = Math.max(currentElevation, accumulated.maxElevation);
|
|
126
|
+
return accumulated;
|
|
127
|
+
}, {
|
|
128
|
+
ascent: 0,
|
|
129
|
+
descent: 0,
|
|
130
|
+
minElevation: initialElevation,
|
|
131
|
+
maxElevation: initialElevation,
|
|
132
|
+
lastElevation: initialElevation,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
ascentInMeters: result.ascent,
|
|
136
|
+
descentInMeters: result.descent,
|
|
137
|
+
minElevationInMeters: result.minElevation,
|
|
138
|
+
maxElevationInMeters: result.maxElevation,
|
|
139
|
+
verticalInMeters: result.maxElevation - result.minElevation,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
3
142
|
//# sourceMappingURL=ElevationProfile.js.map
|
|
@@ -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/dist/Lift.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ElevationData } from './ElevationProfile';
|
|
1
2
|
import { FeatureType } from './FeatureType';
|
|
2
3
|
import { SkiAreaSummaryFeature } from './SkiArea';
|
|
3
4
|
import { Source } from './Source';
|
|
@@ -68,5 +69,9 @@ export declare enum LiftType {
|
|
|
68
69
|
Funicular = "funicular",
|
|
69
70
|
RackRailway = "rack_railway"
|
|
70
71
|
}
|
|
72
|
+
export type LiftElevationData = ElevationData & {
|
|
73
|
+
speedInMetersPerSecond: number | null;
|
|
74
|
+
};
|
|
75
|
+
export declare function getLiftElevationData(feature: LiftFeature): LiftElevationData | null;
|
|
71
76
|
export declare function getFormattedLiftType(liftType: LiftType): string;
|
|
72
77
|
export declare function getLiftColor(status: Status): string;
|
package/dist/Lift.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LiftType = void 0;
|
|
4
|
+
exports.getLiftElevationData = getLiftElevationData;
|
|
4
5
|
exports.getFormattedLiftType = getFormattedLiftType;
|
|
5
6
|
exports.getLiftColor = getLiftColor;
|
|
7
|
+
const ElevationProfile_1 = require("./ElevationProfile");
|
|
6
8
|
const Status_1 = require("./Status");
|
|
7
9
|
const exhaustiveMatchingGuard_1 = require("./util/exhaustiveMatchingGuard");
|
|
8
10
|
var LiftType;
|
|
@@ -20,6 +22,20 @@ var LiftType;
|
|
|
20
22
|
LiftType["Funicular"] = "funicular";
|
|
21
23
|
LiftType["RackRailway"] = "rack_railway";
|
|
22
24
|
})(LiftType || (exports.LiftType = LiftType = {}));
|
|
25
|
+
function getLiftElevationData(feature) {
|
|
26
|
+
const geometry = feature.geometry;
|
|
27
|
+
if (!geometry || geometry.type !== 'LineString') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const elevationData = (0, ElevationProfile_1.getElevationData)(geometry);
|
|
31
|
+
const durationInSeconds = feature.properties.duration;
|
|
32
|
+
return {
|
|
33
|
+
...elevationData,
|
|
34
|
+
speedInMetersPerSecond: durationInSeconds
|
|
35
|
+
? elevationData.inclinedLengthInMeters / durationInSeconds
|
|
36
|
+
: null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
23
39
|
function getFormattedLiftType(liftType) {
|
|
24
40
|
switch (liftType) {
|
|
25
41
|
case LiftType.CableCar:
|
package/dist/Lift.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Lift.js","sourceRoot":"","sources":["../src/Lift.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"Lift.js","sourceRoot":"","sources":["../src/Lift.ts"],"names":[],"mappings":";;;AAuFA,oDAiBC;AAED,oDA6BC;AAED,oCAgBC;AAzJD,yDAAoE;AAIpE,qCAAiC;AACjC,4EAAwE;AA+DxE,IAAY,QAaX;AAbD,WAAY,QAAQ;IAClB,kCAAsB,CAAA;IACtB,+BAAmB,CAAA;IACnB,oCAAwB,CAAA;IACxB,oCAAwB,CAAA;IACxB,kCAAsB,CAAA;IACtB,0BAAc,CAAA;IACd,0BAAc,CAAA;IACd,+BAAmB,CAAA;IACnB,gCAAoB,CAAA;IACpB,wCAA4B,CAAA;IAC5B,mCAAuB,CAAA;IACvB,wCAA4B,CAAA;AAC9B,CAAC,EAbW,QAAQ,wBAAR,QAAQ,QAanB;AAMD,SAAgB,oBAAoB,CAClC,OAAoB;IAEpB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;IACjC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAChD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,aAAa,GAAG,IAAA,mCAAgB,EAAC,QAAQ,CAAC,CAAA;IAChD,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAA;IAErD,OAAO;QACL,GAAG,aAAa;QAChB,sBAAsB,EAAE,iBAAiB;YACvC,CAAC,CAAC,aAAa,CAAC,sBAAsB,GAAG,iBAAiB;YAC1D,CAAC,CAAC,IAAI;KACT,CAAA;AACH,CAAC;AAED,SAAgB,oBAAoB,CAAC,QAAkB;IACrD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ,CAAC,QAAQ;YACpB,OAAO,WAAW,CAAA;QACpB,KAAK,QAAQ,CAAC,OAAO;YACnB,OAAO,SAAS,CAAA;QAClB,KAAK,QAAQ,CAAC,SAAS;YACrB,OAAO,WAAW,CAAA;QACpB,KAAK,QAAQ,CAAC,SAAS;YACrB,OAAO,QAAQ,CAAA;QACjB,KAAK,QAAQ,CAAC,QAAQ;YACpB,OAAO,WAAW,CAAA;QACpB,KAAK,QAAQ,CAAC,IAAI;YAChB,OAAO,OAAO,CAAA;QAChB,KAAK,QAAQ,CAAC,IAAI;YAChB,OAAO,OAAO,CAAA;QAChB,KAAK,QAAQ,CAAC,OAAO;YACnB,OAAO,SAAS,CAAA;QAClB,KAAK,QAAQ,CAAC,OAAO;YACnB,OAAO,SAAS,CAAA;QAClB,KAAK,QAAQ,CAAC,WAAW;YACvB,OAAO,cAAc,CAAA;QACvB,KAAK,QAAQ,CAAC,SAAS;YACrB,OAAO,WAAW,CAAA;QACpB,KAAK,QAAQ,CAAC,WAAW;YACvB,OAAO,cAAc,CAAA;QACvB;YACE,OAAO,IAAA,iDAAuB,EAAC,QAAQ,CAAC,CAAA;IAC5C,CAAC;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,MAAc;IACzC,MAAM,gBAAgB,GAAG,kBAAkB,CAAA;IAC3C,MAAM,aAAa,GAAG,kBAAkB,CAAA;IAExC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,eAAM,CAAC,OAAO,CAAC;QACpB,KAAK,eAAM,CAAC,SAAS;YACnB,OAAO,aAAa,CAAA;QACtB,KAAK,eAAM,CAAC,QAAQ,CAAC;QACrB,KAAK,eAAM,CAAC,OAAO,CAAC;QACpB,KAAK,eAAM,CAAC,YAAY,CAAC;QACzB,KAAK,eAAM,CAAC,SAAS;YACnB,OAAO,gBAAgB,CAAA;QACzB;YACE,OAAO,IAAA,iDAAuB,EAAC,MAAM,CAAC,CAAA;IAC1C,CAAC;AACH,CAAC"}
|
package/dist/Run.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as GeoJSON from 'geojson';
|
|
2
|
-
import { ElevationProfile } from './ElevationProfile';
|
|
2
|
+
import { ElevationData, ElevationProfile } from './ElevationProfile';
|
|
3
3
|
import { FeatureType } from './FeatureType';
|
|
4
4
|
import { SkiAreaSummaryFeature } from './SkiArea';
|
|
5
5
|
import { Source } from './Source';
|
|
@@ -144,6 +144,7 @@ export declare enum RunDifficultyConvention {
|
|
|
144
144
|
*/
|
|
145
145
|
NORTH_AMERICA = "north_america"
|
|
146
146
|
}
|
|
147
|
+
export declare function getRunElevationData(feature: RunFeature): ElevationData | null;
|
|
147
148
|
export declare function getRunColor(convention: RunDifficultyConvention, difficulty: RunDifficulty | null): RunColorValue;
|
|
148
149
|
export declare function runColorNameToValue(color: RunColorName): RunColorValue;
|
|
149
150
|
export declare function getRunColorName(convention: RunDifficultyConvention, difficulty: RunDifficulty | null): RunColorName;
|
package/dist/Run.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RunDifficultyConvention = exports.RunColorValue = exports.RunColorName = exports.RunDifficulty = exports.RunGrooming = exports.RunUse = void 0;
|
|
4
|
+
exports.getRunElevationData = getRunElevationData;
|
|
4
5
|
exports.getRunColor = getRunColor;
|
|
5
6
|
exports.runColorNameToValue = runColorNameToValue;
|
|
6
7
|
exports.getRunColorName = getRunColorName;
|
|
8
|
+
const ElevationProfile_1 = require("./ElevationProfile");
|
|
7
9
|
const exhaustiveMatchingGuard_1 = require("./util/exhaustiveMatchingGuard");
|
|
8
10
|
var RunUse;
|
|
9
11
|
(function (RunUse) {
|
|
@@ -88,6 +90,16 @@ var RunDifficultyConvention;
|
|
|
88
90
|
*/
|
|
89
91
|
RunDifficultyConvention["NORTH_AMERICA"] = "north_america";
|
|
90
92
|
})(RunDifficultyConvention || (exports.RunDifficultyConvention = RunDifficultyConvention = {}));
|
|
93
|
+
function getRunElevationData(feature) {
|
|
94
|
+
const geometry = feature.geometry;
|
|
95
|
+
const profile = feature.properties.elevationProfile;
|
|
96
|
+
if (!profile || !geometry || geometry.type !== 'LineString') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Get a geometry that includes elevation data at an even spacing along the run
|
|
100
|
+
const profileGeometry = (0, ElevationProfile_1.getProfileGeometry)(geometry, profile);
|
|
101
|
+
return (0, ElevationProfile_1.getElevationData)(profileGeometry);
|
|
102
|
+
}
|
|
91
103
|
function getRunColor(convention, difficulty) {
|
|
92
104
|
return runColorNameToValue(getRunColorName(convention, difficulty));
|
|
93
105
|
}
|
package/dist/Run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Run.js","sourceRoot":"","sources":["../src/Run.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"Run.js","sourceRoot":"","sources":["../src/Run.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openskidata-format",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Data format for OpenSkiMap.org",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "rm -rf dist/ && tsc",
|
|
13
13
|
"release": "npm run-script build && npm publish",
|
|
14
|
-
"test": "
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:watch": "jest --watch",
|
|
16
|
+
"test:coverage": "jest --coverage"
|
|
15
17
|
},
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
@@ -25,9 +27,15 @@
|
|
|
25
27
|
"homepage": "https://github.com/russellporter/openskidata-format#readme",
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@types/geojson": "^7946.0.7",
|
|
30
|
+
"@types/jest": "^29.5.14",
|
|
31
|
+
"jest": "^29.7.0",
|
|
32
|
+
"ts-jest": "^29.2.6",
|
|
28
33
|
"typescript": "^5"
|
|
29
34
|
},
|
|
30
35
|
"dependencies": {
|
|
36
|
+
"@turf/length": "^7.2.0",
|
|
37
|
+
"@turf/line-chunk": "^7.2.0",
|
|
38
|
+
"@turf/meta": "^7.2.0",
|
|
31
39
|
"tslib": "^2"
|
|
32
40
|
}
|
|
33
41
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractPointsForElevationProfile,
|
|
3
|
+
getAscentAndDescent,
|
|
4
|
+
getPitchData,
|
|
5
|
+
} from './ElevationProfile'
|
|
6
|
+
import { FeatureType } from './FeatureType'
|
|
7
|
+
import { getLiftElevationData, LiftFeature, LiftType } from './Lift'
|
|
8
|
+
import {
|
|
9
|
+
getRunElevationData,
|
|
10
|
+
RunDifficulty,
|
|
11
|
+
RunDifficultyConvention,
|
|
12
|
+
RunFeature,
|
|
13
|
+
RunUse,
|
|
14
|
+
} from './Run'
|
|
15
|
+
import { Status } from './Status'
|
|
16
|
+
|
|
17
|
+
describe('ElevationProfile', () => {
|
|
18
|
+
describe('getAscentAndDescent', () => {
|
|
19
|
+
it('calculates ascent and descent correctly from elevation profile', () => {
|
|
20
|
+
const profileGeometry: GeoJSON.LineString = {
|
|
21
|
+
type: 'LineString',
|
|
22
|
+
coordinates: [
|
|
23
|
+
[0, 0, 100], // start at 100m
|
|
24
|
+
[1, 0, 150], // climb to 150m (+50m)
|
|
25
|
+
[2, 0, 130], // descend to 130m (-20m)
|
|
26
|
+
[3, 0, 180], // climb to 180m (+50m)
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = getAscentAndDescent(profileGeometry)
|
|
31
|
+
|
|
32
|
+
expect(result.ascentInMeters).toBe(100) // 50 + 50 = 100m of climbing
|
|
33
|
+
expect(result.descentInMeters).toBe(20) // 20m of descent
|
|
34
|
+
expect(result.minElevationInMeters).toBe(100)
|
|
35
|
+
expect(result.maxElevationInMeters).toBe(180)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('throws error when coordinates are empty', () => {
|
|
39
|
+
const profileGeometry: GeoJSON.LineString = {
|
|
40
|
+
type: 'LineString',
|
|
41
|
+
coordinates: [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(() => getAscentAndDescent(profileGeometry)).toThrow(
|
|
45
|
+
'Empty coordinates are not supported for elevation data analysis',
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('throws error when elevation data is missing', () => {
|
|
50
|
+
const profileGeometry: GeoJSON.LineString = {
|
|
51
|
+
type: 'LineString',
|
|
52
|
+
coordinates: [
|
|
53
|
+
[0, 0],
|
|
54
|
+
[1, 1],
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(() => getAscentAndDescent(profileGeometry)).toThrow(
|
|
59
|
+
'Elevation data is required for elevation data analysis',
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('getPitchData', () => {
|
|
65
|
+
it('calculates pitch data correctly', () => {
|
|
66
|
+
const profileGeometry: GeoJSON.LineString = {
|
|
67
|
+
type: 'LineString',
|
|
68
|
+
coordinates: [
|
|
69
|
+
[11.177425600412874, 47.31265682344346, 100], // start
|
|
70
|
+
[11.177224899122194, 47.312533118812354, 110], // 10m rise over ~20.45m distance = ~49% slope / 22.76m inclined
|
|
71
|
+
[11.176823496540862, 47.31229807921545, 105], // -5m fall over ~39.99m distance = ~-13% slope / 40.3m inclined
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = getPitchData(profileGeometry)
|
|
76
|
+
|
|
77
|
+
expect(result.maxPitchInPercent).toBeCloseTo(0.489)
|
|
78
|
+
expect(result.averagePitchInPercent).toBeCloseTo(0.2482)
|
|
79
|
+
expect(result.inclinedLengthInMeters).toBeCloseTo(63.0597)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('throws error when elevation data is missing', () => {
|
|
83
|
+
const profileGeometry: GeoJSON.LineString = {
|
|
84
|
+
type: 'LineString',
|
|
85
|
+
coordinates: [
|
|
86
|
+
[0, 0],
|
|
87
|
+
[1, 1],
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(() => getPitchData(profileGeometry)).toThrow(
|
|
92
|
+
'Elevation data is required for slope analysis',
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('extractPointsForElevationProfile', () => {
|
|
98
|
+
it('extracts points at regular intervals', () => {
|
|
99
|
+
const geometry: GeoJSON.LineString = {
|
|
100
|
+
type: 'LineString',
|
|
101
|
+
coordinates: [
|
|
102
|
+
[11.177452968770694, 47.312650638218656],
|
|
103
|
+
[11.175409464719593, 47.31138883724759],
|
|
104
|
+
],
|
|
105
|
+
} // 208m distance
|
|
106
|
+
|
|
107
|
+
// With 20m resolution, there should be 12 points
|
|
108
|
+
const result = extractPointsForElevationProfile(geometry, 20)
|
|
109
|
+
|
|
110
|
+
expect(result.coordinates.length).toEqual(12)
|
|
111
|
+
expect(result.coordinates[0]).toEqual([
|
|
112
|
+
11.177452968770694, 47.312650638218656,
|
|
113
|
+
])
|
|
114
|
+
expect(result.coordinates[result.coordinates.length - 1]).toEqual([
|
|
115
|
+
11.175409464719593, 47.31138883724759,
|
|
116
|
+
])
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('getRunElevationData', () => {
|
|
121
|
+
it('returns null for non-LineString geometry', () => {
|
|
122
|
+
const runFeature: RunFeature = {
|
|
123
|
+
type: 'Feature',
|
|
124
|
+
geometry: {
|
|
125
|
+
type: 'Polygon',
|
|
126
|
+
coordinates: [
|
|
127
|
+
[
|
|
128
|
+
[0, 0],
|
|
129
|
+
[0, 1],
|
|
130
|
+
[1, 1],
|
|
131
|
+
[1, 0],
|
|
132
|
+
[0, 0],
|
|
133
|
+
],
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
properties: {
|
|
137
|
+
type: FeatureType.Run,
|
|
138
|
+
id: '123',
|
|
139
|
+
ref: null,
|
|
140
|
+
uses: [RunUse.Downhill],
|
|
141
|
+
difficulty: RunDifficulty.EASY,
|
|
142
|
+
status: Status.Operating,
|
|
143
|
+
name: 'Test Run',
|
|
144
|
+
elevationProfile: null,
|
|
145
|
+
skiAreas: [],
|
|
146
|
+
sources: [],
|
|
147
|
+
description: null,
|
|
148
|
+
difficultyConvention: RunDifficultyConvention.EUROPE,
|
|
149
|
+
oneway: null,
|
|
150
|
+
lit: null,
|
|
151
|
+
gladed: null,
|
|
152
|
+
patrolled: null,
|
|
153
|
+
grooming: null,
|
|
154
|
+
websites: [],
|
|
155
|
+
wikidata_id: null,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
expect(getRunElevationData(runFeature)).toBeNull()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('returns null for missing elevation profile', () => {
|
|
163
|
+
const runFeature: RunFeature = {
|
|
164
|
+
type: 'Feature',
|
|
165
|
+
geometry: {
|
|
166
|
+
type: 'LineString',
|
|
167
|
+
coordinates: [
|
|
168
|
+
[0, 0, 0],
|
|
169
|
+
[1, 1, 0],
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
properties: {
|
|
173
|
+
type: FeatureType.Run,
|
|
174
|
+
id: '123',
|
|
175
|
+
difficulty: RunDifficulty.EASY,
|
|
176
|
+
status: Status.Operating,
|
|
177
|
+
name: 'Test Run',
|
|
178
|
+
elevationProfile: null,
|
|
179
|
+
skiAreas: [],
|
|
180
|
+
sources: [],
|
|
181
|
+
uses: [],
|
|
182
|
+
ref: null,
|
|
183
|
+
description: null,
|
|
184
|
+
difficultyConvention: RunDifficultyConvention.EUROPE,
|
|
185
|
+
oneway: null,
|
|
186
|
+
lit: null,
|
|
187
|
+
gladed: null,
|
|
188
|
+
patrolled: null,
|
|
189
|
+
grooming: null,
|
|
190
|
+
websites: [],
|
|
191
|
+
wikidata_id: null,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
expect(getRunElevationData(runFeature)).toBeNull()
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('getLiftElevationData', () => {
|
|
200
|
+
it('returns null for non-LineString geometry', () => {
|
|
201
|
+
const liftFeature: LiftFeature = {
|
|
202
|
+
type: 'Feature',
|
|
203
|
+
geometry: {
|
|
204
|
+
type: 'Point',
|
|
205
|
+
coordinates: [0, 0, 100],
|
|
206
|
+
},
|
|
207
|
+
properties: {
|
|
208
|
+
type: FeatureType.Lift,
|
|
209
|
+
id: '123',
|
|
210
|
+
liftType: LiftType.ChairLift,
|
|
211
|
+
status: Status.Operating,
|
|
212
|
+
name: 'Test Lift',
|
|
213
|
+
skiAreas: [],
|
|
214
|
+
sources: [],
|
|
215
|
+
ref: null,
|
|
216
|
+
description: null,
|
|
217
|
+
oneway: null,
|
|
218
|
+
occupancy: null,
|
|
219
|
+
capacity: null,
|
|
220
|
+
duration: null,
|
|
221
|
+
detachable: null,
|
|
222
|
+
bubble: null,
|
|
223
|
+
heating: null,
|
|
224
|
+
websites: [],
|
|
225
|
+
wikidata_id: null,
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
expect(getLiftElevationData(liftFeature)).toBeNull()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
})
|
package/src/ElevationProfile.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import length from '@turf/length'
|
|
2
|
+
import { lineChunk } from '@turf/line-chunk'
|
|
3
|
+
import { LineString } from 'geojson'
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
* Represents an elevation profile with
|
|
6
|
+
* Represents an elevation profile with height measurements along a LineString.
|
|
3
7
|
*
|
|
4
8
|
* @property {number[]} heights - Array of height measurements in meters. These heights are sampled at regular intervals along the LineString,
|
|
5
9
|
* except for the final height which corresponds to the LineString endpoint and may have a different spacing.
|
|
@@ -10,3 +14,196 @@ export type ElevationProfile = {
|
|
|
10
14
|
heights: number[]
|
|
11
15
|
resolution: number
|
|
12
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Elevation data that can be computed on demand fom a Run or Lift feature.
|
|
20
|
+
*/
|
|
21
|
+
export type ElevationData = AscentDescentData &
|
|
22
|
+
PitchData & {
|
|
23
|
+
profileGeometry: GeoJSON.LineString
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type AscentDescentData = {
|
|
27
|
+
ascentInMeters: number
|
|
28
|
+
descentInMeters: number
|
|
29
|
+
minElevationInMeters: number
|
|
30
|
+
maxElevationInMeters: number
|
|
31
|
+
verticalInMeters: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type PitchData = {
|
|
35
|
+
averagePitchInPercent: number
|
|
36
|
+
maxPitchInPercent: number
|
|
37
|
+
inclinedLengthInMeters: number
|
|
38
|
+
overallPitchInPercent: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getElevationData(
|
|
42
|
+
profileGeometry: GeoJSON.LineString,
|
|
43
|
+
): ElevationData {
|
|
44
|
+
return {
|
|
45
|
+
...getAscentAndDescent(profileGeometry),
|
|
46
|
+
...getPitchData(profileGeometry),
|
|
47
|
+
profileGeometry,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getPitchData(profileGeometry: GeoJSON.LineString): PitchData {
|
|
52
|
+
const coordinates = profileGeometry.coordinates
|
|
53
|
+
if (coordinates[0].length < 3) {
|
|
54
|
+
throw 'Elevation data is required for slope analysis'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let totalLength = 0
|
|
58
|
+
let totalInclinedLength = 0
|
|
59
|
+
let totalElevationChange = 0
|
|
60
|
+
let maxUphillPitch = Number.MIN_VALUE
|
|
61
|
+
let maxDownhillPitch = Number.MAX_VALUE
|
|
62
|
+
for (let i = 0; i < coordinates.length - 1; i++) {
|
|
63
|
+
const before = coordinates[i]
|
|
64
|
+
const after = coordinates[i + 1]
|
|
65
|
+
const elevation = after[2] - before[2]
|
|
66
|
+
const geometry: GeoJSON.LineString = {
|
|
67
|
+
type: 'LineString',
|
|
68
|
+
coordinates: [before, after],
|
|
69
|
+
}
|
|
70
|
+
const feature: GeoJSON.Feature = {
|
|
71
|
+
type: 'Feature',
|
|
72
|
+
geometry,
|
|
73
|
+
properties: {},
|
|
74
|
+
}
|
|
75
|
+
const lengthInMeters = length(feature, { units: 'meters' })
|
|
76
|
+
|
|
77
|
+
const percentSlope = elevation / lengthInMeters
|
|
78
|
+
maxUphillPitch = Math.max(maxUphillPitch, percentSlope)
|
|
79
|
+
maxDownhillPitch = Math.min(maxDownhillPitch, percentSlope)
|
|
80
|
+
|
|
81
|
+
totalLength += lengthInMeters
|
|
82
|
+
totalInclinedLength += Math.sqrt(
|
|
83
|
+
Math.pow(lengthInMeters, 2) + Math.pow(elevation, 2),
|
|
84
|
+
)
|
|
85
|
+
totalElevationChange += Math.abs(elevation)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const maxPitch =
|
|
89
|
+
Math.abs(maxUphillPitch) > Math.abs(maxDownhillPitch)
|
|
90
|
+
? maxUphillPitch
|
|
91
|
+
: maxDownhillPitch
|
|
92
|
+
const averagePitch = totalElevationChange / totalLength
|
|
93
|
+
const overallElevationChange = Math.abs(
|
|
94
|
+
coordinates[coordinates.length - 1][2] - coordinates[0][2],
|
|
95
|
+
)
|
|
96
|
+
return {
|
|
97
|
+
averagePitchInPercent: averagePitch,
|
|
98
|
+
maxPitchInPercent: maxPitch,
|
|
99
|
+
inclinedLengthInMeters: totalInclinedLength,
|
|
100
|
+
overallPitchInPercent: overallElevationChange / totalLength,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getProfileGeometry(
|
|
105
|
+
geometry: GeoJSON.LineString,
|
|
106
|
+
elevationProfile: ElevationProfile,
|
|
107
|
+
): GeoJSON.LineString {
|
|
108
|
+
const points = extractPointsForElevationProfile(
|
|
109
|
+
geometry,
|
|
110
|
+
elevationProfile.resolution,
|
|
111
|
+
).coordinates
|
|
112
|
+
const heights = elevationProfile.heights
|
|
113
|
+
if (points.length !== heights.length) {
|
|
114
|
+
throw 'Mismatch of points & elevation profile.'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
type: 'LineString',
|
|
119
|
+
coordinates: points.map((point, index) => {
|
|
120
|
+
return [point[0], point[1], heights[index]]
|
|
121
|
+
}),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Determines the points to use for an elevation profile.
|
|
127
|
+
*/
|
|
128
|
+
export function extractPointsForElevationProfile(
|
|
129
|
+
geometry: LineString,
|
|
130
|
+
resolution: number,
|
|
131
|
+
): GeoJSON.LineString {
|
|
132
|
+
const subfeatures = lineChunk(geometry, resolution, {
|
|
133
|
+
units: 'meters',
|
|
134
|
+
}).features
|
|
135
|
+
const points: GeoJSON.Position[] = []
|
|
136
|
+
for (let subline of subfeatures) {
|
|
137
|
+
const geometry = subline.geometry
|
|
138
|
+
if (geometry) {
|
|
139
|
+
const point = geometry.coordinates[0]
|
|
140
|
+
points.push([point[0], point[1]])
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (subfeatures.length > 0) {
|
|
144
|
+
const geometry = subfeatures[subfeatures.length - 1].geometry
|
|
145
|
+
if (geometry) {
|
|
146
|
+
const coords = geometry.coordinates
|
|
147
|
+
if (coords.length > 1) {
|
|
148
|
+
const point = coords[coords.length - 1]
|
|
149
|
+
points.push([point[0], point[1]])
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
type: 'LineString',
|
|
156
|
+
coordinates: points,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getAscentAndDescent(
|
|
161
|
+
profileGeometry: GeoJSON.LineString,
|
|
162
|
+
): AscentDescentData {
|
|
163
|
+
const coordinates = profileGeometry.coordinates
|
|
164
|
+
if (coordinates.length === 0) {
|
|
165
|
+
throw 'Empty coordinates are not supported for elevation data analysis'
|
|
166
|
+
}
|
|
167
|
+
if (coordinates[0].length < 3) {
|
|
168
|
+
throw 'Elevation data is required for elevation data analysis'
|
|
169
|
+
}
|
|
170
|
+
const initialElevation = coordinates[0][2]
|
|
171
|
+
const result = coordinates
|
|
172
|
+
.map((point) => point[2])
|
|
173
|
+
.reduce(
|
|
174
|
+
(accumulated, currentElevation) => {
|
|
175
|
+
const ascent = currentElevation - accumulated.lastElevation
|
|
176
|
+
if (ascent > 0) {
|
|
177
|
+
accumulated.ascent += ascent
|
|
178
|
+
} else {
|
|
179
|
+
accumulated.descent -= ascent
|
|
180
|
+
}
|
|
181
|
+
accumulated.lastElevation = currentElevation
|
|
182
|
+
accumulated.minElevation = Math.min(
|
|
183
|
+
currentElevation,
|
|
184
|
+
accumulated.minElevation,
|
|
185
|
+
)
|
|
186
|
+
accumulated.maxElevation = Math.max(
|
|
187
|
+
currentElevation,
|
|
188
|
+
accumulated.maxElevation,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return accumulated
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
ascent: 0,
|
|
195
|
+
descent: 0,
|
|
196
|
+
minElevation: initialElevation,
|
|
197
|
+
maxElevation: initialElevation,
|
|
198
|
+
lastElevation: initialElevation,
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ascentInMeters: result.ascent,
|
|
204
|
+
descentInMeters: result.descent,
|
|
205
|
+
minElevationInMeters: result.minElevation,
|
|
206
|
+
maxElevationInMeters: result.maxElevation,
|
|
207
|
+
verticalInMeters: result.maxElevation - result.minElevation,
|
|
208
|
+
}
|
|
209
|
+
}
|
package/src/Lift.test.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LiftType, getFormattedLiftType, getLiftColor } from './Lift';
|
|
2
|
+
import { Status } from './Status';
|
|
3
|
+
|
|
4
|
+
describe('Lift', () => {
|
|
5
|
+
describe('getFormattedLiftType', () => {
|
|
6
|
+
it('should format lift types correctly', () => {
|
|
7
|
+
expect(getFormattedLiftType(LiftType.ChairLift)).toBe('Chairlift');
|
|
8
|
+
expect(getFormattedLiftType(LiftType.Gondola)).toBe('Gondola');
|
|
9
|
+
expect(getFormattedLiftType(LiftType.MagicCarpet)).toBe('Magic Carpet');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('getLiftColor', () => {
|
|
14
|
+
it('should return dim red for disused lifts', () => {
|
|
15
|
+
expect(getLiftColor(Status.Disused)).toBe('hsl(0, 53%, 42%)');
|
|
16
|
+
expect(getLiftColor(Status.Abandoned)).toBe('hsl(0, 53%, 42%)');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should return bright red for operating lifts', () => {
|
|
20
|
+
expect(getLiftColor(Status.Operating)).toBe('hsl(0, 82%, 42%)');
|
|
21
|
+
expect(getLiftColor(Status.Construction)).toBe('hsl(0, 82%, 42%)');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/Lift.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ElevationData, getElevationData } from './ElevationProfile'
|
|
1
2
|
import { FeatureType } from './FeatureType'
|
|
2
3
|
import { SkiAreaSummaryFeature } from './SkiArea'
|
|
3
4
|
import { Source } from './Source'
|
|
@@ -80,6 +81,29 @@ export enum LiftType {
|
|
|
80
81
|
RackRailway = 'rack_railway',
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
export type LiftElevationData = ElevationData & {
|
|
85
|
+
speedInMetersPerSecond: number | null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getLiftElevationData(
|
|
89
|
+
feature: LiftFeature,
|
|
90
|
+
): LiftElevationData | null {
|
|
91
|
+
const geometry = feature.geometry
|
|
92
|
+
if (!geometry || geometry.type !== 'LineString') {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const elevationData = getElevationData(geometry)
|
|
97
|
+
const durationInSeconds = feature.properties.duration
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...elevationData,
|
|
101
|
+
speedInMetersPerSecond: durationInSeconds
|
|
102
|
+
? elevationData.inclinedLengthInMeters / durationInSeconds
|
|
103
|
+
: null,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
export function getFormattedLiftType(liftType: LiftType): string {
|
|
84
108
|
switch (liftType) {
|
|
85
109
|
case LiftType.CableCar:
|
package/src/Run.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as GeoJSON from 'geojson'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ElevationData,
|
|
4
|
+
ElevationProfile,
|
|
5
|
+
getElevationData,
|
|
6
|
+
getProfileGeometry,
|
|
7
|
+
} from './ElevationProfile'
|
|
3
8
|
import { FeatureType } from './FeatureType'
|
|
4
9
|
import { SkiAreaSummaryFeature } from './SkiArea'
|
|
5
10
|
import { Source } from './Source'
|
|
@@ -157,6 +162,18 @@ export enum RunDifficultyConvention {
|
|
|
157
162
|
NORTH_AMERICA = 'north_america',
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
export function getRunElevationData(feature: RunFeature): ElevationData | null {
|
|
166
|
+
const geometry = feature.geometry
|
|
167
|
+
const profile = feature.properties.elevationProfile
|
|
168
|
+
if (!profile || !geometry || geometry.type !== 'LineString') {
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get a geometry that includes elevation data at an even spacing along the run
|
|
173
|
+
const profileGeometry = getProfileGeometry(geometry, profile)
|
|
174
|
+
return getElevationData(profileGeometry)
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
export function getRunColor(
|
|
161
178
|
convention: RunDifficultyConvention,
|
|
162
179
|
difficulty: RunDifficulty | null,
|