openskidata-format 12.1.0 → 14.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/SkiArea.d.ts CHANGED
@@ -3,6 +3,7 @@ import { LiftType } from './Lift';
3
3
  import { Place } from './Place';
4
4
  import { RunDifficulty } from './Run';
5
5
  import { RunDifficultyConvention } from './RunDifficultyConvention';
6
+ import { SkiPassMembership } from './SkiPass';
6
7
  import { SnowCoverHistory } from './SnowCoverHistory';
7
8
  import { Source } from './Source';
8
9
  import { Status } from './Status';
@@ -41,6 +42,7 @@ export type SkiAreaSummaryFeature = GeoJSON.Feature<SkiAreaGeometry, SkiAreaSumm
41
42
  * @property {string[]} websites - Official website(s) of the ski area. Derived from the OpenStreetMap website tag and Skimap.org data.
42
43
  * @property {string | null} wikidataID - Wikidata identifier. Derived from the OpenStreetMap wikidata tag.
43
44
  * @property {Place[]} places - Geographic places this ski area is within (e.g., city, region, country). Derived from reverse geocoding.
45
+ * @property {SkiPassMembership[]} skiPasses - Multi-resort season passes this ski area is on. Empty if it is on none.
44
46
  */
45
47
  export type SkiAreaProperties = SkiAreaSummaryProperties & {
46
48
  sources: Source[];
@@ -50,6 +52,7 @@ export type SkiAreaProperties = SkiAreaSummaryProperties & {
50
52
  wikidataID: string | null;
51
53
  places: Place[];
52
54
  viewportHint: ViewportHint;
55
+ skiPasses: SkiPassMembership[];
53
56
  };
54
57
  export type SkiAreaSummaryProperties = {
55
58
  type: FeatureType.SkiArea;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"SkiArea.js","sourceRoot":"","sources":["../src/SkiArea.ts"],"names":[],"mappings":";;;AA2EA,IAAY,eAGX;AAHD,WAAY,eAAe;IACzB,wCAAqB,CAAA;IACrB,oCAAiB,CAAA;AACnB,CAAC,EAHW,eAAe,+BAAf,eAAe,QAG1B"}
@@ -0,0 +1,82 @@
1
+ import { Source } from './Source';
2
+ /** Stable identifier for a purchasable ski pass, e.g. "ikon-base". */
3
+ export type SkiPassID = string;
4
+ /** Stable identifier for a family of related ski passes, e.g. "ikon". */
5
+ export type SkiPassBrandID = string;
6
+ /**
7
+ * A ski area's membership of one purchasable ski pass.
8
+ *
9
+ * @property {SkiPassID} passID - Identifier of the ski pass.
10
+ * @property {string} passName - Display name of the ski pass.
11
+ * @property {SkiPassBrandID | null} brandID - Identifier of the pass's brand, or null for a standalone pass.
12
+ * @property {string | null} brandName - Display name of the pass's brand, or null for a standalone pass.
13
+ * @property {string | null} access - Access code, copied verbatim from the source, e.g. "5, 26, 27" or "U". The source documents these per pass in free text, so they are not parsed.
14
+ * @property {number | null} yearJoined - Year the ski area joined this pass, when the source records it.
15
+ * @property {Source[]} sources - Data sources.
16
+ */
17
+ export type SkiPassMembership = {
18
+ passID: SkiPassID;
19
+ passName: string;
20
+ brandID: SkiPassBrandID | null;
21
+ brandName: string | null;
22
+ access: string | null;
23
+ yearJoined: number | null;
24
+ sources: Source[];
25
+ };
26
+ /**
27
+ * A family of related ski passes, used to group the purchasable passes in a catalogue.
28
+ *
29
+ * @property {SkiPassBrandID} id - Stable identifier for the brand.
30
+ * @property {string} name - Display name of the brand.
31
+ * @property {Source[]} sources - Data sources shared by the brand's passes.
32
+ */
33
+ export type SkiPassBrand = {
34
+ type: 'skiPassBrand';
35
+ id: SkiPassBrandID;
36
+ name: string;
37
+ sources: Source[];
38
+ };
39
+ /**
40
+ * A multi-resort season pass.
41
+ *
42
+ * Unlike runs, lifts, ski areas and spots, a ski pass is not a geographic feature: it is a
43
+ * commercial product that covers a set of ski areas. The ski areas it covers each carry a
44
+ * matching `SkiPassMembership`.
45
+ *
46
+ * @property {SkiPassID} id - Unique identifier for the ski pass. Unlike a ski area ID, this is stable across runs.
47
+ * @property {string} name - Name of the ski pass.
48
+ * @property {SkiPassBrandID | null} brandID - Identifier of the pass's brand, or null for a standalone pass.
49
+ * @property {string | null} brandName - Display name of the pass's brand, or null for a standalone pass.
50
+ * @property {Source[]} sources - Data sources.
51
+ * @property {number} skiAreaCount - Number of ski areas this pass covers. Equal to the length of skiAreaIDs.
52
+ * @property {string[]} skiAreaIDs - IDs of the ski areas this pass covers, sorted, so that the order does not depend on anything outside this array.
53
+ * @property {SkiPassUnresolvedRosterEntry[]} unresolvedRosterEntries - Ski areas the source lists on this pass that could not be resolved to a ski area feature.
54
+ */
55
+ export type SkiPass = {
56
+ type: 'skiPass';
57
+ id: SkiPassID;
58
+ name: string;
59
+ brandID: SkiPassBrandID | null;
60
+ brandName: string | null;
61
+ sources: Source[];
62
+ skiAreaCount: number;
63
+ skiAreaIDs: string[];
64
+ unresolvedRosterEntries: SkiPassUnresolvedRosterEntry[];
65
+ };
66
+ /** The complete non-geographic ski-pass dataset published alongside the ski-area data. */
67
+ export type SkiPassCatalog = {
68
+ brands: SkiPassBrand[];
69
+ passes: SkiPass[];
70
+ };
71
+ /**
72
+ * A ski area listed on a pass by the source, which has no corresponding ski area feature.
73
+ *
74
+ * @property {string} name - Ski area name as written by the source.
75
+ * @property {string} location - Location as written by the source, e.g. "U.S. - Colorado".
76
+ * @property {string} reason - Why the entry could not be resolved.
77
+ */
78
+ export type SkiPassUnresolvedRosterEntry = {
79
+ name: string;
80
+ location: string;
81
+ reason: string;
82
+ };
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=SkiPass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SkiPass.js","sourceRoot":"","sources":["../src/SkiPass.ts"],"names":[],"mappings":""}
package/dist/Source.d.ts CHANGED
@@ -1,10 +1,23 @@
1
1
  export declare enum SourceType {
2
2
  SKIMAP_ORG = "skimap.org",
3
- OPENSTREETMAP = "openstreetmap"
3
+ OPENSTREETMAP = "openstreetmap",
4
+ STORM_SKIING = "stormskiing.com"
4
5
  }
5
6
  export type Source = {
6
7
  type: SourceType;
7
8
  id: string;
8
9
  };
10
+ /**
11
+ * The Storm Skiing Journal's ski pass chart, a Google Sheets document.
12
+ *
13
+ * A `STORM_SKIING` source id references a single cell of that document, in `<gid>!<A1>` form
14
+ * (e.g. `677843907!AI64`), so it can be linked to directly.
15
+ */
16
+ export declare const STORM_SKIING_CHART_DOCUMENT_ID = "1G2-l2DVg7-QwroOi7EqRDrJYJ4ICYLcJbLx-nARJdrA";
17
+ /**
18
+ * URL of the CSV export of one sheet of the ski pass chart. Used to fetch the chart; the ids of
19
+ * the sources derived from it are resolved by `getSourceURL`.
20
+ */
21
+ export declare function getStormSkiingChartCSVURL(gid: string): string;
9
22
  export declare function getSourceURL(source: Source): string;
10
23
  export declare function getSourceName(type: SourceType): string;
package/dist/Source.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SourceType = void 0;
3
+ exports.STORM_SKIING_CHART_DOCUMENT_ID = exports.SourceType = void 0;
4
+ exports.getStormSkiingChartCSVURL = getStormSkiingChartCSVURL;
4
5
  exports.getSourceURL = getSourceURL;
5
6
  exports.getSourceName = getSourceName;
6
7
  const exhaustiveMatchingGuard_1 = require("./util/exhaustiveMatchingGuard");
@@ -8,13 +9,41 @@ var SourceType;
8
9
  (function (SourceType) {
9
10
  SourceType["SKIMAP_ORG"] = "skimap.org";
10
11
  SourceType["OPENSTREETMAP"] = "openstreetmap";
12
+ SourceType["STORM_SKIING"] = "stormskiing.com";
11
13
  })(SourceType || (exports.SourceType = SourceType = {}));
14
+ /**
15
+ * The Storm Skiing Journal's ski pass chart, a Google Sheets document.
16
+ *
17
+ * A `STORM_SKIING` source id references a single cell of that document, in `<gid>!<A1>` form
18
+ * (e.g. `677843907!AI64`), so it can be linked to directly.
19
+ */
20
+ exports.STORM_SKIING_CHART_DOCUMENT_ID = '1G2-l2DVg7-QwroOi7EqRDrJYJ4ICYLcJbLx-nARJdrA';
21
+ /**
22
+ * URL of the CSV export of one sheet of the ski pass chart. Used to fetch the chart; the ids of
23
+ * the sources derived from it are resolved by `getSourceURL`.
24
+ */
25
+ function getStormSkiingChartCSVURL(gid) {
26
+ return (`https://docs.google.com/spreadsheets/d/${exports.STORM_SKIING_CHART_DOCUMENT_ID}` +
27
+ `/export?format=csv&gid=${gid}`);
28
+ }
12
29
  function getSourceURL(source) {
13
30
  switch (source.type) {
14
31
  case SourceType.OPENSTREETMAP:
15
32
  return 'https://www.openstreetmap.org/' + source.id;
16
33
  case SourceType.SKIMAP_ORG:
17
34
  return 'https://www.skimap.org/SkiAreas/view/' + source.id;
35
+ case SourceType.STORM_SKIING: {
36
+ const separatorIndex = source.id.indexOf('!');
37
+ if (separatorIndex === -1) {
38
+ throw new Error(`Malformed ${SourceType.STORM_SKIING} source id "${source.id}". Expected <gid>!<A1 reference>.`);
39
+ }
40
+ const gid = source.id.slice(0, separatorIndex);
41
+ const range = source.id.slice(separatorIndex + 1);
42
+ // The gid appears twice: as a query parameter to select the sheet, and in the fragment,
43
+ // which is the only place Google Sheets accepts a cell range.
44
+ return (`https://docs.google.com/spreadsheets/d/${exports.STORM_SKIING_CHART_DOCUMENT_ID}` +
45
+ `/edit?gid=${gid}#gid=${gid}&range=${range}`);
46
+ }
18
47
  default:
19
48
  return (0, exhaustiveMatchingGuard_1.exhaustiveMatchingGuard)(source.type);
20
49
  }
@@ -25,6 +54,8 @@ function getSourceName(type) {
25
54
  return 'OpenStreetMap';
26
55
  case SourceType.SKIMAP_ORG:
27
56
  return 'Skimap.org';
57
+ case SourceType.STORM_SKIING:
58
+ return 'The Storm Skiing Journal';
28
59
  default:
29
60
  return (0, exhaustiveMatchingGuard_1.exhaustiveMatchingGuard)(type);
30
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Source.js","sourceRoot":"","sources":["../src/Source.ts"],"names":[],"mappings":";;;AAYA,oCASC;AAED,sCASC;AAhCD,4EAAwE;AAExE,IAAY,UAGX;AAHD,WAAY,UAAU;IACpB,uCAAyB,CAAA;IACzB,6CAA+B,CAAA;AACjC,CAAC,EAHW,UAAU,0BAAV,UAAU,QAGrB;AAOD,SAAgB,YAAY,CAAC,MAAc;IACzC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,UAAU,CAAC,aAAa;YAC3B,OAAO,gCAAgC,GAAG,MAAM,CAAC,EAAE,CAAA;QACrD,KAAK,UAAU,CAAC,UAAU;YACxB,OAAO,uCAAuC,GAAG,MAAM,CAAC,EAAE,CAAA;QAC5D;YACE,OAAO,IAAA,iDAAuB,EAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/C,CAAC;AACH,CAAC;AAED,SAAgB,aAAa,CAAC,IAAgB;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,aAAa;YAC3B,OAAO,eAAe,CAAA;QACxB,KAAK,UAAU,CAAC,UAAU;YACxB,OAAO,YAAY,CAAA;QACrB;YACE,OAAO,IAAA,iDAAuB,EAAC,IAAI,CAAC,CAAA;IACxC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"Source.js","sourceRoot":"","sources":["../src/Source.ts"],"names":[],"mappings":";;;AA0BA,8DAKC;AAED,oCAyBC;AAED,sCAWC;AAvED,4EAAwE;AAExE,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,uCAAyB,CAAA;IACzB,6CAA+B,CAAA;IAC/B,8CAAgC,CAAA;AAClC,CAAC,EAJW,UAAU,0BAAV,UAAU,QAIrB;AAOD;;;;;GAKG;AACU,QAAA,8BAA8B,GACzC,8CAA8C,CAAA;AAEhD;;;GAGG;AACH,SAAgB,yBAAyB,CAAC,GAAW;IACnD,OAAO,CACL,0CAA0C,sCAA8B,EAAE;QAC1E,0BAA0B,GAAG,EAAE,CAChC,CAAA;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,MAAc;IACzC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,UAAU,CAAC,aAAa;YAC3B,OAAO,gCAAgC,GAAG,MAAM,CAAC,EAAE,CAAA;QACrD,KAAK,UAAU,CAAC,UAAU;YACxB,OAAO,uCAAuC,GAAG,MAAM,CAAC,EAAE,CAAA;QAC5D,KAAK,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC;YAC7B,MAAM,cAAc,GAAG,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YAC7C,IAAI,cAAc,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CACb,aAAa,UAAU,CAAC,YAAY,eAAe,MAAM,CAAC,EAAE,mCAAmC,CAChG,CAAA;YACH,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAA;YAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC,CAAA;YACjD,wFAAwF;YACxF,8DAA8D;YAC9D,OAAO,CACL,0CAA0C,sCAA8B,EAAE;gBAC1E,aAAa,GAAG,QAAQ,GAAG,UAAU,KAAK,EAAE,CAC7C,CAAA;QACH,CAAC;QACD;YACE,OAAO,IAAA,iDAAuB,EAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/C,CAAC;AACH,CAAC;AAED,SAAgB,aAAa,CAAC,IAAgB;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,aAAa;YAC3B,OAAO,eAAe,CAAA;QACxB,KAAK,UAAU,CAAC,UAAU;YACxB,OAAO,YAAY,CAAA;QACrB,KAAK,UAAU,CAAC,YAAY;YAC1B,OAAO,0BAA0B,CAAA;QACnC;YACE,OAAO,IAAA,iDAAuB,EAAC,IAAI,CAAC,CAAA;IACxC,CAAC;AACH,CAAC"}
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './Run';
7
7
  export * from './RunDifficultyConvention';
8
8
  export * from './SlopeGradingScale';
9
9
  export * from './SkiArea';
10
+ export * from './SkiPass';
10
11
  export * from './SnowCoverHistory';
11
12
  export * from './Source';
12
13
  export * from './Spot';
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ tslib_1.__exportStar(require("./Run"), exports);
10
10
  tslib_1.__exportStar(require("./RunDifficultyConvention"), exports);
11
11
  tslib_1.__exportStar(require("./SlopeGradingScale"), exports);
12
12
  tslib_1.__exportStar(require("./SkiArea"), exports);
13
+ tslib_1.__exportStar(require("./SkiPass"), exports);
13
14
  tslib_1.__exportStar(require("./SnowCoverHistory"), exports);
14
15
  tslib_1.__exportStar(require("./Source"), exports);
15
16
  tslib_1.__exportStar(require("./Spot"), exports);
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;AACxB,yDAA8B"}
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,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": "12.1.0",
3
+ "version": "14.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/SkiArea.ts CHANGED
@@ -3,6 +3,7 @@ import { LiftType } from './Lift'
3
3
  import { Place } from './Place'
4
4
  import { RunDifficulty } from './Run'
5
5
  import { RunDifficultyConvention } from './RunDifficultyConvention'
6
+ import { SkiPassMembership } from './SkiPass'
6
7
  import { SnowCoverHistory } from './SnowCoverHistory'
7
8
  import { Source } from './Source'
8
9
  import { Status } from './Status'
@@ -51,6 +52,7 @@ export type SkiAreaSummaryFeature = GeoJSON.Feature<
51
52
  * @property {string[]} websites - Official website(s) of the ski area. Derived from the OpenStreetMap website tag and Skimap.org data.
52
53
  * @property {string | null} wikidataID - Wikidata identifier. Derived from the OpenStreetMap wikidata tag.
53
54
  * @property {Place[]} places - Geographic places this ski area is within (e.g., city, region, country). Derived from reverse geocoding.
55
+ * @property {SkiPassMembership[]} skiPasses - Multi-resort season passes this ski area is on. Empty if it is on none.
54
56
  */
55
57
  export type SkiAreaProperties = SkiAreaSummaryProperties & {
56
58
  sources: Source[]
@@ -60,6 +62,7 @@ export type SkiAreaProperties = SkiAreaSummaryProperties & {
60
62
  wikidataID: string | null
61
63
  places: Place[]
62
64
  viewportHint: ViewportHint
65
+ skiPasses: SkiPassMembership[]
63
66
  }
64
67
 
65
68
  export type SkiAreaSummaryProperties = {
package/src/SkiPass.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { Source } from './Source'
2
+
3
+ /** Stable identifier for a purchasable ski pass, e.g. "ikon-base". */
4
+ export type SkiPassID = string
5
+
6
+ /** Stable identifier for a family of related ski passes, e.g. "ikon". */
7
+ export type SkiPassBrandID = string
8
+
9
+ /**
10
+ * A ski area's membership of one purchasable ski pass.
11
+ *
12
+ * @property {SkiPassID} passID - Identifier of the ski pass.
13
+ * @property {string} passName - Display name of the ski pass.
14
+ * @property {SkiPassBrandID | null} brandID - Identifier of the pass's brand, or null for a standalone pass.
15
+ * @property {string | null} brandName - Display name of the pass's brand, or null for a standalone pass.
16
+ * @property {string | null} access - Access code, copied verbatim from the source, e.g. "5, 26, 27" or "U". The source documents these per pass in free text, so they are not parsed.
17
+ * @property {number | null} yearJoined - Year the ski area joined this pass, when the source records it.
18
+ * @property {Source[]} sources - Data sources.
19
+ */
20
+ export type SkiPassMembership = {
21
+ passID: SkiPassID
22
+ passName: string
23
+ brandID: SkiPassBrandID | null
24
+ brandName: string | null
25
+ access: string | null
26
+ yearJoined: number | null
27
+ sources: Source[]
28
+ }
29
+
30
+ /**
31
+ * A family of related ski passes, used to group the purchasable passes in a catalogue.
32
+ *
33
+ * @property {SkiPassBrandID} id - Stable identifier for the brand.
34
+ * @property {string} name - Display name of the brand.
35
+ * @property {Source[]} sources - Data sources shared by the brand's passes.
36
+ */
37
+ export type SkiPassBrand = {
38
+ type: 'skiPassBrand'
39
+ id: SkiPassBrandID
40
+ name: string
41
+ sources: Source[]
42
+ }
43
+
44
+ /**
45
+ * A multi-resort season pass.
46
+ *
47
+ * Unlike runs, lifts, ski areas and spots, a ski pass is not a geographic feature: it is a
48
+ * commercial product that covers a set of ski areas. The ski areas it covers each carry a
49
+ * matching `SkiPassMembership`.
50
+ *
51
+ * @property {SkiPassID} id - Unique identifier for the ski pass. Unlike a ski area ID, this is stable across runs.
52
+ * @property {string} name - Name of the ski pass.
53
+ * @property {SkiPassBrandID | null} brandID - Identifier of the pass's brand, or null for a standalone pass.
54
+ * @property {string | null} brandName - Display name of the pass's brand, or null for a standalone pass.
55
+ * @property {Source[]} sources - Data sources.
56
+ * @property {number} skiAreaCount - Number of ski areas this pass covers. Equal to the length of skiAreaIDs.
57
+ * @property {string[]} skiAreaIDs - IDs of the ski areas this pass covers, sorted, so that the order does not depend on anything outside this array.
58
+ * @property {SkiPassUnresolvedRosterEntry[]} unresolvedRosterEntries - Ski areas the source lists on this pass that could not be resolved to a ski area feature.
59
+ */
60
+ export type SkiPass = {
61
+ type: 'skiPass'
62
+ id: SkiPassID
63
+ name: string
64
+ brandID: SkiPassBrandID | null
65
+ brandName: string | null
66
+ sources: Source[]
67
+ skiAreaCount: number
68
+ skiAreaIDs: string[]
69
+ unresolvedRosterEntries: SkiPassUnresolvedRosterEntry[]
70
+ }
71
+
72
+ /** The complete non-geographic ski-pass dataset published alongside the ski-area data. */
73
+ export type SkiPassCatalog = {
74
+ brands: SkiPassBrand[]
75
+ passes: SkiPass[]
76
+ }
77
+
78
+ /**
79
+ * A ski area listed on a pass by the source, which has no corresponding ski area feature.
80
+ *
81
+ * @property {string} name - Ski area name as written by the source.
82
+ * @property {string} location - Location as written by the source, e.g. "U.S. - Colorado".
83
+ * @property {string} reason - Why the entry could not be resolved.
84
+ */
85
+ export type SkiPassUnresolvedRosterEntry = {
86
+ name: string
87
+ location: string
88
+ reason: string
89
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ getSourceName,
3
+ getSourceURL,
4
+ getStormSkiingChartCSVURL,
5
+ SourceType,
6
+ } from './Source'
7
+
8
+ describe('getSourceURL', () => {
9
+ it('links to an OpenStreetMap element', () => {
10
+ expect(
11
+ getSourceURL({ type: SourceType.OPENSTREETMAP, id: 'relation/5883752' }),
12
+ ).toBe('https://www.openstreetmap.org/relation/5883752')
13
+ })
14
+
15
+ it('links to a Skimap.org ski area', () => {
16
+ expect(getSourceURL({ type: SourceType.SKIMAP_ORG, id: '181' })).toBe(
17
+ 'https://www.skimap.org/SkiAreas/view/181',
18
+ )
19
+ })
20
+
21
+ it('links to a cell of the ski pass chart', () => {
22
+ expect(
23
+ getSourceURL({ type: SourceType.STORM_SKIING, id: '677843907!AI64' }),
24
+ ).toBe(
25
+ 'https://docs.google.com/spreadsheets/d/1G2-l2DVg7-QwroOi7EqRDrJYJ4ICYLcJbLx-nARJdrA/edit?gid=677843907#gid=677843907&range=AI64',
26
+ )
27
+ })
28
+
29
+ it('rejects a ski pass chart id without a sheet', () => {
30
+ expect(() =>
31
+ getSourceURL({ type: SourceType.STORM_SKIING, id: 'AI64' }),
32
+ ).toThrow('Malformed stormskiing.com source id')
33
+ })
34
+ })
35
+
36
+ describe('getSourceName', () => {
37
+ it('names every source type', () => {
38
+ for (const type of Object.values(SourceType)) {
39
+ expect(getSourceName(type).length).toBeGreaterThan(0)
40
+ }
41
+ })
42
+ })
43
+
44
+ describe('getStormSkiingChartCSVURL', () => {
45
+ it('builds the CSV export URL for a sheet', () => {
46
+ expect(getStormSkiingChartCSVURL('677843907')).toBe(
47
+ 'https://docs.google.com/spreadsheets/d/1G2-l2DVg7-QwroOi7EqRDrJYJ4ICYLcJbLx-nARJdrA/export?format=csv&gid=677843907',
48
+ )
49
+ })
50
+ })
package/src/Source.ts CHANGED
@@ -3,6 +3,7 @@ import { exhaustiveMatchingGuard } from './util/exhaustiveMatchingGuard'
3
3
  export enum SourceType {
4
4
  SKIMAP_ORG = 'skimap.org',
5
5
  OPENSTREETMAP = 'openstreetmap',
6
+ STORM_SKIING = 'stormskiing.com',
6
7
  }
7
8
 
8
9
  export type Source = {
@@ -10,12 +11,48 @@ export type Source = {
10
11
  id: string
11
12
  }
12
13
 
14
+ /**
15
+ * The Storm Skiing Journal's ski pass chart, a Google Sheets document.
16
+ *
17
+ * A `STORM_SKIING` source id references a single cell of that document, in `<gid>!<A1>` form
18
+ * (e.g. `677843907!AI64`), so it can be linked to directly.
19
+ */
20
+ export const STORM_SKIING_CHART_DOCUMENT_ID =
21
+ '1G2-l2DVg7-QwroOi7EqRDrJYJ4ICYLcJbLx-nARJdrA'
22
+
23
+ /**
24
+ * URL of the CSV export of one sheet of the ski pass chart. Used to fetch the chart; the ids of
25
+ * the sources derived from it are resolved by `getSourceURL`.
26
+ */
27
+ export function getStormSkiingChartCSVURL(gid: string): string {
28
+ return (
29
+ `https://docs.google.com/spreadsheets/d/${STORM_SKIING_CHART_DOCUMENT_ID}` +
30
+ `/export?format=csv&gid=${gid}`
31
+ )
32
+ }
33
+
13
34
  export function getSourceURL(source: Source): string {
14
35
  switch (source.type) {
15
36
  case SourceType.OPENSTREETMAP:
16
37
  return 'https://www.openstreetmap.org/' + source.id
17
38
  case SourceType.SKIMAP_ORG:
18
39
  return 'https://www.skimap.org/SkiAreas/view/' + source.id
40
+ case SourceType.STORM_SKIING: {
41
+ const separatorIndex = source.id.indexOf('!')
42
+ if (separatorIndex === -1) {
43
+ throw new Error(
44
+ `Malformed ${SourceType.STORM_SKIING} source id "${source.id}". Expected <gid>!<A1 reference>.`,
45
+ )
46
+ }
47
+ const gid = source.id.slice(0, separatorIndex)
48
+ const range = source.id.slice(separatorIndex + 1)
49
+ // The gid appears twice: as a query parameter to select the sheet, and in the fragment,
50
+ // which is the only place Google Sheets accepts a cell range.
51
+ return (
52
+ `https://docs.google.com/spreadsheets/d/${STORM_SKIING_CHART_DOCUMENT_ID}` +
53
+ `/edit?gid=${gid}#gid=${gid}&range=${range}`
54
+ )
55
+ }
19
56
  default:
20
57
  return exhaustiveMatchingGuard(source.type)
21
58
  }
@@ -27,6 +64,8 @@ export function getSourceName(type: SourceType): string {
27
64
  return 'OpenStreetMap'
28
65
  case SourceType.SKIMAP_ORG:
29
66
  return 'Skimap.org'
67
+ case SourceType.STORM_SKIING:
68
+ return 'The Storm Skiing Journal'
30
69
  default:
31
70
  return exhaustiveMatchingGuard(type)
32
71
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from './Run'
7
7
  export * from './RunDifficultyConvention'
8
8
  export * from './SlopeGradingScale'
9
9
  export * from './SkiArea'
10
+ export * from './SkiPass'
10
11
  export * from './SnowCoverHistory'
11
12
  export * from './Source'
12
13
  export * from './Spot'