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 +3 -0
- package/dist/Run.js.map +1 -1
- package/dist/SkiArea.d.ts +11 -0
- package/dist/SkiArea.js.map +1 -1
- package/dist/SnowCoverHistory.d.ts +55 -0
- package/dist/SnowCoverHistory.js +55 -0
- package/dist/SnowCoverHistory.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Run.ts +3 -0
- package/src/SkiArea.ts +10 -0
- package/src/SnowCoverHistory.test.ts +103 -0
- package/src/SnowCoverHistory.ts +107 -0
- package/src/index.ts +1 -0
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":";;;
|
|
1
|
+
{"version":3,"file":"Run.js","sourceRoot":"","sources":["../src/Run.ts"],"names":[],"mappings":";;;AAuKA,kDAUC;AAED,kCAKC;AAED,kDAiBC;AAED,0CAyDC;AArQD,yDAK2B;AAM3B,4EAAwE;AAsExE,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;AAED;;;GAGG;AACH,IAAY,uBA4BX;AA5BD,WAAY,uBAAuB;IACjC;;;;;;;OAOG;IACH,4CAAiB,CAAA;IAEjB;;;;;;OAMG;IACH,0CAAe,CAAA;IAEf;;;;;;OAMG;IACH,0DAA+B,CAAA;AACjC,CAAC,EA5BW,uBAAuB,uCAAvB,uBAAuB,QA4BlC;AAED,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,uBAAuB,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,uBAAuB,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,uBAAuB,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
|
@@ -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;
|
package/dist/SkiArea.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SkiArea.js","sourceRoot":"","sources":["../src/SkiArea.ts"],"names":[],"mappings":";;;
|
|
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
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
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
|
+
}
|