openskidata-format 11.0.0 → 12.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/Lift.d.ts CHANGED
@@ -5,6 +5,7 @@ import { SkiAreaSummaryFeature } from './SkiArea';
5
5
  import { Source } from './Source';
6
6
  import { LiftStationSpotFeature } from './Spot';
7
7
  import { Status } from './Status';
8
+ import { ViewportHint } from './ViewportHint';
8
9
  export type LiftFeature = GeoJSON.Feature<LiftGeometry, LiftProperties>;
9
10
  export type LiftGeometry = GeoJSON.LineString | GeoJSON.MultiLineString;
10
11
  /**
@@ -72,6 +73,7 @@ export type LiftProperties = {
72
73
  websites: string[];
73
74
  wikidataID: string | null;
74
75
  places: Place[];
76
+ viewportHint: ViewportHint;
75
77
  };
76
78
  export declare enum LiftType {
77
79
  CableCar = "cable_car",
package/dist/Lift.js CHANGED
@@ -4,7 +4,10 @@ exports.LiftType = void 0;
4
4
  exports.getLiftElevationData = getLiftElevationData;
5
5
  exports.getFormattedLiftType = getFormattedLiftType;
6
6
  exports.getLiftColor = getLiftColor;
7
+ const tslib_1 = require("tslib");
8
+ const distance_1 = tslib_1.__importDefault(require("@turf/distance"));
7
9
  const ElevationProfile_1 = require("./ElevationProfile");
10
+ const Spot_1 = require("./Spot");
8
11
  const Status_1 = require("./Status");
9
12
  const exhaustiveMatchingGuard_1 = require("./util/exhaustiveMatchingGuard");
10
13
  var LiftType;
@@ -31,16 +34,37 @@ function getLiftElevationData(feature) {
31
34
  }
32
35
  const elevationData = (0, ElevationProfile_1.getElevationData)(geometry);
33
36
  const durationInSeconds = feature.properties.duration;
37
+ const terminalDistances = getTerminalStationDistances(feature);
38
+ const inclinedLength = terminalDistances?.inclinedLengthInMeters ??
39
+ elevationData.inclinedLengthInMeters;
40
+ const verticalLength = terminalDistances?.verticalInMeters ?? elevationData.verticalInMeters;
34
41
  return {
35
42
  ...elevationData,
36
43
  speedInMetersPerSecond: durationInSeconds
37
- ? elevationData.inclinedLengthInMeters / durationInSeconds
44
+ ? inclinedLength / durationInSeconds
38
45
  : null,
39
46
  verticalSpeedInMetersPerSecond: durationInSeconds
40
- ? elevationData.verticalInMeters / durationInSeconds
47
+ ? verticalLength / durationInSeconds
41
48
  : null,
42
49
  };
43
50
  }
51
+ function getTerminalStationDistances(feature) {
52
+ const stations = feature.properties.stations;
53
+ const topStation = stations.find((s) => s.properties.position === Spot_1.LiftStationPosition.Top);
54
+ const bottomStation = stations.find((s) => s.properties.position === Spot_1.LiftStationPosition.Bottom);
55
+ if (!topStation || !bottomStation) {
56
+ return null;
57
+ }
58
+ const topCoords = topStation.geometry.coordinates;
59
+ const bottomCoords = bottomStation.geometry.coordinates;
60
+ if (topCoords.length < 3 || bottomCoords.length < 3) {
61
+ return null;
62
+ }
63
+ const horizontalDistanceInMeters = (0, distance_1.default)(topStation.geometry, bottomStation.geometry, { units: 'meters' });
64
+ const verticalInMeters = Math.abs(topCoords[2] - bottomCoords[2]);
65
+ const inclinedLengthInMeters = Math.sqrt(Math.pow(horizontalDistanceInMeters, 2) + Math.pow(verticalInMeters, 2));
66
+ return { inclinedLengthInMeters, verticalInMeters };
67
+ }
44
68
  function getFormattedLiftType(liftType) {
45
69
  switch (liftType) {
46
70
  case LiftType.CableCar:
package/dist/Lift.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Lift.js","sourceRoot":"","sources":["../src/Lift.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Lift.js","sourceRoot":"","sources":["../src/Lift.ts"],"names":[],"mappings":"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"}
package/dist/Run.d.ts CHANGED
@@ -7,6 +7,7 @@ import { SkiAreaSummaryFeature } from './SkiArea';
7
7
  import { SnowCoverHistory } from './SnowCoverHistory';
8
8
  import { Source } from './Source';
9
9
  import { Status } from './Status';
10
+ import { ViewportHint } from './ViewportHint';
10
11
  export type RunGeometry = GeoJSON.LineString | GeoJSON.Polygon;
11
12
  /**
12
13
  * Represents a segment of a ski run/trail.
@@ -79,6 +80,7 @@ export type RunProperties = {
79
80
  wikidataID: string | null;
80
81
  places: Place[];
81
82
  snowCoverHistory?: SnowCoverHistory;
83
+ viewportHint: ViewportHint;
82
84
  };
83
85
  export declare enum RunUse {
84
86
  Downhill = "downhill",
package/dist/Run.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Run.js","sourceRoot":"","sources":["../src/Run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Run.js","sourceRoot":"","sources":["../src/Run.ts"],"names":[],"mappings":";;;AAkJA,kDAUC;AAED,kCAKC;AAED,kDAiBC;AAED,0CAyDC;AAhPD,yDAK2B;AAG3B,uEAAmE;AAKnE,4EAAwE;AAgFxE,IAAY,MAYX;AAZD,WAAY,MAAM;IAChB,+BAAqB,CAAA;IACrB,2BAAiB,CAAA;IACjB,6BAAmB,CAAA;IACnB,uBAAa,CAAA;IACb,uBAAa,CAAA;IACb,2BAAiB,CAAA;IACjB,gCAAsB,CAAA;IACtB,gCAAsB,CAAA;IACtB,mCAAyB,CAAA;IACzB,mCAAyB,CAAA;IACzB,6BAAmB,CAAA;AACrB,CAAC,EAZW,MAAM,sBAAN,MAAM,QAYjB;AAED,IAAY,WAOX;AAPD,WAAY,WAAW;IACrB,kCAAmB,CAAA;IACnB,8BAAe,CAAA;IACf,oDAAqC,CAAA;IACrC,kCAAmB,CAAA;IACnB,kCAAmB,CAAA;IACnB,0CAA2B,CAAA;AAC7B,CAAC,EAPW,WAAW,2BAAX,WAAW,QAOtB;AAED,IAAY,aAQX;AARD,WAAY,aAAa;IACvB,kCAAiB,CAAA;IACjB,8BAAa,CAAA;IACb,8CAA6B,CAAA;IAC7B,sCAAqB,CAAA;IACrB,kCAAiB,CAAA;IACjB,sCAAqB,CAAA;IACrB,oCAAmB,CAAA;AACrB,CAAC,EARW,aAAa,6BAAb,aAAa,QAQxB;AAED,IAAY,YAOX;AAPD,WAAY,YAAY;IACtB,+BAAe,CAAA;IACf,6BAAa,CAAA;IACb,2BAAW,CAAA;IACX,+BAAe,CAAA;IACf,iCAAiB,CAAA;IACjB,6BAAa,CAAA;AACf,CAAC,EAPW,YAAY,4BAAZ,YAAY,QAOvB;AAED,IAAY,aAOX;AAPD,WAAY,aAAa;IACvB,8CAA6B,CAAA;IAC7B,6CAA4B,CAAA;IAC5B,2CAA0B,CAAA;IAC1B,yCAAwB,CAAA;IACxB,8CAA6B,CAAA;IAC7B,yCAAwB,CAAA;AAC1B,CAAC,EAPW,aAAa,6BAAb,aAAa,QAOxB;AAGD,SAAgB,mBAAmB,CAAC,OAAmB;IACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;IACjC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAA;IACnD,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC5D,OAAO,IAAI,CAAA;IACb,CAAC;IAED,+EAA+E;IAC/E,MAAM,eAAe,GAAG,IAAA,qCAAkB,EAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC7D,OAAO,IAAA,mCAAgB,EAAC,eAAe,CAAC,CAAA;AAC1C,CAAC;AAED,SAAgB,WAAW,CACzB,UAAmC,EACnC,UAAgC;IAEhC,OAAO,mBAAmB,CAAC,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAA;AACrE,CAAC;AAED,SAAgB,mBAAmB,CAAC,KAAmB;IACrD,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,YAAY,CAAC,KAAK;YACrB,OAAO,aAAa,CAAC,KAAK,CAAA;QAC5B,KAAK,YAAY,CAAC,IAAI;YACpB,OAAO,aAAa,CAAC,IAAI,CAAA;QAC3B,KAAK,YAAY,CAAC,GAAG;YACnB,OAAO,aAAa,CAAC,GAAG,CAAA;QAC1B,KAAK,YAAY,CAAC,KAAK;YACrB,OAAO,aAAa,CAAC,KAAK,CAAA;QAC5B,KAAK,YAAY,CAAC,MAAM;YACtB,OAAO,aAAa,CAAC,MAAM,CAAA;QAC7B,KAAK,YAAY,CAAC,IAAI;YACpB,OAAO,aAAa,CAAC,IAAI,CAAA;QAC3B;YACE,MAAM,eAAe,CAAA;IACzB,CAAC;AACH,CAAC;AAED,SAAgB,eAAe,CAC7B,UAAmC,EACnC,UAAgC;IAEhC,QAAQ,UAAU,EAAE,CAAC;QACnB,KAAK,iDAAuB,CAAC,MAAM;YACjC,QAAQ,UAAU,EAAE,CAAC;gBACnB,KAAK,aAAa,CAAC,MAAM;oBACvB,OAAO,YAAY,CAAC,KAAK,CAAA;gBAC3B,KAAK,aAAa,CAAC,IAAI;oBACrB,OAAO,YAAY,CAAC,IAAI,CAAA;gBAC1B,KAAK,aAAa,CAAC,YAAY;oBAC7B,OAAO,YAAY,CAAC,GAAG,CAAA;gBACzB,KAAK,aAAa,CAAC,QAAQ,CAAC;gBAC5B,KAAK,aAAa,CAAC,MAAM;oBACvB,OAAO,YAAY,CAAC,KAAK,CAAA;gBAC3B,KAAK,aAAa,CAAC,QAAQ,CAAC;gBAC5B,KAAK,aAAa,CAAC,OAAO;oBACxB,OAAO,YAAY,CAAC,MAAM,CAAA;gBAC5B;oBACE,OAAO,YAAY,CAAC,IAAI,CAAA;YAC5B,CAAC;QACH,KAAK,iDAAuB,CAAC,KAAK;YAChC,QAAQ,UAAU,EAAE,CAAC;gBACnB,KAAK,aAAa,CAAC,MAAM,CAAC;gBAC1B,KAAK,aAAa,CAAC,IAAI;oBACrB,OAAO,YAAY,CAAC,KAAK,CAAA;gBAC3B,KAAK,aAAa,CAAC,YAAY;oBAC7B,OAAO,YAAY,CAAC,GAAG,CAAA;gBACzB,KAAK,aAAa,CAAC,QAAQ,CAAC;gBAC5B,KAAK,aAAa,CAAC,MAAM;oBACvB,OAAO,YAAY,CAAC,KAAK,CAAA;gBAC3B,KAAK,aAAa,CAAC,QAAQ,CAAC;gBAC5B,KAAK,aAAa,CAAC,OAAO;oBACxB,OAAO,YAAY,CAAC,MAAM,CAAA;gBAC5B;oBACE,OAAO,YAAY,CAAC,IAAI,CAAA;YAC5B,CAAC;QACH,KAAK,iDAAuB,CAAC,aAAa;YACxC,QAAQ,UAAU,EAAE,CAAC;gBACnB,KAAK,aAAa,CAAC,MAAM,CAAC;gBAC1B,KAAK,aAAa,CAAC,IAAI;oBACrB,OAAO,YAAY,CAAC,KAAK,CAAA;gBAC3B,KAAK,aAAa,CAAC,YAAY;oBAC7B,OAAO,YAAY,CAAC,IAAI,CAAA;gBAC1B,KAAK,aAAa,CAAC,QAAQ,CAAC;gBAC5B,KAAK,aAAa,CAAC,MAAM;oBACvB,OAAO,YAAY,CAAC,KAAK,CAAA;gBAC3B,KAAK,aAAa,CAAC,QAAQ,CAAC;gBAC5B,KAAK,aAAa,CAAC,OAAO;oBACxB,OAAO,YAAY,CAAC,MAAM,CAAA;gBAC5B;oBACE,OAAO,YAAY,CAAC,IAAI,CAAA;YAC5B,CAAC;QACH;YACE,OAAO,IAAA,iDAAuB,EAAC,UAAU,CAAC,CAAA;IAC9C,CAAC;AACH,CAAC"}
package/dist/SkiArea.d.ts CHANGED
@@ -6,6 +6,7 @@ import { RunDifficultyConvention } from './RunDifficultyConvention';
6
6
  import { SnowCoverHistory } from './SnowCoverHistory';
7
7
  import { Source } from './Source';
8
8
  import { Status } from './Status';
9
+ import { ViewportHint } from './ViewportHint';
9
10
  /**
10
11
  * A ski area feature is derived from several sources:
11
12
  * - OpenStreetMap site=piste relations (must contain at least one lift or run of any kind)
@@ -48,6 +49,7 @@ export type SkiAreaProperties = SkiAreaSummaryProperties & {
48
49
  websites: string[];
49
50
  wikidataID: string | null;
50
51
  places: Place[];
52
+ viewportHint: ViewportHint;
51
53
  };
52
54
  export type SkiAreaSummaryProperties = {
53
55
  type: FeatureType.SkiArea;
@@ -1 +1 @@
1
- {"version":3,"file":"SkiArea.js","sourceRoot":"","sources":["../src/SkiArea.ts"],"names":[],"mappings":";;;AAsEA,IAAY,eAGX;AAHD,WAAY,eAAe;IACzB,wCAAqB,CAAA;IACrB,oCAAiB,CAAA;AACnB,CAAC,EAHW,eAAe,+BAAf,eAAe,QAG1B"}
1
+ {"version":3,"file":"SkiArea.js","sourceRoot":"","sources":["../src/SkiArea.ts"],"names":[],"mappings":";;;AAwEA,IAAY,eAGX;AAHD,WAAY,eAAe;IACzB,wCAAqB,CAAA;IACrB,oCAAiB,CAAA;AACnB,CAAC,EAHW,eAAe,+BAAf,eAAe,QAG1B"}
package/dist/Spot.d.ts CHANGED
@@ -3,6 +3,7 @@ import { FeatureType } from './FeatureType';
3
3
  import { Place } from './Place';
4
4
  import { SkiAreaSummaryFeature } from './SkiArea';
5
5
  import { Source } from './Source';
6
+ import { ViewportHint } from './ViewportHint';
6
7
  /**
7
8
  * A GeoJSON feature representing a spot (point of interest) in or around a ski area.
8
9
  *
@@ -30,6 +31,7 @@ export type SpotBaseProperties = {
30
31
  skiAreas: SkiAreaSummaryFeature[];
31
32
  sources: Source[];
32
33
  places: Place[];
34
+ viewportHint: ViewportHint;
33
35
  };
34
36
  /**
35
37
  * Types of spots that can be found in or around ski areas.
package/dist/Spot.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Spot.js","sourceRoot":"","sources":["../src/Spot.ts"],"names":[],"mappings":";;;AA+CA;;GAEG;AACH,IAAY,QAMX;AAND,WAAY,QAAQ;IAClB,iCAAqB,CAAA;IACrB,wCAA4B,CAAA;IAC5B,2EAA+D,CAAA;IAC/D,+EAAmE,CAAA;IACnE,iCAAqB,CAAA;AACvB,CAAC,EANW,QAAQ,wBAAR,QAAQ,QAMnB;AAaD;;GAEG;AACH,IAAY,mBAIX;AAJD,WAAY,mBAAmB;IAC7B,kCAAW,CAAA;IACX,gCAAS,CAAA;IACT,8CAAuB,CAAA;AACzB,CAAC,EAJW,mBAAmB,mCAAnB,mBAAmB,QAI9B;AAuBD;;GAEG;AACH,IAAY,mBAIX;AAJD,WAAY,mBAAmB;IAC7B,kCAAW,CAAA;IACX,kCAAW,CAAA;IACX,wCAAiB,CAAA;AACnB,CAAC,EAJW,mBAAmB,mCAAnB,mBAAmB,QAI9B"}
1
+ {"version":3,"file":"Spot.js","sourceRoot":"","sources":["../src/Spot.ts"],"names":[],"mappings":";;;AAiDA;;GAEG;AACH,IAAY,QAMX;AAND,WAAY,QAAQ;IAClB,iCAAqB,CAAA;IACrB,wCAA4B,CAAA;IAC5B,2EAA+D,CAAA;IAC/D,+EAAmE,CAAA;IACnE,iCAAqB,CAAA;AACvB,CAAC,EANW,QAAQ,wBAAR,QAAQ,QAMnB;AAaD;;GAEG;AACH,IAAY,mBAIX;AAJD,WAAY,mBAAmB;IAC7B,kCAAW,CAAA;IACX,gCAAS,CAAA;IACT,8CAAuB,CAAA;AACzB,CAAC,EAJW,mBAAmB,mCAAnB,mBAAmB,QAI9B;AAuBD;;GAEG;AACH,IAAY,mBAIX;AAJD,WAAY,mBAAmB;IAC7B,kCAAW,CAAA;IACX,kCAAW,CAAA;IACX,wCAAiB,CAAA;AACnB,CAAC,EAJW,mBAAmB,mCAAnB,mBAAmB,QAI9B"}
@@ -0,0 +1,33 @@
1
+ import * as GeoJSON from 'geojson';
2
+ /**
3
+ * Pre-computed viewport positioning data stored on every feature's properties.
4
+ * Camera space: Y axis points uphill (along bearing), X axis points screen-right.
5
+ * Metric values are in that rotated frame with elevation-adjusted Y positions
6
+ * (at 45° pitch, Δe metres above mean elevation shifts Y by Δe metres).
7
+ */
8
+ export type ViewportHint = {
9
+ /** Geographic centre of the rotated bounding box as [lng, lat]. */
10
+ center: [number, number];
11
+ /**
12
+ * Uphill bearing in degrees clockwise from north. null when elevation variation is
13
+ * insufficient to determine orientation — client should use top-down view (pitch 0, bearing 0).
14
+ */
15
+ bearing: number | null;
16
+ /** Bounding box width along the camera X axis, in metres. */
17
+ rotatedWidthMeters: number;
18
+ /** Bounding box height along the camera Y axis, in metres (elevation-adjusted). */
19
+ rotatedHeightMeters: number;
20
+ /**
21
+ * Most-downhill edge of the elevation-adjusted bounding box in camera-Y metres, relative
22
+ * to mean feature elevation. Used to compute the perspective centre shift at 45° pitch:
23
+ * look-at point = minCameraY + nearFraction * rotatedHeightMeters, where
24
+ * nearFraction = nearGround / (nearGround + farGround) from camera pitch and vFOV.
25
+ */
26
+ minCameraY: number;
27
+ };
28
+ /**
29
+ * Computes a ViewportHint from one or more GeoJSON geometries.
30
+ * All geometry types are accepted; elevation is read from the Z coordinate (defaults to 0).
31
+ * Throws if no coordinates are found.
32
+ */
33
+ export declare function computeViewportHint(geometries: GeoJSON.Geometry[]): ViewportHint;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeViewportHint = computeViewportHint;
4
+ const tslib_1 = require("tslib");
5
+ const distance_1 = tslib_1.__importDefault(require("@turf/distance"));
6
+ /**
7
+ * Computes a ViewportHint from one or more GeoJSON geometries.
8
+ * All geometry types are accepted; elevation is read from the Z coordinate (defaults to 0).
9
+ * Throws if no coordinates are found.
10
+ */
11
+ function computeViewportHint(geometries) {
12
+ // Collect all coordinates (for bbox + elevation stats) and consecutive segment pairs
13
+ // (for the downhill bearing calculation).
14
+ const allCoords = [];
15
+ const bearingSegments = [];
16
+ for (const geom of geometries) {
17
+ collectGeometry(geom, allCoords, bearingSegments);
18
+ }
19
+ if (allCoords.length === 0)
20
+ throw new Error('computeViewportHint: no coordinates found in provided geometries');
21
+ let minLng = Infinity, maxLng = -Infinity;
22
+ let minLat = Infinity, maxLat = -Infinity;
23
+ for (const [lng, lat] of allCoords) {
24
+ if (lng < minLng)
25
+ minLng = lng;
26
+ if (lng > maxLng)
27
+ maxLng = lng;
28
+ if (lat < minLat)
29
+ minLat = lat;
30
+ if (lat > maxLat)
31
+ maxLat = lat;
32
+ }
33
+ const centerLng = (minLng + maxLng) / 2;
34
+ const centerLat = (minLat + maxLat) / 2;
35
+ // Weight each segment's downhill direction by length to find dominant bearing.
36
+ let sumSin = 0, sumCos = 0;
37
+ for (const [[lng0, lat0, elev0], [lng1, lat1, elev1]] of bearingSegments) {
38
+ if (elev0 === elev1)
39
+ continue;
40
+ const [fromLng, fromLat, toLng, toLat] = elev0 > elev1
41
+ ? [lng0, lat0, lng1, lat1]
42
+ : [lng1, lat1, lng0, lat0];
43
+ const bear = greatCircleBearing(fromLat, fromLng, toLat, toLng);
44
+ const len = (0, distance_1.default)([lng0, lat0], [lng1, lat1], { units: 'meters' });
45
+ const bearingRad = (bear * Math.PI) / 180;
46
+ sumSin += Math.sin(bearingRad) * len;
47
+ sumCos += Math.cos(bearingRad) * len;
48
+ }
49
+ // Uphill bearing = downhill + 180°; null if no elevation variation found.
50
+ let mapBearing = null;
51
+ if (sumSin !== 0 || sumCos !== 0) {
52
+ const dominantDownhill = (Math.atan2(sumSin, sumCos) * 180) / Math.PI;
53
+ const normalizedDownhill = ((dominantDownhill % 360) + 360) % 360;
54
+ mapBearing = (normalizedDownhill + 180) % 360;
55
+ }
56
+ const metersPerDeg = 111320;
57
+ const cosLat = Math.cos(centerLat * (Math.PI / 180));
58
+ const bearingRad = ((mapBearing ?? 0) * Math.PI) / 180;
59
+ let elevationSum = 0;
60
+ for (const [, , elev] of allCoords)
61
+ elevationSum += elev;
62
+ const meanElevation = elevationSum / allCoords.length;
63
+ let minCameraX = Infinity, maxCameraX = -Infinity;
64
+ let minCameraY = Infinity, maxCameraY = -Infinity;
65
+ for (const [lng, lat, elev] of allCoords) {
66
+ const dx = (lng - centerLng) * cosLat * metersPerDeg;
67
+ const dy = (lat - centerLat) * metersPerDeg;
68
+ const cx = dx * Math.cos(bearingRad) - dy * Math.sin(bearingRad);
69
+ const cy = dx * Math.sin(bearingRad) + dy * Math.cos(bearingRad);
70
+ // At 45° pitch, elevation above mean shifts apparent Y by the same amount (tan 45° = 1).
71
+ const cyEffective = cy + (elev - meanElevation);
72
+ if (cx < minCameraX)
73
+ minCameraX = cx;
74
+ if (cx > maxCameraX)
75
+ maxCameraX = cx;
76
+ if (cyEffective < minCameraY)
77
+ minCameraY = cyEffective;
78
+ if (cyEffective > maxCameraY)
79
+ maxCameraY = cyEffective;
80
+ }
81
+ return {
82
+ center: [centerLng, centerLat],
83
+ bearing: mapBearing,
84
+ rotatedWidthMeters: maxCameraX - minCameraX,
85
+ rotatedHeightMeters: maxCameraY - minCameraY,
86
+ minCameraY,
87
+ };
88
+ }
89
+ /** Recursively extracts coordinates and segment pairs from a geometry. */
90
+ function collectGeometry(geom, allCoords, bearingSegments) {
91
+ switch (geom.type) {
92
+ case 'Point': {
93
+ const [lng, lat, elev = 0] = geom.coordinates;
94
+ allCoords.push([lng, lat, elev]);
95
+ break;
96
+ }
97
+ case 'MultiPoint': {
98
+ for (const pos of geom.coordinates) {
99
+ const [lng, lat, elev = 0] = pos;
100
+ allCoords.push([lng, lat, elev]);
101
+ }
102
+ break;
103
+ }
104
+ case 'LineString':
105
+ collectPositions(geom.coordinates, allCoords, bearingSegments);
106
+ break;
107
+ case 'MultiLineString':
108
+ for (const line of geom.coordinates) {
109
+ collectPositions(line, allCoords, bearingSegments);
110
+ }
111
+ break;
112
+ case 'Polygon':
113
+ for (const ring of geom.coordinates) {
114
+ collectPositions(ring, allCoords, bearingSegments);
115
+ }
116
+ break;
117
+ case 'MultiPolygon':
118
+ for (const polygon of geom.coordinates) {
119
+ for (const ring of polygon) {
120
+ collectPositions(ring, allCoords, bearingSegments);
121
+ }
122
+ }
123
+ break;
124
+ case 'GeometryCollection':
125
+ for (const g of geom.geometries) {
126
+ collectGeometry(g, allCoords, bearingSegments);
127
+ }
128
+ break;
129
+ }
130
+ }
131
+ /** Adds positions to allCoords and consecutive pairs to bearingSegments. Z defaults to 0. */
132
+ function collectPositions(positions, allCoords, bearingSegments) {
133
+ const elevated = positions.map(([lng, lat, elev = 0]) => [lng, lat, elev]);
134
+ for (const coord of elevated)
135
+ allCoords.push(coord);
136
+ for (let i = 0; i < elevated.length - 1; i++) {
137
+ bearingSegments.push([elevated[i], elevated[i + 1]]);
138
+ }
139
+ }
140
+ /** Great-circle bearing from (lat1, lng1) to (lat2, lng2), in degrees 0–360. */
141
+ function greatCircleBearing(lat1, lng1, lat2, lng2) {
142
+ const toRad = (d) => (d * Math.PI) / 180;
143
+ const φ1 = toRad(lat1);
144
+ const φ2 = toRad(lat2);
145
+ const Δλ = toRad(lng2 - lng1);
146
+ const y = Math.sin(Δλ) * Math.cos(φ2);
147
+ const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
148
+ return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
149
+ }
150
+ //# sourceMappingURL=ViewportHint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ViewportHint.js","sourceRoot":"","sources":["../src/ViewportHint.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -11,3 +11,4 @@ export * from './SnowCoverHistory';
11
11
  export * from './Source';
12
12
  export * from './Spot';
13
13
  export * from './Status';
14
+ export * from './ViewportHint';
package/dist/index.js CHANGED
@@ -14,4 +14,5 @@ tslib_1.__exportStar(require("./SnowCoverHistory"), exports);
14
14
  tslib_1.__exportStar(require("./Source"), exports);
15
15
  tslib_1.__exportStar(require("./Spot"), exports);
16
16
  tslib_1.__exportStar(require("./Status"), exports);
17
+ tslib_1.__exportStar(require("./ViewportHint"), exports);
17
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6DAAkC;AAClC,wDAA6B;AAC7B,iDAAsB;AACtB,8DAAmC;AACnC,kDAAuB;AACvB,gDAAqB;AACrB,oEAAyC;AACzC,8DAAmC;AACnC,oDAAyB;AACzB,6DAAkC;AAClC,mDAAwB;AACxB,iDAAsB;AACtB,mDAAwB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6DAAkC;AAClC,wDAA6B;AAC7B,iDAAsB;AACtB,8DAAmC;AACnC,kDAAuB;AACvB,gDAAqB;AACrB,oEAAyC;AACzC,8DAAmC;AACnC,oDAAyB;AACzB,6DAAkC;AAClC,mDAAwB;AACxB,iDAAsB;AACtB,mDAAwB;AACxB,yDAA8B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openskidata-format",
3
- "version": "11.0.0",
3
+ "version": "12.0.0",
4
4
  "description": "Data format for OpenSkiMap.org",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/Lift.test.ts CHANGED
@@ -1,7 +1,175 @@
1
- import { LiftType, getFormattedLiftType, getLiftColor } from './Lift';
1
+ import { FeatureType } from './FeatureType';
2
+ import { getLiftElevationData, LiftFeature, LiftType, getFormattedLiftType, getLiftColor } from './Lift';
3
+ import { LiftStationPosition, LiftStationSpotFeature, SpotType } from './Spot';
2
4
  import { Status } from './Status';
3
5
 
6
+ function makeLiftFeature(overrides: {
7
+ coordinates?: GeoJSON.Position[];
8
+ duration?: number | null;
9
+ stations?: LiftStationSpotFeature[];
10
+ }): LiftFeature {
11
+ return {
12
+ type: 'Feature',
13
+ geometry: {
14
+ type: 'LineString',
15
+ coordinates: overrides.coordinates ?? [
16
+ [8.0, 46.0, 1000],
17
+ [8.01, 46.05, 1500],
18
+ ],
19
+ },
20
+ properties: {
21
+ type: FeatureType.Lift,
22
+ id: 'test',
23
+ liftType: LiftType.ChairLift,
24
+ status: Status.Operating,
25
+ access: null,
26
+ name: null,
27
+ ref: null,
28
+ refFRCAIRN: null,
29
+ description: null,
30
+ oneway: null,
31
+ occupancy: null,
32
+ capacity: null,
33
+ duration: overrides.duration !== undefined ? overrides.duration : 600,
34
+ detachable: null,
35
+ bubble: null,
36
+ heating: null,
37
+ tunnel: null,
38
+ stations: overrides.stations ?? [],
39
+ skiAreas: [],
40
+ sources: [],
41
+ websites: [],
42
+ wikidataID: null,
43
+ places: [],
44
+ },
45
+ }
46
+ }
47
+
48
+ function makeStation(
49
+ position: LiftStationPosition,
50
+ coordinates: GeoJSON.Position,
51
+ ): LiftStationSpotFeature {
52
+ return {
53
+ type: 'Feature',
54
+ geometry: { type: 'Point', coordinates },
55
+ properties: {
56
+ type: FeatureType.Spot,
57
+ id: `station-${position}`,
58
+ spotType: SpotType.LiftStation,
59
+ name: null,
60
+ position,
61
+ entry: null,
62
+ exit: null,
63
+ liftId: 'test',
64
+ skiAreas: [],
65
+ sources: [],
66
+ places: [],
67
+ },
68
+ }
69
+ }
70
+
4
71
  describe('Lift', () => {
72
+ describe('getLiftElevationData', () => {
73
+ it('returns null for non-LineString geometry', () => {
74
+ const feature: LiftFeature = {
75
+ ...makeLiftFeature({}),
76
+ geometry: { type: 'MultiLineString', coordinates: [] },
77
+ }
78
+ expect(getLiftElevationData(feature)).toBeNull()
79
+ })
80
+
81
+ it('returns null when coordinates have no elevation', () => {
82
+ const feature = makeLiftFeature({
83
+ coordinates: [[8.0, 46.0], [8.01, 46.05]],
84
+ })
85
+ expect(getLiftElevationData(feature)).toBeNull()
86
+ })
87
+
88
+ it('returns null speed when duration is null', () => {
89
+ const result = getLiftElevationData(makeLiftFeature({ duration: null }))
90
+ expect(result?.speedInMetersPerSecond).toBeNull()
91
+ expect(result?.verticalSpeedInMetersPerSecond).toBeNull()
92
+ })
93
+
94
+ it('uses full geometry length when no stations', () => {
95
+ const feature = makeLiftFeature({ stations: [] })
96
+ const result = getLiftElevationData(feature)!
97
+ expect(result.speedInMetersPerSecond).toBeGreaterThan(0)
98
+ // Speed should be inclinedLength / duration from geometry
99
+ const expectedSpeed = result.inclinedLengthInMeters / 600
100
+ expect(result.speedInMetersPerSecond).toBeCloseTo(expectedSpeed)
101
+ })
102
+
103
+ it('uses full geometry length when only one terminal station is present', () => {
104
+ const noStations = makeLiftFeature({ stations: [] })
105
+ const oneStation = makeLiftFeature({
106
+ stations: [makeStation(LiftStationPosition.Bottom, [8.0, 46.0, 1000])],
107
+ })
108
+ const resultNoStations = getLiftElevationData(noStations)!
109
+ const resultOneStation = getLiftElevationData(oneStation)!
110
+ expect(resultOneStation.speedInMetersPerSecond).toBeCloseTo(
111
+ resultNoStations.speedInMetersPerSecond!,
112
+ )
113
+ })
114
+
115
+ it('uses station-based distance when top and bottom stations have elevation', () => {
116
+ // Geometry extends beyond the stations (extra approach at each end)
117
+ const feature = makeLiftFeature({
118
+ coordinates: [
119
+ [8.0, 46.0, 900], // approach before bottom station
120
+ [8.005, 46.01, 1000], // bottom station location
121
+ [8.015, 46.04, 1500], // top station location
122
+ [8.02, 46.05, 1600], // approach after top station
123
+ ],
124
+ duration: 600,
125
+ stations: [
126
+ makeStation(LiftStationPosition.Bottom, [8.005, 46.01, 1000]),
127
+ makeStation(LiftStationPosition.Top, [8.015, 46.04, 1500]),
128
+ ],
129
+ })
130
+
131
+ const resultWithStations = getLiftElevationData(feature)!
132
+
133
+ // Without stations, use full geometry (which is longer)
134
+ const featureNoStations = makeLiftFeature({
135
+ coordinates: [
136
+ [8.0, 46.0, 900],
137
+ [8.005, 46.01, 1000],
138
+ [8.015, 46.04, 1500],
139
+ [8.02, 46.05, 1600],
140
+ ],
141
+ duration: 600,
142
+ stations: [],
143
+ })
144
+ const resultNoStations = getLiftElevationData(featureNoStations)!
145
+
146
+ // Station-based distance is shorter → lower speed
147
+ expect(resultWithStations.speedInMetersPerSecond).toBeLessThan(
148
+ resultNoStations.speedInMetersPerSecond!,
149
+ )
150
+ expect(resultWithStations.verticalSpeedInMetersPerSecond).toBeLessThan(
151
+ resultNoStations.verticalSpeedInMetersPerSecond!,
152
+ )
153
+ })
154
+
155
+ it('falls back to geometry when stations have no elevation', () => {
156
+ const featureWithStations = makeLiftFeature({
157
+ stations: [
158
+ makeStation(LiftStationPosition.Bottom, [8.0, 46.0]), // no Z
159
+ makeStation(LiftStationPosition.Top, [8.01, 46.05]), // no Z
160
+ ],
161
+ })
162
+ const featureNoStations = makeLiftFeature({ stations: [] })
163
+
164
+ const resultWithStations = getLiftElevationData(featureWithStations)!
165
+ const resultNoStations = getLiftElevationData(featureNoStations)!
166
+
167
+ expect(resultWithStations.speedInMetersPerSecond).toBeCloseTo(
168
+ resultNoStations.speedInMetersPerSecond!,
169
+ )
170
+ })
171
+ })
172
+
5
173
  describe('getFormattedLiftType', () => {
6
174
  it('should format lift types correctly', () => {
7
175
  expect(getFormattedLiftType(LiftType.ChairLift)).toBe('Chairlift');
@@ -21,4 +189,4 @@ describe('Lift', () => {
21
189
  expect(getLiftColor(Status.Construction)).toBe('hsl(0, 82%, 42%)');
22
190
  });
23
191
  });
24
- });
192
+ });
package/src/Lift.ts CHANGED
@@ -1,11 +1,13 @@
1
+ import distance from '@turf/distance'
1
2
  import { ElevationData, getElevationData } from './ElevationProfile'
2
3
  import { FeatureType } from './FeatureType'
3
4
  import { Place } from './Place'
4
5
  import { SkiAreaSummaryFeature } from './SkiArea'
5
6
  import { Source } from './Source'
6
- import { LiftStationSpotFeature } from './Spot'
7
+ import { LiftStationPosition, LiftStationSpotFeature } from './Spot'
7
8
  import { Status } from './Status'
8
9
  import { exhaustiveMatchingGuard } from './util/exhaustiveMatchingGuard'
10
+ import { ViewportHint } from './ViewportHint'
9
11
 
10
12
  export type LiftFeature = GeoJSON.Feature<LiftGeometry, LiftProperties>
11
13
 
@@ -77,6 +79,7 @@ export type LiftProperties = {
77
79
  websites: string[]
78
80
  wikidataID: string | null
79
81
  places: Place[]
82
+ viewportHint: ViewportHint
80
83
  }
81
84
 
82
85
  export enum LiftType {
@@ -113,18 +116,59 @@ export function getLiftElevationData(
113
116
 
114
117
  const elevationData = getElevationData(geometry)
115
118
  const durationInSeconds = feature.properties.duration
119
+ const terminalDistances = getTerminalStationDistances(feature)
120
+ const inclinedLength =
121
+ terminalDistances?.inclinedLengthInMeters ??
122
+ elevationData.inclinedLengthInMeters
123
+ const verticalLength =
124
+ terminalDistances?.verticalInMeters ?? elevationData.verticalInMeters
116
125
 
117
126
  return {
118
127
  ...elevationData,
119
128
  speedInMetersPerSecond: durationInSeconds
120
- ? elevationData.inclinedLengthInMeters / durationInSeconds
129
+ ? inclinedLength / durationInSeconds
121
130
  : null,
122
131
  verticalSpeedInMetersPerSecond: durationInSeconds
123
- ? elevationData.verticalInMeters / durationInSeconds
132
+ ? verticalLength / durationInSeconds
124
133
  : null,
125
134
  }
126
135
  }
127
136
 
137
+ function getTerminalStationDistances(
138
+ feature: LiftFeature,
139
+ ): { inclinedLengthInMeters: number; verticalInMeters: number } | null {
140
+ const stations = feature.properties.stations
141
+ const topStation = stations.find(
142
+ (s) => s.properties.position === LiftStationPosition.Top,
143
+ )
144
+ const bottomStation = stations.find(
145
+ (s) => s.properties.position === LiftStationPosition.Bottom,
146
+ )
147
+
148
+ if (!topStation || !bottomStation) {
149
+ return null
150
+ }
151
+
152
+ const topCoords = topStation.geometry.coordinates
153
+ const bottomCoords = bottomStation.geometry.coordinates
154
+
155
+ if (topCoords.length < 3 || bottomCoords.length < 3) {
156
+ return null
157
+ }
158
+
159
+ const horizontalDistanceInMeters = distance(
160
+ topStation.geometry,
161
+ bottomStation.geometry,
162
+ { units: 'meters' },
163
+ )
164
+ const verticalInMeters = Math.abs(topCoords[2] - bottomCoords[2])
165
+ const inclinedLengthInMeters = Math.sqrt(
166
+ Math.pow(horizontalDistanceInMeters, 2) + Math.pow(verticalInMeters, 2),
167
+ )
168
+
169
+ return { inclinedLengthInMeters, verticalInMeters }
170
+ }
171
+
128
172
  export function getFormattedLiftType(liftType: LiftType): string {
129
173
  switch (liftType) {
130
174
  case LiftType.CableCar:
package/src/Run.ts CHANGED
@@ -13,6 +13,7 @@ import { SnowCoverHistory } from './SnowCoverHistory'
13
13
  import { Source } from './Source'
14
14
  import { Status } from './Status'
15
15
  import { exhaustiveMatchingGuard } from './util/exhaustiveMatchingGuard'
16
+ import { ViewportHint } from './ViewportHint'
16
17
 
17
18
  export type RunGeometry = GeoJSON.LineString | GeoJSON.Polygon
18
19
 
@@ -88,6 +89,7 @@ export type RunProperties = {
88
89
  wikidataID: string | null
89
90
  places: Place[]
90
91
  snowCoverHistory?: SnowCoverHistory
92
+ viewportHint: ViewportHint
91
93
  }
92
94
 
93
95
  export enum RunUse {
package/src/SkiArea.ts CHANGED
@@ -6,6 +6,7 @@ import { RunDifficultyConvention } from './RunDifficultyConvention'
6
6
  import { SnowCoverHistory } from './SnowCoverHistory'
7
7
  import { Source } from './Source'
8
8
  import { Status } from './Status'
9
+ import { ViewportHint } from './ViewportHint'
9
10
 
10
11
  /**
11
12
  * A ski area feature is derived from several sources:
@@ -58,6 +59,7 @@ export type SkiAreaProperties = SkiAreaSummaryProperties & {
58
59
  websites: string[]
59
60
  wikidataID: string | null
60
61
  places: Place[]
62
+ viewportHint: ViewportHint
61
63
  }
62
64
 
63
65
  export type SkiAreaSummaryProperties = {
package/src/Spot.ts CHANGED
@@ -3,6 +3,7 @@ import { FeatureType } from './FeatureType'
3
3
  import { Place } from './Place'
4
4
  import { SkiAreaSummaryFeature } from './SkiArea'
5
5
  import { Source } from './Source'
6
+ import { ViewportHint } from './ViewportHint'
6
7
 
7
8
  /**
8
9
  * A GeoJSON feature representing a spot (point of interest) in or around a ski area.
@@ -43,6 +44,7 @@ export type SpotBaseProperties = {
43
44
  skiAreas: SkiAreaSummaryFeature[]
44
45
  sources: Source[]
45
46
  places: Place[]
47
+ viewportHint: ViewportHint
46
48
  }
47
49
 
48
50
  /**
@@ -0,0 +1,205 @@
1
+ import * as GeoJSON from 'geojson'
2
+ import distance from '@turf/distance'
3
+
4
+ /**
5
+ * Pre-computed viewport positioning data stored on every feature's properties.
6
+ * Camera space: Y axis points uphill (along bearing), X axis points screen-right.
7
+ * Metric values are in that rotated frame with elevation-adjusted Y positions
8
+ * (at 45° pitch, Δe metres above mean elevation shifts Y by Δe metres).
9
+ */
10
+ export type ViewportHint = {
11
+ /** Geographic centre of the rotated bounding box as [lng, lat]. */
12
+ center: [number, number]
13
+
14
+ /**
15
+ * Uphill bearing in degrees clockwise from north. null when elevation variation is
16
+ * insufficient to determine orientation — client should use top-down view (pitch 0, bearing 0).
17
+ */
18
+ bearing: number | null
19
+
20
+ /** Bounding box width along the camera X axis, in metres. */
21
+ rotatedWidthMeters: number
22
+
23
+ /** Bounding box height along the camera Y axis, in metres (elevation-adjusted). */
24
+ rotatedHeightMeters: number
25
+
26
+ /**
27
+ * Most-downhill edge of the elevation-adjusted bounding box in camera-Y metres, relative
28
+ * to mean feature elevation. Used to compute the perspective centre shift at 45° pitch:
29
+ * look-at point = minCameraY + nearFraction * rotatedHeightMeters, where
30
+ * nearFraction = nearGround / (nearGround + farGround) from camera pitch and vFOV.
31
+ */
32
+ minCameraY: number
33
+ }
34
+
35
+ /**
36
+ * Computes a ViewportHint from one or more GeoJSON geometries.
37
+ * All geometry types are accepted; elevation is read from the Z coordinate (defaults to 0).
38
+ * Throws if no coordinates are found.
39
+ */
40
+ export function computeViewportHint(
41
+ geometries: GeoJSON.Geometry[],
42
+ ): ViewportHint {
43
+ // Collect all coordinates (for bbox + elevation stats) and consecutive segment pairs
44
+ // (for the downhill bearing calculation).
45
+ const allCoords: [number, number, number][] = []
46
+ const bearingSegments: [[number, number, number], [number, number, number]][] =
47
+ []
48
+
49
+ for (const geom of geometries) {
50
+ collectGeometry(geom, allCoords, bearingSegments)
51
+ }
52
+
53
+ if (allCoords.length === 0) throw new Error('computeViewportHint: no coordinates found in provided geometries')
54
+
55
+ let minLng = Infinity,
56
+ maxLng = -Infinity
57
+ let minLat = Infinity,
58
+ maxLat = -Infinity
59
+ for (const [lng, lat] of allCoords) {
60
+ if (lng < minLng) minLng = lng
61
+ if (lng > maxLng) maxLng = lng
62
+ if (lat < minLat) minLat = lat
63
+ if (lat > maxLat) maxLat = lat
64
+ }
65
+
66
+ const centerLng = (minLng + maxLng) / 2
67
+ const centerLat = (minLat + maxLat) / 2
68
+
69
+ // Weight each segment's downhill direction by length to find dominant bearing.
70
+ let sumSin = 0,
71
+ sumCos = 0
72
+ for (const [[lng0, lat0, elev0], [lng1, lat1, elev1]] of bearingSegments) {
73
+ if (elev0 === elev1) continue
74
+ const [fromLng, fromLat, toLng, toLat] =
75
+ elev0 > elev1
76
+ ? [lng0, lat0, lng1, lat1]
77
+ : [lng1, lat1, lng0, lat0]
78
+ const bear = greatCircleBearing(fromLat, fromLng, toLat, toLng)
79
+ const len = distance([lng0, lat0], [lng1, lat1], { units: 'meters' })
80
+ const bearingRad = (bear * Math.PI) / 180
81
+ sumSin += Math.sin(bearingRad) * len
82
+ sumCos += Math.cos(bearingRad) * len
83
+ }
84
+
85
+ // Uphill bearing = downhill + 180°; null if no elevation variation found.
86
+ let mapBearing: number | null = null
87
+ if (sumSin !== 0 || sumCos !== 0) {
88
+ const dominantDownhill = (Math.atan2(sumSin, sumCos) * 180) / Math.PI
89
+ const normalizedDownhill = ((dominantDownhill % 360) + 360) % 360
90
+ mapBearing = (normalizedDownhill + 180) % 360
91
+ }
92
+
93
+ const metersPerDeg = 111320
94
+ const cosLat = Math.cos(centerLat * (Math.PI / 180))
95
+ const bearingRad = ((mapBearing ?? 0) * Math.PI) / 180
96
+
97
+ let elevationSum = 0
98
+ for (const [, , elev] of allCoords) elevationSum += elev
99
+ const meanElevation = elevationSum / allCoords.length
100
+
101
+ let minCameraX = Infinity,
102
+ maxCameraX = -Infinity
103
+ let minCameraY = Infinity,
104
+ maxCameraY = -Infinity
105
+
106
+ for (const [lng, lat, elev] of allCoords) {
107
+ const dx = (lng - centerLng) * cosLat * metersPerDeg
108
+ const dy = (lat - centerLat) * metersPerDeg
109
+ const cx = dx * Math.cos(bearingRad) - dy * Math.sin(bearingRad)
110
+ const cy = dx * Math.sin(bearingRad) + dy * Math.cos(bearingRad)
111
+ // At 45° pitch, elevation above mean shifts apparent Y by the same amount (tan 45° = 1).
112
+ const cyEffective = cy + (elev - meanElevation)
113
+ if (cx < minCameraX) minCameraX = cx
114
+ if (cx > maxCameraX) maxCameraX = cx
115
+ if (cyEffective < minCameraY) minCameraY = cyEffective
116
+ if (cyEffective > maxCameraY) maxCameraY = cyEffective
117
+ }
118
+
119
+ return {
120
+ center: [centerLng, centerLat],
121
+ bearing: mapBearing,
122
+ rotatedWidthMeters: maxCameraX - minCameraX,
123
+ rotatedHeightMeters: maxCameraY - minCameraY,
124
+ minCameraY,
125
+ }
126
+ }
127
+
128
+ /** Recursively extracts coordinates and segment pairs from a geometry. */
129
+ function collectGeometry(
130
+ geom: GeoJSON.Geometry,
131
+ allCoords: [number, number, number][],
132
+ bearingSegments: [[number, number, number], [number, number, number]][],
133
+ ): void {
134
+ switch (geom.type) {
135
+ case 'Point': {
136
+ const [lng, lat, elev = 0] = geom.coordinates
137
+ allCoords.push([lng, lat, elev])
138
+ break
139
+ }
140
+ case 'MultiPoint': {
141
+ for (const pos of geom.coordinates) {
142
+ const [lng, lat, elev = 0] = pos
143
+ allCoords.push([lng, lat, elev])
144
+ }
145
+ break
146
+ }
147
+ case 'LineString':
148
+ collectPositions(geom.coordinates, allCoords, bearingSegments)
149
+ break
150
+ case 'MultiLineString':
151
+ for (const line of geom.coordinates) {
152
+ collectPositions(line, allCoords, bearingSegments)
153
+ }
154
+ break
155
+ case 'Polygon':
156
+ for (const ring of geom.coordinates) {
157
+ collectPositions(ring, allCoords, bearingSegments)
158
+ }
159
+ break
160
+ case 'MultiPolygon':
161
+ for (const polygon of geom.coordinates) {
162
+ for (const ring of polygon) {
163
+ collectPositions(ring, allCoords, bearingSegments)
164
+ }
165
+ }
166
+ break
167
+ case 'GeometryCollection':
168
+ for (const g of geom.geometries) {
169
+ collectGeometry(g, allCoords, bearingSegments)
170
+ }
171
+ break
172
+ }
173
+ }
174
+
175
+ /** Adds positions to allCoords and consecutive pairs to bearingSegments. Z defaults to 0. */
176
+ function collectPositions(
177
+ positions: GeoJSON.Position[],
178
+ allCoords: [number, number, number][],
179
+ bearingSegments: [[number, number, number], [number, number, number]][],
180
+ ): void {
181
+ const elevated: [number, number, number][] = positions.map(
182
+ ([lng, lat, elev = 0]) => [lng, lat, elev],
183
+ )
184
+ for (const coord of elevated) allCoords.push(coord)
185
+ for (let i = 0; i < elevated.length - 1; i++) {
186
+ bearingSegments.push([elevated[i], elevated[i + 1]])
187
+ }
188
+ }
189
+
190
+ /** Great-circle bearing from (lat1, lng1) to (lat2, lng2), in degrees 0–360. */
191
+ function greatCircleBearing(
192
+ lat1: number,
193
+ lng1: number,
194
+ lat2: number,
195
+ lng2: number,
196
+ ): number {
197
+ const toRad = (d: number) => (d * Math.PI) / 180
198
+ const φ1 = toRad(lat1)
199
+ const φ2 = toRad(lat2)
200
+ const Δλ = toRad(lng2 - lng1)
201
+ const y = Math.sin(Δλ) * Math.cos(φ2)
202
+ const x =
203
+ Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ)
204
+ return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360
205
+ }
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from './SnowCoverHistory'
11
11
  export * from './Source'
12
12
  export * from './Spot'
13
13
  export * from './Status'
14
+ export * from './ViewportHint'