openskidata-format 5.0.0 → 5.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/Run.d.ts CHANGED
@@ -2,6 +2,7 @@ import * as GeoJSON from 'geojson';
2
2
  import { ElevationData, ElevationProfile } from './ElevationProfile';
3
3
  import { FeatureType } from './FeatureType';
4
4
  import { SkiAreaSummaryFeature } from './SkiArea';
5
+ import { SnowCoverHistory } from './SnowCoverHistory';
5
6
  import { Source } from './Source';
6
7
  import { Status } from './Status';
7
8
  export type RunGeometry = GeoJSON.LineString | GeoJSON.Polygon;
@@ -45,6 +46,7 @@ export type RunFeature = GeoJSON.Feature<RunGeometry, RunProperties>;
45
46
  * @property {Source[]} sources - Data sources for this run's information
46
47
  * @property {string[]} websites - Websites associated with this run, derived from the OpenStreetMap website tag
47
48
  * @property {string | null} wikidata_id - Wikidata identifier, if available, derived from the OpenStreetMap wikidata tag
49
+ * @property {SnowCoverHistory} [snowCoverHistory] - Historical snow cover data for this run, if available
48
50
  */
49
51
  export type RunProperties = {
50
52
  type: FeatureType.Run;
@@ -66,6 +68,7 @@ export type RunProperties = {
66
68
  sources: Source[];
67
69
  websites: string[];
68
70
  wikidata_id: string | null;
71
+ snowCoverHistory?: SnowCoverHistory;
69
72
  };
70
73
  export declare enum RunUse {
71
74
  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":"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"}
package/dist/SkiArea.d.ts CHANGED
@@ -2,6 +2,7 @@ import { FeatureType } from './FeatureType';
2
2
  import { LiftType } from './Lift';
3
3
  import { Location } from './Location';
4
4
  import { RunDifficulty, RunDifficultyConvention } from './Run';
5
+ import { SnowCoverHistory } from './SnowCoverHistory';
5
6
  import { Source } from './Source';
6
7
  import { Status } from './Status';
7
8
  /**
@@ -63,6 +64,16 @@ export type SkiAreaStatistics = {
63
64
  lifts: LiftStatistics;
64
65
  minElevation?: number;
65
66
  maxElevation?: number;
67
+ snowCover?: SkiAreaSnowCoverStatistics;
68
+ };
69
+ /**
70
+ * Aggregated snow cover data for a ski area's runs.
71
+ */
72
+ export type SkiAreaSnowCoverStatistics = {
73
+ overall: SnowCoverHistory;
74
+ byActivity: {
75
+ [key in SkiAreaActivity | 'other']?: SnowCoverHistory;
76
+ };
66
77
  };
67
78
  export type RunStatistics = {
68
79
  minElevation?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"SkiArea.js","sourceRoot":"","sources":["../src/SkiArea.ts"],"names":[],"mappings":";;;AAoEA,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":";;;AAqEA,IAAY,eAGX;AAHD,WAAY,eAAe;IACzB,wCAAqB,CAAA;IACrB,oCAAiB,CAAA;AACnB,CAAC,EAHW,eAAe,+BAAf,eAAe,QAG1B"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Historical snow cover information for a geometry.
3
+ * Data is organized chronologically, with the oldest year first.
4
+ *
5
+ * @property {number} year - The year for this data
6
+ * @property {Array<[number, number, number]>} days - Daily measurements as tuples of [day of year, NDSI snow cover value, % of valid pixels]
7
+ */
8
+ export type SnowCoverHistory = {
9
+ year: number;
10
+ /**
11
+ * Array of daily snow cover measurements ordered chronologically by day of year.
12
+ * Each tuple contains:
13
+ * - Day of year (1-365/366)
14
+ * - NDSI snow cover value (0-100) - measures snow presence, not depth
15
+ * - Percentage of valid pixels (0-100) - pixels with valid data for that day
16
+ *
17
+ * Data source: VIIRS/NPP CGF Snow Cover Daily L3 Global 375m product (VNP10A1F)
18
+ * Resolution: 375m (affects measurement accuracy for small geometries)
19
+ * Frequency: Weekly under cloud-free conditions, varies in practice
20
+ *
21
+ * Valid pixel percentage may be less than 100% due to:
22
+ * - Cloud cover
23
+ * - Different cloud gap filling dates for pixels
24
+ * - Incomplete pipeline data
25
+ */
26
+ days: [number, number, number][];
27
+ }[];
28
+ /**
29
+ * Monthly aggregated snow cover history.
30
+ * Data is organized chronologically, with the oldest year first.
31
+ *
32
+ * @property {number} year - The year for this data
33
+ * @property {Array<[number, number, number]>} months - Monthly aggregations as tuples of [month, average snow cover, average valid pixels]
34
+ */
35
+ export type MonthlySnowCoverHistory = {
36
+ year: number;
37
+ /**
38
+ * Array of monthly snow cover aggregations ordered chronologically by month.
39
+ * Each tuple contains:
40
+ * - Month (1-12)
41
+ * - Average NDSI snow cover value (0-100) - weighted by valid pixels
42
+ * - Average percentage of valid pixels (0-100) - across all days in month
43
+ */
44
+ months: [number, number, number][];
45
+ }[];
46
+ /**
47
+ * Converts daily snow cover measurements to monthly aggregations.
48
+ *
49
+ * Snow cover values are weighted by valid pixel percentages when calculating monthly averages.
50
+ * Input data is assumed to be sorted chronologically.
51
+ *
52
+ * @param snowCoverHistory Daily snow cover history to aggregate
53
+ * @returns Monthly aggregated snow cover history
54
+ */
55
+ export declare function getMonthlySnowCover(snowCoverHistory: SnowCoverHistory): MonthlySnowCoverHistory;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMonthlySnowCover = getMonthlySnowCover;
4
+ /**
5
+ * Converts daily snow cover measurements to monthly aggregations.
6
+ *
7
+ * Snow cover values are weighted by valid pixel percentages when calculating monthly averages.
8
+ * Input data is assumed to be sorted chronologically.
9
+ *
10
+ * @param snowCoverHistory Daily snow cover history to aggregate
11
+ * @returns Monthly aggregated snow cover history
12
+ */
13
+ function getMonthlySnowCover(snowCoverHistory) {
14
+ return snowCoverHistory.map(yearData => {
15
+ const monthlyData = new Map();
16
+ // Group daily data by month
17
+ yearData.days.forEach(([dayOfYear, snowCover, validPixelPercent]) => {
18
+ const date = dayOfYearToDate(yearData.year, dayOfYear);
19
+ const month = date.getMonth() + 1; // Convert to 1-12
20
+ if (!monthlyData.has(month)) {
21
+ monthlyData.set(month, { values: [], validPixels: [], count: 0 });
22
+ }
23
+ const monthData = monthlyData.get(month);
24
+ monthData.values.push(snowCover * validPixelPercent / 100); // Weight by valid pixels
25
+ monthData.validPixels.push(validPixelPercent);
26
+ monthData.count++;
27
+ });
28
+ // Calculate monthly averages
29
+ const months = [];
30
+ monthlyData.forEach((data, month) => {
31
+ const totalValidPixels = data.validPixels.reduce((sum, val) => sum + val, 0);
32
+ const weightedSnowCover = data.values.reduce((sum, val) => sum + val, 0);
33
+ const avgValidPixels = totalValidPixels / data.count;
34
+ const avgSnowCover = avgValidPixels > 0 ? (weightedSnowCover / totalValidPixels) * 100 : 0;
35
+ months.push([month, Math.round(avgSnowCover * 100) / 100, Math.round(avgValidPixels * 100) / 100]);
36
+ });
37
+ return {
38
+ year: yearData.year,
39
+ months
40
+ };
41
+ });
42
+ }
43
+ /**
44
+ * Converts day of year to a Date object.
45
+ *
46
+ * @param year The year
47
+ * @param dayOfYear Day of year (1-365/366)
48
+ * @returns Date object for the specified day
49
+ */
50
+ function dayOfYearToDate(year, dayOfYear) {
51
+ const date = new Date(year, 0, 1);
52
+ date.setDate(dayOfYear);
53
+ return date;
54
+ }
55
+ //# sourceMappingURL=SnowCoverHistory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SnowCoverHistory.js","sourceRoot":"","sources":["../src/SnowCoverHistory.ts"],"names":[],"mappings":";;AAwDA,kDAqCC;AA9CD;;;;;;;;GAQG;AACH,SAAgB,mBAAmB,CACjC,gBAAkC;IAElC,OAAO,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;QACrC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAsE,CAAA;QAEjG,4BAA4B;QAC5B,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,iBAAiB,CAAC,EAAE,EAAE;YAClE,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;YACtD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA,CAAC,kBAAkB;YAEpD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5B,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;YACnE,CAAC;YAED,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,KAAK,CAAE,CAAA;YACzC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,GAAG,iBAAiB,GAAG,GAAG,CAAC,CAAA,CAAC,yBAAyB;YACpF,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;YAC7C,SAAS,CAAC,KAAK,EAAE,CAAA;QACnB,CAAC,CAAC,CAAA;QAEF,6BAA6B;QAC7B,MAAM,MAAM,GAA+B,EAAE,CAAA;QAC7C,WAAW,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;YAClC,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC,CAAC,CAAA;YAC5E,MAAM,iBAAiB,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC,CAAC,CAAA;YACxE,MAAM,cAAc,GAAG,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAA;YACpD,MAAM,YAAY,GAAG,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YAE1F,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;QACpG,CAAC,CAAC,CAAA;QAEF,OAAO;YACL,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,MAAM;SACP,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,IAAY,EAAE,SAAiB;IACtD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IACjC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IACvB,OAAO,IAAI,CAAA;AACb,CAAC"}
package/dist/index.d.ts CHANGED
@@ -5,5 +5,6 @@ export * from './LiftNameFormatter';
5
5
  export * from './Location';
6
6
  export * from './Run';
7
7
  export * from './SkiArea';
8
+ export * from './SnowCoverHistory';
8
9
  export * from './Source';
9
10
  export * from './Status';
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ tslib_1.__exportStar(require("./LiftNameFormatter"), exports);
8
8
  tslib_1.__exportStar(require("./Location"), exports);
9
9
  tslib_1.__exportStar(require("./Run"), exports);
10
10
  tslib_1.__exportStar(require("./SkiArea"), exports);
11
+ tslib_1.__exportStar(require("./SnowCoverHistory"), exports);
11
12
  tslib_1.__exportStar(require("./Source"), exports);
12
13
  tslib_1.__exportStar(require("./Status"), exports);
13
14
  //# 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,qDAA0B;AAC1B,gDAAqB;AACrB,oDAAyB;AACzB,mDAAwB;AACxB,mDAAwB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6DAAkC;AAClC,wDAA6B;AAC7B,iDAAsB;AACtB,8DAAmC;AACnC,qDAA0B;AAC1B,gDAAqB;AACrB,oDAAyB;AACzB,6DAAkC;AAClC,mDAAwB;AACxB,mDAAwB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openskidata-format",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "Data format for OpenSkiMap.org",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/Run.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from './ElevationProfile'
8
8
  import { FeatureType } from './FeatureType'
9
9
  import { SkiAreaSummaryFeature } from './SkiArea'
10
+ import { SnowCoverHistory } from './SnowCoverHistory'
10
11
  import { Source } from './Source'
11
12
  import { Status } from './Status'
12
13
  import { exhaustiveMatchingGuard } from './util/exhaustiveMatchingGuard'
@@ -54,6 +55,7 @@ export type RunFeature = GeoJSON.Feature<RunGeometry, RunProperties>
54
55
  * @property {Source[]} sources - Data sources for this run's information
55
56
  * @property {string[]} websites - Websites associated with this run, derived from the OpenStreetMap website tag
56
57
  * @property {string | null} wikidata_id - Wikidata identifier, if available, derived from the OpenStreetMap wikidata tag
58
+ * @property {SnowCoverHistory} [snowCoverHistory] - Historical snow cover data for this run, if available
57
59
  */
58
60
  export type RunProperties = {
59
61
  type: FeatureType.Run
@@ -75,6 +77,7 @@ export type RunProperties = {
75
77
  sources: Source[]
76
78
  websites: string[]
77
79
  wikidata_id: string | null
80
+ snowCoverHistory?: SnowCoverHistory
78
81
  }
79
82
 
80
83
  export enum RunUse {
package/src/SkiArea.ts CHANGED
@@ -2,6 +2,7 @@ import { FeatureType } from './FeatureType'
2
2
  import { LiftType } from './Lift'
3
3
  import { Location } from './Location'
4
4
  import { RunDifficulty, RunDifficultyConvention } from './Run'
5
+ import { SnowCoverHistory } from './SnowCoverHistory'
5
6
  import { Source } from './Source'
6
7
  import { Status } from './Status'
7
8
 
@@ -76,6 +77,15 @@ export type SkiAreaStatistics = {
76
77
  lifts: LiftStatistics
77
78
  minElevation?: number
78
79
  maxElevation?: number
80
+ snowCover?: SkiAreaSnowCoverStatistics
81
+ }
82
+
83
+ /**
84
+ * Aggregated snow cover data for a ski area's runs.
85
+ */
86
+ export type SkiAreaSnowCoverStatistics = {
87
+ overall: SnowCoverHistory
88
+ byActivity: { [key in SkiAreaActivity | 'other']?: SnowCoverHistory }
79
89
  }
80
90
 
81
91
  export type RunStatistics = {
@@ -0,0 +1,103 @@
1
+ import { getMonthlySnowCover, SnowCoverHistory, MonthlySnowCoverHistory } from './SnowCoverHistory'
2
+
3
+ describe('getMonthlySnowCover', () => {
4
+ it('should process daily data into monthly averages', () => {
5
+ const input: SnowCoverHistory = [
6
+ {
7
+ year: 2023,
8
+ days: [
9
+ [1, 80, 95], // Jan 1
10
+ [15, 75, 90], // Jan 15
11
+ [32, 60, 85], // Feb 1
12
+ [46, 65, 80], // Feb 15
13
+ [60, 40, 75], // Mar 1
14
+ [74, 35, 70], // Mar 15
15
+ ]
16
+ }
17
+ ]
18
+
19
+ const result = getMonthlySnowCover(input)
20
+
21
+ expect(result).toHaveLength(1)
22
+ expect(result[0].year).toBe(2023)
23
+ expect(result[0].months).toHaveLength(3)
24
+
25
+ // January: (80*95 + 75*90) / (95 + 90) = (7600 + 6750) / 185 = 77.57
26
+ expect(result[0].months[0][0]).toBe(1) // Month
27
+ expect(result[0].months[0][1]).toBeCloseTo(77.57, 1) // Snow cover
28
+ expect(result[0].months[0][2]).toBe(92.5) // Valid pixels average
29
+
30
+ // February: (60*85 + 65*80) / (85 + 80) = (5100 + 5200) / 165 = 62.42
31
+ expect(result[0].months[1][0]).toBe(2)
32
+ expect(result[0].months[1][1]).toBeCloseTo(62.42, 1)
33
+ expect(result[0].months[1][2]).toBe(82.5)
34
+
35
+ // March: (40*75 + 35*70) / (75 + 70) = (3000 + 2450) / 145 = 37.59
36
+ expect(result[0].months[2][0]).toBe(3)
37
+ expect(result[0].months[2][1]).toBeCloseTo(37.59, 1)
38
+ expect(result[0].months[2][2]).toBe(72.5)
39
+ })
40
+
41
+ it('should handle multiple years', () => {
42
+ const input: SnowCoverHistory = [
43
+ {
44
+ year: 2022,
45
+ days: [[1, 50, 80]]
46
+ },
47
+ {
48
+ year: 2023,
49
+ days: [[1, 60, 90]]
50
+ }
51
+ ]
52
+
53
+ const result = getMonthlySnowCover(input)
54
+
55
+ expect(result).toHaveLength(2)
56
+ expect(result[0].year).toBe(2022)
57
+ expect(result[1].year).toBe(2023)
58
+ })
59
+
60
+ it('should handle empty data', () => {
61
+ const input: SnowCoverHistory = []
62
+ const result = getMonthlySnowCover(input)
63
+ expect(result).toEqual([])
64
+ })
65
+
66
+ it('should handle zero valid pixels gracefully', () => {
67
+ const input: SnowCoverHistory = [
68
+ {
69
+ year: 2023,
70
+ days: [
71
+ [1, 80, 0], // No valid pixels
72
+ [15, 75, 100] // Full valid pixels
73
+ ]
74
+ }
75
+ ]
76
+
77
+ const result = getMonthlySnowCover(input)
78
+
79
+ expect(result[0].months[0][0]).toBe(1)
80
+ expect(result[0].months[0][1]).toBe(75) // Only the second day contributes
81
+ expect(result[0].months[0][2]).toBe(50) // Average of 0 and 100
82
+ })
83
+
84
+ it('should handle data across multiple months', () => {
85
+ const input: SnowCoverHistory = [
86
+ {
87
+ year: 2023,
88
+ days: [
89
+ [1, 80, 90], // January
90
+ [100, 40, 70], // April (day 100)
91
+ [335, 50, 80] // December (day 335)
92
+ ]
93
+ }
94
+ ]
95
+
96
+ const result = getMonthlySnowCover(input)
97
+
98
+ expect(result[0].months).toHaveLength(3)
99
+ expect(result[0].months[0][0]).toBe(1) // January
100
+ expect(result[0].months[1][0]).toBe(4) // April
101
+ expect(result[0].months[2][0]).toBe(12) // December
102
+ })
103
+ })
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Historical snow cover information for a geometry.
3
+ * Data is organized chronologically, with the oldest year first.
4
+ *
5
+ * @property {number} year - The year for this data
6
+ * @property {Array<[number, number, number]>} days - Daily measurements as tuples of [day of year, NDSI snow cover value, % of valid pixels]
7
+ */
8
+ export type SnowCoverHistory = {
9
+ year: number
10
+ /**
11
+ * Array of daily snow cover measurements ordered chronologically by day of year.
12
+ * Each tuple contains:
13
+ * - Day of year (1-365/366)
14
+ * - NDSI snow cover value (0-100) - measures snow presence, not depth
15
+ * - Percentage of valid pixels (0-100) - pixels with valid data for that day
16
+ *
17
+ * Data source: VIIRS/NPP CGF Snow Cover Daily L3 Global 375m product (VNP10A1F)
18
+ * Resolution: 375m (affects measurement accuracy for small geometries)
19
+ * Frequency: Weekly under cloud-free conditions, varies in practice
20
+ *
21
+ * Valid pixel percentage may be less than 100% due to:
22
+ * - Cloud cover
23
+ * - Different cloud gap filling dates for pixels
24
+ * - Incomplete pipeline data
25
+ */
26
+ days: [number, number, number][]
27
+ }[]
28
+
29
+ /**
30
+ * Monthly aggregated snow cover history.
31
+ * Data is organized chronologically, with the oldest year first.
32
+ *
33
+ * @property {number} year - The year for this data
34
+ * @property {Array<[number, number, number]>} months - Monthly aggregations as tuples of [month, average snow cover, average valid pixels]
35
+ */
36
+ export type MonthlySnowCoverHistory = {
37
+ year: number
38
+ /**
39
+ * Array of monthly snow cover aggregations ordered chronologically by month.
40
+ * Each tuple contains:
41
+ * - Month (1-12)
42
+ * - Average NDSI snow cover value (0-100) - weighted by valid pixels
43
+ * - Average percentage of valid pixels (0-100) - across all days in month
44
+ */
45
+ months: [number, number, number][]
46
+ }[]
47
+
48
+ /**
49
+ * Converts daily snow cover measurements to monthly aggregations.
50
+ *
51
+ * Snow cover values are weighted by valid pixel percentages when calculating monthly averages.
52
+ * Input data is assumed to be sorted chronologically.
53
+ *
54
+ * @param snowCoverHistory Daily snow cover history to aggregate
55
+ * @returns Monthly aggregated snow cover history
56
+ */
57
+ export function getMonthlySnowCover(
58
+ snowCoverHistory: SnowCoverHistory
59
+ ): MonthlySnowCoverHistory {
60
+ return snowCoverHistory.map(yearData => {
61
+ const monthlyData = new Map<number, { values: number[], validPixels: number[], count: number }>()
62
+
63
+ // Group daily data by month
64
+ yearData.days.forEach(([dayOfYear, snowCover, validPixelPercent]) => {
65
+ const date = dayOfYearToDate(yearData.year, dayOfYear)
66
+ const month = date.getMonth() + 1 // Convert to 1-12
67
+
68
+ if (!monthlyData.has(month)) {
69
+ monthlyData.set(month, { values: [], validPixels: [], count: 0 })
70
+ }
71
+
72
+ const monthData = monthlyData.get(month)!
73
+ monthData.values.push(snowCover * validPixelPercent / 100) // Weight by valid pixels
74
+ monthData.validPixels.push(validPixelPercent)
75
+ monthData.count++
76
+ })
77
+
78
+ // Calculate monthly averages
79
+ const months: [number, number, number][] = []
80
+ monthlyData.forEach((data, month) => {
81
+ const totalValidPixels = data.validPixels.reduce((sum, val) => sum + val, 0)
82
+ const weightedSnowCover = data.values.reduce((sum, val) => sum + val, 0)
83
+ const avgValidPixels = totalValidPixels / data.count
84
+ const avgSnowCover = avgValidPixels > 0 ? (weightedSnowCover / totalValidPixels) * 100 : 0
85
+
86
+ months.push([month, Math.round(avgSnowCover * 100) / 100, Math.round(avgValidPixels * 100) / 100])
87
+ })
88
+
89
+ return {
90
+ year: yearData.year,
91
+ months
92
+ }
93
+ })
94
+ }
95
+
96
+ /**
97
+ * Converts day of year to a Date object.
98
+ *
99
+ * @param year The year
100
+ * @param dayOfYear Day of year (1-365/366)
101
+ * @returns Date object for the specified day
102
+ */
103
+ function dayOfYearToDate(year: number, dayOfYear: number): Date {
104
+ const date = new Date(year, 0, 1)
105
+ date.setDate(dayOfYear)
106
+ return date
107
+ }
package/src/index.ts CHANGED
@@ -5,5 +5,6 @@ export * from './LiftNameFormatter'
5
5
  export * from './Location'
6
6
  export * from './Run'
7
7
  export * from './SkiArea'
8
+ export * from './SnowCoverHistory'
8
9
  export * from './Source'
9
10
  export * from './Status'