@johnmorrisdotca/kyogijo-stadiums 0.29.0 → 0.32.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.
@@ -5,6 +5,7 @@ export const STADIUM_OSM = {
5
5
  "base": "2026-10-20T21:46:16Z",
6
6
  "licence": "ODbL-1.0",
7
7
  "attribution": "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright",
8
+ "height": 61,
8
9
  "axis": { "heading": 52.8, "basis": "opening", "strength": 1.47, "elongation": 1.19, "centre": [-123.112002, 49.276698] },
9
10
  "roof": {
10
11
  "source": "relation/5836463",
@@ -55,4 +56,117 @@ export const STADIUM_OSM = {
55
56
  [-123.113623, 49.276478],
56
57
  ],
57
58
  },
59
+ "lumen-field": {
60
+ "source": "relation/2305831",
61
+ "read": "2026-10-20",
62
+ "base": "2026-10-20T23:05:49Z",
63
+ "licence": "ODbL-1.0",
64
+ "attribution": "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright",
65
+ "axis": { "heading": 0, "basis": "pitch", "strength": 1.32, "elongation": 2.16, "centre": [-122.33163, 47.595154] },
66
+ "canopies": [
67
+ { "source": "way/458208106", "height": 36, "rise": 10, "ring": [
68
+ [-122.332395, 47.596154],
69
+ [-122.33234, 47.595145],
70
+ [-122.332404, 47.594143],
71
+ [-122.332683, 47.594276],
72
+ [-122.332869, 47.594407],
73
+ [-122.333005, 47.594605],
74
+ [-122.333075, 47.594764],
75
+ [-122.333159, 47.595095],
76
+ [-122.33313, 47.59539],
77
+ [-122.332993, 47.595744],
78
+ [-122.332861, 47.59592],
79
+ [-122.332747, 47.596004],
80
+ [-122.332452, 47.596154],
81
+ ] },
82
+ { "source": "way/458208107", "height": 36, "rise": 10, "ring": [
83
+ [-122.330897, 47.596155],
84
+ [-122.330837, 47.596159],
85
+ [-122.33062, 47.596055],
86
+ [-122.330448, 47.595948],
87
+ [-122.330298, 47.595762],
88
+ [-122.330117, 47.5953],
89
+ [-122.330116, 47.59501],
90
+ [-122.330212, 47.594711],
91
+ [-122.33041, 47.594391],
92
+ [-122.330582, 47.594279],
93
+ [-122.330872, 47.59415],
94
+ [-122.330942, 47.595174],
95
+ [-122.330936, 47.595579],
96
+ ] },
97
+ ],
98
+ "ring": [
99
+ [-122.332249, 47.59627],
100
+ [-122.332463, 47.596203],
101
+ [-122.332661, 47.596118],
102
+ [-122.33284, 47.596015],
103
+ [-122.332996, 47.595897],
104
+ [-122.333126, 47.595765],
105
+ [-122.333162, 47.595677],
106
+ [-122.333156, 47.594577],
107
+ [-122.333041, 47.594455],
108
+ [-122.332904, 47.594343],
109
+ [-122.332747, 47.594244],
110
+ [-122.332574, 47.594159],
111
+ [-122.332187, 47.594034],
112
+ [-122.331979, 47.593997],
113
+ [-122.331766, 47.593978],
114
+ [-122.331338, 47.593994],
115
+ [-122.331311, 47.594047],
116
+ [-122.331129, 47.594028],
117
+ [-122.330739, 47.594148],
118
+ [-122.330405, 47.594329],
119
+ [-122.330265, 47.594439],
120
+ [-122.330166, 47.594722],
121
+ [-122.330125, 47.59496],
122
+ [-122.330117, 47.5953],
123
+ [-122.330168, 47.595589],
124
+ [-122.330269, 47.59587],
125
+ [-122.330506, 47.596048],
126
+ [-122.330646, 47.596124],
127
+ [-122.331021, 47.596263],
128
+ [-122.331021, 47.596093],
129
+ [-122.331134, 47.596093],
130
+ [-122.331227, 47.596281],
131
+ [-122.332031, 47.596283],
132
+ [-122.332131, 47.596089],
133
+ [-122.33225, 47.596089],
134
+ ],
135
+ },
136
+ "t-mobile-park": {
137
+ "source": "relation/3008332",
138
+ "read": "2026-10-20",
139
+ "base": "2026-10-20T23:06:50Z",
140
+ "licence": "ODbL-1.0",
141
+ "attribution": "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright",
142
+ "height": 21,
143
+ "axis": { "heading": 49.4, "basis": "infield", "strength": 1.43, "elongation": 1, "centre": [-122.332905, 47.591117] },
144
+ "ring": [
145
+ [-122.330843, 47.59228],
146
+ [-122.331382, 47.590448],
147
+ [-122.333677, 47.590452],
148
+ [-122.333678, 47.59047],
149
+ [-122.333819, 47.590456],
150
+ [-122.333945, 47.5905],
151
+ [-122.333997, 47.590554],
152
+ [-122.334013, 47.590618],
153
+ [-122.333989, 47.590681],
154
+ [-122.33393, 47.59073],
155
+ [-122.333978, 47.59073],
156
+ [-122.333975, 47.59216],
157
+ [-122.333739, 47.59216],
158
+ [-122.33374, 47.591759],
159
+ [-122.333776, 47.591759],
160
+ [-122.333776, 47.591748],
161
+ [-122.333683, 47.591723],
162
+ [-122.333451, 47.592099],
163
+ [-122.333388, 47.592119],
164
+ [-122.333409, 47.592138],
165
+ [-122.333293, 47.5922],
166
+ [-122.333231, 47.592154],
167
+ [-122.333131, 47.592213],
168
+ [-122.333256, 47.592215],
169
+ [-122.333251, 47.592271],
170
+ ],
171
+ },
58
172
  };
package/dist/strings.d.ts CHANGED
@@ -516,6 +516,10 @@ export interface KyogijoWords {
516
516
  stadiumOpening: string;
517
517
  stadiumBuilding: string;
518
518
  stadiumNone: string;
519
+ /** A ballpark's stands: {seats} seats fitted inside the outline of {stadium}'s building; and how the pitch or the infield says the field lies. */
520
+ stadiumPark: string;
521
+ stadiumPitch: string;
522
+ stadiumInfield: string;
519
523
  /** A district: a plan of {count} real venues at their real positions, {mode}; and what `mode` says for the two kinds of plan, and the word before the credit. */
520
524
  district: string;
521
525
  districtNeighbours: string;
package/dist/strings.js CHANGED
@@ -199,7 +199,7 @@ export const KYOGIJO_STRINGS = {
199
199
  },
200
200
  },
201
201
  scoreTitle: "{away} {awayScore}, {home} {homeScore}",
202
- said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", stadium: "The stands are a plausible bowl of {seats} seats in {tiers} bowls, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the stadium's plan.", stadiumOpening: "The field lies along the opening in the roof, which is the size of the field, and the arrow points north.", stadiumBuilding: "The field lies along the building's long axis, which is an inference, and the arrow points north.", stadiumNone: "The field's orientation is not known, so it is drawn east-west with north at the top.", district: "A plan of {count} real venues at their real positions and orientations, from the outlines OpenStreetMap maps; {mode}.", districtNeighbours: "it is fitted close round the buildings", districtCity: "it keeps the real distances, with a grid of kilometres and a scale", districtCredit: "Credit:", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
202
+ said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", stadium: "The stands are a plausible bowl of {seats} seats in {tiers} bowls, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the stadium's plan.", stadiumOpening: "The field lies along the opening in the roof, which is the size of the field, and the arrow points north.", stadiumBuilding: "The field lies along the building's long axis, which is an inference, and the arrow points north.", stadiumNone: "The field's orientation is not known, so it is drawn east-west with north at the top.", stadiumPark: "The stands are the usual stands of a ballpark, each band as deep as its seats need, for {seats} seats, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the park's plan.", stadiumPitch: "The field lies as OpenStreetMap maps the pitch, and the arrow points north.", stadiumInfield: "The way home plate faces is worked out from the infield OpenStreetMap maps and the building's outline, and the arrow points north.", district: "A plan of {count} real venues at their real positions and orientations, from the outlines OpenStreetMap maps; {mode}.", districtNeighbours: "it is fitted close round the buildings", districtCity: "it keeps the real distances, with a grid of kilometres and a scale", districtCredit: "Credit:", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
203
203
  budo: {
204
204
  sides: { north: "north", south: "south", east: "east", west: "west" },
205
205
  judo: "a judo contest area of {contest} by {contest} metres in a safety area {safety} metres wide, laid with tatami of {tatami}",
@@ -758,7 +758,7 @@ export const KYOGIJO_STRINGS = {
758
758
  },
759
759
  },
760
760
  scoreTitle: "{away} {awayScore}、{home} {homeScore}",
761
- said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", stadium: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層のスタンドを推定して描いています。スタジアムの設計図ではありません。", stadiumOpening: "フィールドは屋根の開口部(フィールドと同じ大きさ)に沿って置いており、矢印は北を指します。", stadiumBuilding: "フィールドは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumNone: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
761
+ said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", stadium: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層のスタンドを推定して描いています。スタジアムの設計図ではありません。", stadiumOpening: "フィールドは屋根の開口部(フィールドと同じ大きさ)に沿って置いており、矢印は北を指します。", stadiumBuilding: "フィールドは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumNone: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", stadiumPark: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席のスタンドを推定して描いています。球場の設計図ではありません。", stadiumPitch: "フィールドの向きはOpenStreetMapに記録されたピッチのとおりで、矢印は北を指します。", stadiumInfield: "ホームプレートの向きは、OpenStreetMapに記録された内野と建物の輪郭から推定しており、矢印は北を指します。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
762
762
  budo: {
763
763
  sides: { north: "北", south: "南", east: "東", west: "西" },
764
764
  judo: "柔道の試合場。試合場内は{contest}m四方、幅{safety}mの安全地帯があり、{tatami}の畳を敷いています",
package/dist/venue.js CHANGED
@@ -116,16 +116,16 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
116
116
  }
117
117
  // The low wall under the first row of a tier the walks do not lead to (a ballpark's wall seats stand on the fence, which the fence's own planes make).
118
118
  if (ring.tier !== "wall" && !chained.has(ring) && ring.height[0] > 0.5) {
119
- for (const strip of strips) {
119
+ strips.forEach((strip, si) => {
120
120
  for (let i = 0; i + 1 < strip.inner.length; i += 1) {
121
121
  const [a, b] = [strip.inner[i], strip.inner[i + 1]];
122
122
  if (Math.hypot(b[0] - a[0], b[1] - a[1]) < 0.5 || inCover([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]))
123
123
  continue;
124
124
  const h = lift + ring.height[0];
125
- const w = wall(`${ring.id}-front-${i}`, "fascia", a[0], a[1], b[0], b[1], 0, h, `<rect class="u-fascia" width="${n(Math.hypot(b[0] - a[0], b[1] - a[1]))}" height="${n(h)}"/>`);
125
+ const w = wall(`${ring.id}-front-${si === 0 ? "" : `s${si}-`}${i}`, "fascia", a[0], a[1], b[0], b[1], 0, h, `<rect class="u-fascia" width="${n(Math.hypot(b[0] - a[0], b[1] - a[1]))}" height="${n(h)}"/>`);
126
126
  seatPlanes.push(shy(w, centre));
127
127
  }
128
- }
128
+ });
129
129
  }
130
130
  }
131
131
  // The walks between the tiers: a plane from the outer edge of one tier up to the first row of the next.
@@ -15,6 +15,8 @@ export declare function realBuilding(real: RealShell, top: StandRing | undefined
15
15
  walls: boolean;
16
16
  roof: boolean;
17
17
  }): Items;
18
+ /** How far a canopy's low edge, the field's side of it, stands under its high edge: the OpenStreetMap roof's `roof:height` is the rise, and the canopy rises to the back of the stands (ours: the sources do not say which way it slopes). */
19
+ export declare const CANOPY_NOTE = "low over the field, high over the back of the stands";
18
20
  /** The pod in the middle of a cable roof's opening, as a share of the opening's size, and how thick it is, in feet (ours: the article says the fabric gathers into a pod above the video board, not how large it is). */
19
21
  export declare const POD: {
20
22
  readonly share: 0.24;
@@ -1,5 +1,6 @@
1
1
  // The building of a real venue, raised: its walls, standing on the outline OpenStreetMap gives, and its roof over the top tier, a lid that rises towards the middle, a saddle that rises at the two ends and falls at the two
2
- // sides, or a cable-supported roof that rises to an opening over the surface which a retractable fabric closes (BC Place's, with the fabric gathered into its pod when `roofOpen` is asked). All are asked for with the roof
2
+ // sides, a cable-supported roof that rises to an opening over the surface which a retractable fabric closes (BC Place's, with the fabric gathered into its pod when `roofOpen` is asked), canopies over some of the
3
+ // stands that leave the field open (Lumen Field's), or retractable panels high over a ballpark (T-Mobile Park's, resting over its east side when `roofOpen` is asked). All are asked for with the roof
3
4
  // (`venue: { roof: true }`) and give way to the eye as the other walls do. Pure: data in, planes out.
4
5
  import { rayAt } from "./crowd.js";
5
6
  import { n, shy, wall } from "./turn.kit.js";
@@ -70,11 +71,23 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
70
71
  const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
71
72
  if (long < 1)
72
73
  return;
73
- const high = lift + roofAt(real, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
74
+ const high = lift + (real.wall ?? roofAt(real, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]));
74
75
  planes.push({ ...shy(wall(`shell-${i}`, "wall", a[0], a[1], b[0], b[1], 0, high, `<rect class="u-shell" width="${n(long)}" height="${n(high)}"/>`), centre), fadeTo: 0.02 });
75
76
  });
76
77
  if (parts.walls && planes.length > 0)
77
78
  items.push({ id: "shell", kind: "walls", at: centre, height: Math.max(...planes.map((p) => p.o[2])), planes });
79
+ if (parts.roof && real.roof === "canopy" && real.canopies !== undefined) {
80
+ const planes = canopyRoof(real.canopies, c, lift, steps);
81
+ if (planes.length > 0)
82
+ items.push({ id: "roof", kind: "roof", at: centre, height: heightOf(planes), planes });
83
+ return items;
84
+ }
85
+ if (parts.roof && real.roof === "panels" && real.panels !== undefined) {
86
+ const planes = panelsRoof(real.panels, c, lift, steps, open);
87
+ if (planes.length > 0)
88
+ items.push({ id: "roof", kind: "roof", at: centre, height: heightOf(planes), planes });
89
+ return items;
90
+ }
78
91
  const crown = real.roof === "cable" ? real.crown : undefined;
79
92
  if (!parts.roof || (top?.hole == null && crown === undefined))
80
93
  return items;
@@ -105,20 +118,121 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
105
118
  roof.push({ ...piece, shy: [(mid[0] - c[0]) / away, (mid[1] - c[1]) / away] });
106
119
  }
107
120
  if (crown !== undefined)
108
- roof.push(...cablePod(real, crown, inner, c, lift, steps, open));
121
+ roof.push(...cablePod(lift + real.edge + crown.rise, inner, c, steps, open));
109
122
  if (roof.length > 0)
110
- items.push({ id: "roof", kind: "roof", at: centre, height: Math.max(...roof.flatMap((p) => [p.o[2], p.o[2] + p.u[2], p.o[2] + p.v[2], p.o[2] + p.u[2] + p.v[2]])), planes: roof });
123
+ items.push({ id: "roof", kind: "roof", at: centre, height: heightOf(roof), planes: roof });
111
124
  return items;
112
125
  }
126
+ /** The top of a set of planes. */
127
+ const heightOf = (planes) => Math.max(...planes.flatMap((p) => [p.o[2], p.o[2] + p.u[2], p.o[2] + p.v[2], p.o[2] + p.u[2] + p.v[2]]));
128
+ /** The two points where a ray from `from` first enters a polygon and last leaves it (the polygon is a band round the middle, so a ray crosses it once), or `undefined` where it misses. */
129
+ function span(poly, from, angle) {
130
+ const [dx, dy] = [Math.cos(angle), Math.sin(angle)];
131
+ const hits = [];
132
+ poly.forEach(([ax, ay], i) => {
133
+ const [bx, by] = poly[(i + 1) % poly.length];
134
+ const [ex, ey] = [bx - ax, by - ay];
135
+ const den = dx * ey - dy * ex;
136
+ if (Math.abs(den) < 1e-12)
137
+ return;
138
+ const t = ((ax - from[0]) * ey - (ay - from[1]) * ex) / den;
139
+ const u = ((ax - from[0]) * dy - (ay - from[1]) * dx) / den;
140
+ if (t > 1e-6 && u >= 0 && u < 1)
141
+ hits.push(t);
142
+ });
143
+ if (hits.length < 2)
144
+ return undefined;
145
+ const [lo, hi] = [Math.min(...hits), Math.max(...hits)];
146
+ return [[from[0] + dx * lo, from[1] + dy * lo], [from[0] + dx * hi, from[1] + dy * hi]];
147
+ }
148
+ /**
149
+ * A piece of roof between two rays: the quad `[inner a, inner b, outer b, outer a]` at the heights given, as a plane that gives way to the eye. A quad whose edges are not parallel is not flat at those heights, so
150
+ * with `exact` it is two triangles, each a plane through its corners.
151
+ */
152
+ function piece(id, quad, heights, c, cables, exact = false) {
153
+ const [ai, bi, b, a] = quad;
154
+ const [ex, ey] = [b[0] - a[0], b[1] - a[1]];
155
+ const el = Math.hypot(ex, ey);
156
+ if (el < 1e-6)
157
+ return [];
158
+ const make = (name, poly, hs, cable) => {
159
+ const slope = slopeThrough(poly.map((p, i) => [p[0], p[1], hs[i]]));
160
+ const mid = [poly.reduce((s, p) => s + p[0], 0) / poly.length, poly.reduce((s, p) => s + p[1], 0) / poly.length];
161
+ const away = Math.hypot(mid[0] - c[0], mid[1] - c[1]) || 1;
162
+ const paint = (corners) => `<polygon class="u-roof" points="${pointList(corners)}"/>${cable ? `<path class="u-cable" d="M${n(corners[0][0])} ${n(corners[0][1])}L${n(corners[3][0])} ${n(corners[3][1])}"/>` : ""}`;
163
+ const plane = raked(name, "roof", poly, slope, [ex / el, ey / el], paint);
164
+ return plane === undefined ? [] : [{ ...plane, shy: [(mid[0] - c[0]) / away, (mid[1] - c[1]) / away] }];
165
+ };
166
+ if (!exact)
167
+ return make(id, quad, heights, cables);
168
+ return [...make(`${id}a`, [ai, bi, b], [heights[0], heights[1], heights[2]], false), ...make(`${id}b`, [ai, b, a], [heights[0], heights[2], heights[3]], false)];
169
+ }
170
+ /** How far a canopy's low edge, the field's side of it, stands under its high edge: the OpenStreetMap roof's `roof:height` is the rise, and the canopy rises to the back of the stands (ours: the sources do not say which way it slopes). */
171
+ export const CANOPY_NOTE = "low over the field, high over the back of the stands";
172
+ /**
173
+ * Canopies over some of the stands (Lumen Field's two, one each side of the field): each OpenStreetMap roof outline is cut by the rays from the middle of the field into the strips between them, the edge towards
174
+ * the field low and the edge towards the back of the stands `rise` higher.
175
+ */
176
+ function canopyRoof(canopies, c, lift, steps) {
177
+ const planes = [];
178
+ canopies.forEach((canopy, ci) => {
179
+ const spans = Array.from({ length: steps }, (_, k) => span(canopy.ring, c, (2 * Math.PI * k) / steps));
180
+ for (let k = 0; k < steps; k += 1) {
181
+ const [s, t] = [spans[k], spans[(k + 1) % steps]];
182
+ if (s === undefined || t === undefined)
183
+ continue;
184
+ const [lo, hi] = [lift + canopy.low, lift + canopy.low + canopy.rise];
185
+ planes.push(...piece(`roof-c${ci}-${k}`, [s[0], t[0], t[1], s[1]], [lo, lo, hi, hi], c, false, true));
186
+ }
187
+ });
188
+ return planes;
189
+ }
190
+ /** The share of a retractable roof's cover that the closed roof's flat crown takes (the sections meet in the middle at the crown's height). */
191
+ const PANEL_CROWN = 0.3;
192
+ /** How thick a parked section is, in feet (ours). */
193
+ const PANEL_THICK = 7;
194
+ /**
195
+ * A ballpark's retractable roof: closed, a band from the rim of its cover up to a crown over the middle with the fabric laid over the crown (as the cable roof's); open, only the sections resting over the side of the
196
+ * building they park on, as slabs at the rim's height.
197
+ */
198
+ function panelsRoof(panels, c, lift, steps, open) {
199
+ const planes = [];
200
+ if (!open) {
201
+ const rim = Array.from({ length: steps }, (_, k) => firstHit(panels.cover, c, (2 * Math.PI * k) / steps) ?? c);
202
+ const inner = rim.map((p) => [c[0] + (p[0] - c[0]) * PANEL_CROWN, c[1] + (p[1] - c[1]) * PANEL_CROWN]);
203
+ for (let k = 0; k < steps; k += 1) {
204
+ const [a, b] = [rim[k], rim[(k + 1) % steps]];
205
+ const [ai, bi] = [inner[k], inner[(k + 1) % steps]];
206
+ const [lo, hi] = [lift + panels.bottom, lift + panels.top];
207
+ planes.push(...piece(`roof-${k}`, [ai, bi, b, a], [hi, hi, lo, lo], c, false, true));
208
+ }
209
+ // The crown itself: a flat cap over the middle, in the fabric's drawing.
210
+ planes.push(...cablePod(lift + panels.top, inner, c, steps, false));
211
+ return planes;
212
+ }
213
+ // Open: the parked sections as slabs.
214
+ const z = lift + panels.bottom;
215
+ const top = z + PANEL_THICK;
216
+ const slab = raked("parked-top", "roof", panels.parked, { x0: panels.parked[0][0], y0: panels.parked[0][1], z0: top, gx: 0, gy: 0 }, [1, 0], (corners) => `<polygon class="u-pod" points="${pointList(corners)}"/>`);
217
+ if (slab !== undefined)
218
+ planes.push({ ...slab, id: "parked-top" });
219
+ panels.parked.forEach((a, i) => {
220
+ const b = panels.parked[(i + 1) % panels.parked.length];
221
+ const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
222
+ if (long < 0.5)
223
+ return;
224
+ planes.push({ ...shy(wall(`parked-${i}`, "roof", a[0], a[1], b[0], b[1], z, top, `<rect class="u-pod" width="${n(long)}" height="${n(PANEL_THICK)}"/>`), c), fadeTo: 0.3 });
225
+ });
226
+ return planes;
227
+ }
113
228
  /** The pod in the middle of a cable roof's opening, as a share of the opening's size, and how thick it is, in feet (ours: the article says the fabric gathers into a pod above the video board, not how large it is). */
114
229
  export const POD = { share: 0.24, thick: 9 };
115
230
  /**
116
231
  * What lies over the opening of a cable roof: the pod in the middle (a box hung at the crown's height) and, with the roof closed, the fabric between the pod and the opening's edge, drawn thin so that the seats
117
232
  * show through it. Open, only the pod is there: the fabric has gathered into it.
118
233
  */
119
- function cablePod(real, crown, edge, c, lift, steps, open) {
234
+ function cablePod(z, edge, c, steps, open) {
120
235
  const planes = [];
121
- const z = lift + real.edge + crown.rise;
122
236
  const pod = edge.map((p) => [c[0] + (p[0] - c[0]) * POD.share, c[1] + (p[1] - c[1]) * POD.share]);
123
237
  if (!open) {
124
238
  for (let k = 0; k < steps; k += 1) {
package/dist/venues.d.ts CHANGED
@@ -28,8 +28,8 @@ export interface RealVenueData {
28
28
  /** The seats for ice hockey: Wikidata's where its item gives a figure for ice hockey (`wikidata`), else the article's (`wikipedia`). */
29
29
  hockey: number;
30
30
  source: "wikidata" | "wikipedia";
31
- /** Wikidata's own capacity (P1083), kept beside it: it is often for another event, or an older year. */
32
- wikidata: number;
31
+ /** Wikidata's own capacity (P1083), kept beside it: it is often for another event, or an older year; `null` where the item gives none (the Little Caesars Arena's, the UBS Arena's). */
32
+ wikidata: number | null;
33
33
  /** What was read, in a few words of ours. */
34
34
  read: string;
35
35
  };
@@ -143,4 +143,148 @@ export const VENUE_DATA = [
143
143
  "tiers": { "count": 2, "basis": "documented", "lowerShare": 0.579, "read": "the article says the lower bowl holds 8,812 and that premium seats were added to the upper level: two seating levels, the lower with 8,812 of the 15,225" },
144
144
  "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
145
145
  },
146
+ {
147
+ "id": "td-garden",
148
+ "wikidata": "Q432105",
149
+ "division": "atlantic",
150
+ "en": { "title": "TD Garden", "revision": 1372238200 },
151
+ "seats": { "hockey": 17850, "source": "wikipedia", "wikidata": 18624, "read": "the infobox's present figure for ice hockey (17,850, from 2019), which is the last of three it lists; Wikidata's one capacity (18,624) is the basketball one of 1995" },
152
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn" },
153
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
154
+ },
155
+ {
156
+ "id": "keybank-center",
157
+ "wikidata": "Q1110789",
158
+ "division": "atlantic",
159
+ "en": { "title": "KeyBank Center", "revision": 1381057909 },
160
+ "seats": { "hockey": 19070, "source": "wikipedia", "wikidata": 19070, "read": "the infobox's present figure for ice hockey and lacrosse (19,070, from 2013), which the article's first sentence gives too; Wikidata's one capacity (19,070) has no sport and is the same" },
161
+ "tiers": { "count": 3, "basis": "documented", "read": "the article speaks of seats in the 200 level and the 300 level (95 were added behind the last row of the 300 level, 380 mainly as a row in the 200 level) and of the second bowl: three seating levels, 100, 200 and 300" },
162
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise; the article says the main scoreboard and roof were replaced in 2024" },
163
+ },
164
+ {
165
+ "id": "little-caesars-arena",
166
+ "wikidata": "Q18154984",
167
+ "division": "atlantic",
168
+ "en": { "title": "Little Caesars Arena", "revision": 1380799342 },
169
+ "seats": { "hockey": 19515, "source": "wikipedia", "wikidata": null, "read": "the article's figure for ice hockey, 19,515, which the infobox gives too; Wikidata gives no capacity for the item" },
170
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article says the arena is built as a bowl with its floor 40 feet below street level and a gondola seating level suspended above the stands, which is not a bowl" },
171
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise; the glass roof the article speaks of is over the concourse between the arena and its neighbours" },
172
+ },
173
+ {
174
+ "id": "amerant-bank-arena",
175
+ "wikidata": "Q795526",
176
+ "division": "atlantic",
177
+ "en": { "title": "Amerant Bank Arena", "revision": 1379821144 },
178
+ "seats": { "hockey": 19250, "source": "wikipedia", "wikidata": 20737, "read": "the infobox's figure for ice hockey, 19,250 (at least 20,000 with standing room); Wikidata's one capacity (20,737) has no sport and is the basketball one" },
179
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article speaks of 70 suites and 2,623 club seats, which are not seating bowls" },
180
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
181
+ },
182
+ {
183
+ "id": "bell-centre",
184
+ "wikidata": "Q522282",
185
+ "division": "atlantic",
186
+ "en": { "title": "Bell Centre", "revision": 1380110375 },
187
+ "seats": { "hockey": 20962, "source": "wikipedia", "wikidata": 21273, "read": "the article's seating capacity for hockey, 20,962 (the infobox's present figure, from 2025); Wikidata's one capacity (21,273) is the first figure of 1996" },
188
+ "tiers": { "count": 3, "basis": "documented", "read": "the article says the Bell Centre is arranged in a three-tier layout: the lower 100 section, the 200 section between two levels of boxes, and the upper 300 section" },
189
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
190
+ },
191
+ {
192
+ "id": "canadian-tire-centre",
193
+ "wikidata": "Q1327290",
194
+ "division": "atlantic",
195
+ "en": { "title": "Canadian Tire Centre", "revision": 1378044395 },
196
+ "seats": { "hockey": 17010, "source": "wikipedia", "wikidata": 19153, "read": "the infobox's present figure for ice hockey (17,010, from 2026, after the 300 level's upper seats gave way to a standing deck); Wikidata's one capacity (19,153) is the figure of 2005" },
197
+ "tiers": { "count": 3, "basis": "documented", "read": "the article says the arena seating is in three levels, 100, 200 and 300, fixed sections round the floor, with suites in the 100 and 200 levels and a mezzanine level above the 300 level that holds no seating" },
198
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
199
+ },
200
+ {
201
+ "id": "benchmark-international-arena",
202
+ "wikidata": "Q940199",
203
+ "division": "atlantic",
204
+ "en": { "title": "Benchmark International Arena", "revision": 1376541285 },
205
+ "seats": { "hockey": 19092, "source": "wikipedia", "wikidata": 19204, "read": "the infobox's figure for ice hockey, 19,092; Wikidata's one capacity (19,204) has no sport" },
206
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn" },
207
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise; OpenStreetMap maps the building 43.47 metres high" },
208
+ },
209
+ {
210
+ "id": "scotiabank-arena",
211
+ "wikidata": "Q406972",
212
+ "division": "atlantic",
213
+ "en": { "title": "Scotiabank Arena", "revision": 1380381634 },
214
+ "seats": { "hockey": 18819, "source": "wikipedia", "wikidata": 19800, "read": "the infobox's figure for ice hockey, 18,819 (20,270 with standing room); Wikidata's one capacity (19,800) is the basketball one" },
215
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article speaks of a 100-level concourse" },
216
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
217
+ },
218
+ {
219
+ "id": "lenovo-center",
220
+ "wikidata": "Q39177",
221
+ "division": "metropolitan",
222
+ "en": { "title": "Lenovo Center", "revision": 1380055594 },
223
+ "seats": { "hockey": 18547, "source": "wikipedia", "wikidata": 19722, "read": "the article's seating for ice hockey, 18,547 (the infobox's present figure, from 2026); Wikidata's one capacity (19,722) has no sport and is nearer the basketball one" },
224
+ "tiers": { "count": 2, "basis": "documented", "read": "the article speaks of the arena's lower bowl and its upper bowl (the seating of the upper bowl replaced in 2018), with 61 suites, 13 luxury boxes and a club level between them: two seating tiers" },
225
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise; the article says $7.8 million was budgeted for a new roof in 2022" },
226
+ },
227
+ {
228
+ "id": "nationwide-arena",
229
+ "wikidata": "Q166503",
230
+ "division": "metropolitan",
231
+ "en": { "title": "Nationwide Arena", "revision": 1379010656 },
232
+ "seats": { "hockey": 18500, "source": "wikipedia", "wikidata": 18144, "read": "the article's seating capacity for hockey, 18,500, which the infobox gives too (19,500 with standing room); Wikidata's one capacity (18,144) has no sport" },
233
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; OpenStreetMap maps five building levels, which are floors, not seating bowls" },
234
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
235
+ },
236
+ {
237
+ "id": "prudential-center",
238
+ "wikidata": "Q849027",
239
+ "division": "metropolitan",
240
+ "en": { "title": "Prudential Center", "revision": 1380112777 },
241
+ "seats": { "hockey": 16514, "source": "wikipedia", "wikidata": 17625, "read": "the infobox's present figure for ice hockey (16,514, from 2015); Wikidata's one capacity (17,625) is the first figure of 2007" },
242
+ "tiers": { "count": 2, "basis": "documented", "read": "the article says the arena has separate concourses for its lower and upper levels (and the lower bowl's middle sections hold 2,330 club seats) with suite levels between: two seating tiers" },
243
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise; the article speaks of a high roof of steel" },
244
+ },
245
+ {
246
+ "id": "ubs-arena",
247
+ "wikidata": "Q48742753",
248
+ "division": "metropolitan",
249
+ "en": { "title": "UBS Arena", "revision": 1374271562 },
250
+ "seats": { "hockey": 17255, "source": "wikipedia", "wikidata": null, "read": "the infobox's figure for ice hockey, 17,255; the article's first lines say it officially seats 17,250; Wikidata gives no capacity for the item" },
251
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn" },
252
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
253
+ },
254
+ {
255
+ "id": "madison-square-garden",
256
+ "wikidata": "Q186125",
257
+ "division": "metropolitan",
258
+ "en": { "title": "Madison Square Garden", "revision": 1380115046 },
259
+ "seats": { "hockey": 18006, "source": "wikipedia", "wikidata": 18006, "read": "the infobox's figure for ice hockey, 18,006; Wikidata lists six capacities without a sport, 18,006 among them" },
260
+ "tiers": { "count": 2, "basis": "documented", "read": "the article tells of the lower bowl (rebuilt in 2011) and the upper bowl (built in 2012 on top of the suites), each with a concourse of its own: two seating tiers, the suites between" },
261
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
262
+ },
263
+ {
264
+ "id": "xfinity-mobile-arena",
265
+ "wikidata": "Q818735",
266
+ "division": "metropolitan",
267
+ "en": { "title": "Xfinity Mobile Arena", "revision": 1380172806 },
268
+ "seats": { "hockey": 19173, "source": "wikipedia", "wikidata": 20328, "read": "the infobox's figure for ice hockey and lacrosse, 19,173 (20,000 with standing room); Wikidata's one capacity (20,328) has no sport and is nearer the basketball one" },
269
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article speaks of a suite level and a mezzanine level" },
270
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
271
+ },
272
+ {
273
+ "id": "ppg-paints-arena",
274
+ "wikidata": "Q595460",
275
+ "division": "metropolitan",
276
+ "en": { "title": "PPG Paints Arena", "revision": 1378458359 },
277
+ "seats": { "hockey": 18187, "source": "wikipedia", "wikidata": 18387, "read": "the infobox's present figure for ice hockey (18,187, from 2023); Wikidata's one capacity (18,387) is the figure of 2011 to 2023" },
278
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn" },
279
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
280
+ },
281
+ {
282
+ "id": "capital-one-arena",
283
+ "wikidata": "Q223240",
284
+ "division": "metropolitan",
285
+ "en": { "title": "Capital One Arena", "revision": 1375927841 },
286
+ "seats": { "hockey": 18573, "source": "wikipedia", "wikidata": 20674, "read": "the infobox's figure for ice hockey, 18,573; Wikidata's one capacity (20,674) has no sport and is nearer the basketball one (20,356)" },
287
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article says the seats are built vertically rather than out, with limited leg room in the upper levels" },
288
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
289
+ },
146
290
  ];