@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.
- package/CHANGELOG.md +38 -0
- package/NOTICE.md +21 -3
- package/README.md +24 -4
- package/dist/crowd.d.ts +20 -2
- package/dist/crowd.park.d.ts +6 -0
- package/dist/crowd.park.js +3 -3
- package/dist/district.d.ts +2 -2
- package/dist/district.js +13 -12
- package/dist/draw.js +25 -10
- package/dist/flythrough-entry.d.ts +1 -0
- package/dist/flythrough-entry.js +1 -0
- package/dist/flythrough.dom.js +52 -6
- package/dist/flythrough.game.d.ts +36 -0
- package/dist/flythrough.game.js +145 -0
- package/dist/flythrough.markup.js +1 -0
- package/dist/flythrough.path.d.ts +6 -4
- package/dist/flythrough.path.js +26 -22
- package/dist/flythrough.style.d.ts +1 -1
- package/dist/flythrough.style.js +3 -0
- package/dist/flythrough.words.d.ts +2 -0
- package/dist/flythrough.words.js +6 -2
- package/dist/stadiums-entry.d.ts +2 -2
- package/dist/stadiums-entry.js +2 -2
- package/dist/stadiums.d.ts +109 -28
- package/dist/stadiums.data.js +25 -0
- package/dist/stadiums.js +159 -30
- package/dist/stadiums.osm.data.js +114 -0
- package/dist/strings.d.ts +4 -0
- package/dist/strings.js +2 -2
- package/dist/venue.js +3 -3
- package/dist/venue.real.d.ts +2 -0
- package/dist/venue.real.js +120 -6
- package/dist/venues.d.ts +2 -2
- package/dist/venues.data.js +144 -0
- package/dist/venues.osm.data.js +662 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/stadiums.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { type Box, type Pt, type RealBowl } from "./crowd.ts";
|
|
1
|
+
import { type Box, type CrowdStands, type Pt, type RealBowl } from "./crowd.ts";
|
|
2
|
+
import { type ParkInput } from "./crowd.park.ts";
|
|
2
3
|
import { type Ground } from "./grounds.ts";
|
|
3
4
|
import { VENUE_ATTRIBUTION } from "./venues.ts";
|
|
4
5
|
export { VENUE_ATTRIBUTION };
|
|
5
6
|
/** How a fact is known: `documented` is what a source cited with it says; `plausible` is ours, for a bowl no source describes. */
|
|
6
7
|
export type StadiumBasis = "documented" | "plausible";
|
|
7
|
-
/** The configurations of a stadium: the field the seats are counted for. `cfl`
|
|
8
|
-
export type StadiumConfigId = "cfl" | "soccer";
|
|
9
|
-
/**
|
|
10
|
-
export type
|
|
8
|
+
/** The configurations of a stadium: the field the seats are counted for. `cfl` and `nfl` are the football fields of the two leagues, `soccer` a soccer pitch, `mlb` a baseball field. */
|
|
9
|
+
export type StadiumConfigId = "cfl" | "nfl" | "soccer" | "mlb";
|
|
10
|
+
/** What a stadium is: a `stadium` has a football field and a soccer pitch lying along its length, a `park` is a baseball park, whose frame is home plate's. */
|
|
11
|
+
export type StadiumKind = "stadium" | "park";
|
|
12
|
+
/**
|
|
13
|
+
* The kinds of roof: `cable` is a cable-supported roof with an opening over the field that a retractable fabric closes (BC Place's); `fixed` a lid over the bowl; `canopy` roofs over some of the stands that leave the field
|
|
14
|
+
* open (Lumen Field's); `panels` a retractable roof of sections high over a ballpark (T-Mobile Park's).
|
|
15
|
+
*/
|
|
16
|
+
export type StadiumRoof = "cable" | "fixed" | "canopy" | "panels";
|
|
11
17
|
/** What one configuration of a stadium is: its sport and its seats, and where the figure was read. */
|
|
12
18
|
export interface StadiumConfigData {
|
|
13
|
-
sport:
|
|
19
|
+
sport: StadiumConfigId;
|
|
14
20
|
/** The seats for this configuration: the infobox's figure for it, or Wikidata's. */
|
|
15
21
|
seats: number;
|
|
16
22
|
source: "wikidata" | "wikipedia";
|
|
@@ -19,45 +25,68 @@ export interface StadiumConfigData {
|
|
|
19
25
|
}
|
|
20
26
|
/** The facts of a stadium: the seats of each configuration, the number of bowls and the kind of roof, each with what a source says. No text of an article is kept. */
|
|
21
27
|
export interface RealStadiumData {
|
|
22
|
-
/** The key: `bc-place`, the key of the stadium's ground in `GROUNDS
|
|
28
|
+
/** The key: `bc-place`, the key of the stadium's ground in `GROUNDS` (or of its park in `PARKS`). */
|
|
23
29
|
id: string;
|
|
30
|
+
kind?: StadiumKind;
|
|
24
31
|
/** The Wikidata item of the stadium. */
|
|
25
32
|
wikidata: string;
|
|
26
|
-
/** The key of the stadium's ground in `GROUNDS` (its name, its place, its pitch). */
|
|
27
|
-
ground
|
|
33
|
+
/** The key of the stadium's ground in `GROUNDS` (its name, its place, its pitch): a stadium has one. */
|
|
34
|
+
ground?: string;
|
|
35
|
+
/** The key of a ballpark in `PARKS` (its name, its place, its fence): a park has one. */
|
|
36
|
+
park?: string;
|
|
37
|
+
/** Whose football field the first row stands round: the CFL's (the default) or the NFL's. */
|
|
38
|
+
gridiron?: "cfl" | "nfl";
|
|
28
39
|
/** The English Wikipedia article the facts that are not Wikidata's were read from, at a revision. */
|
|
29
40
|
en: {
|
|
30
41
|
title: string;
|
|
31
42
|
revision: number;
|
|
32
43
|
};
|
|
33
|
-
configs: Record<StadiumConfigId, StadiumConfigData
|
|
44
|
+
configs: Partial<Record<StadiumConfigId, StadiumConfigData>>;
|
|
45
|
+
/** The seats of the whole bowl where it holds more than a configuration's (Lumen Field's soccer seats are fewer: the upper sections are closed): the bowl is fitted to these and a configuration opens its seats from the lowest bowl up. */
|
|
46
|
+
bowlSeats?: number;
|
|
47
|
+
/** Wikidata's own capacities (P1083), kept beside the figures used: they are often for another event or another year. */
|
|
48
|
+
wikidataSeats?: number[];
|
|
34
49
|
tiers: {
|
|
35
|
-
/** The seating levels (bowls). */
|
|
36
|
-
count: 1 | 2 | 3;
|
|
50
|
+
/** The seating levels (bowls): a ballpark's four levels are drawn as a lower bowl, an upper bowl and the bleachers. */
|
|
51
|
+
count: 1 | 2 | 3 | 4;
|
|
37
52
|
basis: StadiumBasis;
|
|
38
53
|
/** The seats of the lowest bowl, where a source gives them: the lower tier's band holds this many whatever the configuration (the upper bowl is what is opened or closed for the others). */
|
|
39
54
|
lowerSeats?: number;
|
|
55
|
+
/** A ballpark's seats by band, where the article gives them by level: the lower bowl, the upper bowl (the club, suite and view levels) and the bleachers. */
|
|
56
|
+
tierSeats?: {
|
|
57
|
+
lower: number;
|
|
58
|
+
upper: number;
|
|
59
|
+
bleachers: number;
|
|
60
|
+
};
|
|
40
61
|
read: string;
|
|
41
62
|
};
|
|
42
63
|
roof: {
|
|
43
64
|
kind: StadiumRoof;
|
|
44
65
|
basis: StadiumBasis;
|
|
45
|
-
read: string;
|
|
66
|
+
read: string;
|
|
67
|
+
/** A cable roof's opening over the field, as the article gives it, in metres. */
|
|
46
68
|
opening?: {
|
|
47
69
|
length: number;
|
|
48
70
|
width: number;
|
|
49
71
|
unit: "m";
|
|
50
72
|
};
|
|
73
|
+
/** A canopy's share of the seats it covers, as the article gives it. */
|
|
74
|
+
cover?: number;
|
|
75
|
+
/** A retractable roof's bottom and top above the field, in feet, as the article gives them. */
|
|
76
|
+
bottom?: number;
|
|
77
|
+
top?: number;
|
|
51
78
|
};
|
|
52
79
|
}
|
|
53
80
|
/** What OpenStreetMap gave of a stadium: ODbL 1.0 data, "(c) OpenStreetMap contributors". */
|
|
54
81
|
export interface StadiumOsm {
|
|
55
|
-
/** The element the outline is: `way/24705904`. */
|
|
82
|
+
/** The element the outline is: `way/24705904`, `relation/2305831`. */
|
|
56
83
|
source: string;
|
|
57
84
|
read: string;
|
|
58
85
|
base: string;
|
|
59
86
|
licence: "ODbL-1.0";
|
|
60
87
|
attribution: string;
|
|
88
|
+
/** The building's height in metres, where OpenStreetMap maps one. */
|
|
89
|
+
height?: number;
|
|
61
90
|
/** The roof OpenStreetMap maps over the building: its heights in metres (`height` to the top of the roof, `rise` of that the roof's own) and the opening in it as `[longitude, latitude]`. */
|
|
62
91
|
roof?: {
|
|
63
92
|
source: string;
|
|
@@ -69,16 +98,24 @@ export interface StadiumOsm {
|
|
|
69
98
|
ring: [number, number][];
|
|
70
99
|
};
|
|
71
100
|
};
|
|
101
|
+
/** The canopies OpenStreetMap maps over some of the stands: each one's outline, the height of its top and how far it rises (metres). */
|
|
102
|
+
canopies?: {
|
|
103
|
+
source: string;
|
|
104
|
+
height: number;
|
|
105
|
+
rise: number;
|
|
106
|
+
ring: [number, number][];
|
|
107
|
+
}[];
|
|
72
108
|
/** The building's outline as `[longitude, latitude]`, not repeating its first point. */
|
|
73
109
|
ring: [number, number][];
|
|
74
110
|
/**
|
|
75
|
-
* How the field lies, worked out when the data was made: the heading of its long side (degrees clockwise from north, 0 to under 180
|
|
76
|
-
*
|
|
77
|
-
*
|
|
111
|
+
* How the field lies, worked out when the data was made: the heading of its long side (degrees clockwise from north, 0 to under 180; for a ballpark, the heading from home plate out to centre field, 0 to under 360), how that
|
|
112
|
+
* is known (`opening`: from the roof's opening, which the article says is the field's size; `pitch`: from the pitches OpenStreetMap maps; `infield`: from the mapped infield, with the outline choosing which way is out to
|
|
113
|
+
* centre field; `building`: from the long axis of the building's main hall, an inference; `none`: no direction, 90 is stood in), how much of a direction the building has (`strength`) and how much longer than wide the
|
|
114
|
+
* opening, the pitch or the hall is (`elongation`), and the middle of the field as `[longitude, latitude]` (home plate for a ballpark).
|
|
78
115
|
*/
|
|
79
116
|
axis: {
|
|
80
117
|
heading: number;
|
|
81
|
-
basis: "opening" | "building" | "none";
|
|
118
|
+
basis: "opening" | "pitch" | "infield" | "building" | "none";
|
|
82
119
|
strength: number;
|
|
83
120
|
elongation: number;
|
|
84
121
|
centre: [number, number];
|
|
@@ -88,10 +125,10 @@ export interface StadiumOsm {
|
|
|
88
125
|
export interface RealStadium extends RealStadiumData {
|
|
89
126
|
osm: StadiumOsm;
|
|
90
127
|
}
|
|
91
|
-
/** How the field lies in the building: its long side's heading, how that is known, and the way north points on the page (degrees clockwise from the top). */
|
|
128
|
+
/** How the field lies in the building: its long side's heading (a ballpark's: home plate to centre field), how that is known, and the way north points on the page (degrees clockwise from the top). */
|
|
92
129
|
export interface StadiumOrientation {
|
|
93
130
|
heading: number;
|
|
94
|
-
basis: "
|
|
131
|
+
basis: StadiumOsm["axis"]["basis"];
|
|
95
132
|
north: number;
|
|
96
133
|
}
|
|
97
134
|
/** The stadiums by key. */
|
|
@@ -102,13 +139,24 @@ export declare const STADIUM_IDS: readonly string[];
|
|
|
102
139
|
export declare const stadiumById: (id: string | undefined) => RealStadium | undefined;
|
|
103
140
|
/** The stadium whose Wikidata item is `Q612227`, or `undefined`. */
|
|
104
141
|
export declare const stadiumByWikidata: (wikidata: string | undefined) => RealStadium | undefined;
|
|
105
|
-
/** The
|
|
142
|
+
/** The kind of a stadium: a `stadium` unless it is a ballpark. */
|
|
143
|
+
export declare const stadiumKind: (stadium: RealStadium) => StadiumKind;
|
|
144
|
+
/** The configurations a stadium has, in the order of its data. */
|
|
145
|
+
export declare const stadiumConfigs: (stadium: RealStadium) => StadiumConfigId[];
|
|
146
|
+
/** The ground a stadium is (its name, place and pitch). Throws for a ballpark, which has a park. */
|
|
106
147
|
export declare const groundOfStadium: (stadium: RealStadium) => Ground;
|
|
148
|
+
/** The names a stadium goes by in both languages, of its ground or its park. */
|
|
149
|
+
export declare function stadiumNames(stadium: RealStadium): {
|
|
150
|
+
en: string;
|
|
151
|
+
ja: string;
|
|
152
|
+
};
|
|
153
|
+
/** The heading the field's frame runs along, degrees clockwise from north: the long side's, and for a ballpark (whose frame has home plate at its origin, x to the right of the way out and y minus the way out) a quarter turn from the way out to centre field. */
|
|
154
|
+
export declare const stadiumFrameHeading: (stadium: RealStadium) => number;
|
|
107
155
|
/** How the field lies in a stadium's building (see `StadiumOsm.axis`). */
|
|
108
156
|
export declare const stadiumOrientation: (stadium: RealStadium) => StadiumOrientation;
|
|
109
|
-
/** The configuration a sport is: `cfl` or `
|
|
157
|
+
/** The configuration a sport is: `cfl`, `nfl`, `soccer` or `mlb`, or `undefined` for a sport a stadium has no configuration for. */
|
|
110
158
|
export declare const stadiumConfigOf: (sport: string | undefined) => StadiumConfigId | undefined;
|
|
111
|
-
/** The seats of a stadium for a configuration. */
|
|
159
|
+
/** The seats of a stadium for a configuration (0 for one it has none of). */
|
|
112
160
|
export declare const stadiumSeats: (stadium: RealStadium, config: StadiumConfigId) => number;
|
|
113
161
|
/** The height of the first row above the field, in feet, the rise of a bowl's rows by the number of bowls (what the lowest, a middle and the upper rise, at most), and the rise of the walk between two bowls (a suite level): ours, no source gives them. */
|
|
114
162
|
export declare const STADIUM_FIRST_HEIGHT = 5;
|
|
@@ -116,6 +164,8 @@ export declare const STADIUM_RISES: Readonly<Record<1 | 2 | 3, readonly number[]
|
|
|
116
164
|
export declare const STADIUM_WALK_RISE = 24;
|
|
117
165
|
/** The height a roof's rim stands over the top row, at the least, in feet. */
|
|
118
166
|
export declare const STADIUM_CLEARANCE = 22;
|
|
167
|
+
/** How far a canopy's low edge stands over the top row at the least, in feet: a bowl under a canopy is lowered to keep it. */
|
|
168
|
+
export declare const CANOPY_CLEARANCE = 6;
|
|
119
169
|
/** The plan of a stadium for a configuration: its building in the drawing's frame, the bowl fitted inside, and how it lies. */
|
|
120
170
|
export interface StadiumPlan {
|
|
121
171
|
stadium: RealStadium;
|
|
@@ -129,26 +179,28 @@ export interface StadiumPlan {
|
|
|
129
179
|
opening: Pt[] | undefined;
|
|
130
180
|
/** The stands for `layoutOf`: bowls, first row, ground, and the building and roof. */
|
|
131
181
|
bowl: RealBowl;
|
|
132
|
-
/** The seats the bowls hold, by bowl, and in all: equal to the
|
|
182
|
+
/** The seats the bowls hold, by bowl, and in all: equal to the bowl's seats where the building has room (`fit` is `fits`). */
|
|
133
183
|
seats: {
|
|
134
184
|
tier: string;
|
|
135
185
|
seats: number;
|
|
136
186
|
}[];
|
|
137
187
|
total: number;
|
|
188
|
+
/** The seats open in this configuration: the configuration's own, counted from the lowest bowl up (the rest of a larger bowl is closed). */
|
|
189
|
+
open: number;
|
|
138
190
|
/** The floor a seat takes in the plan, in square feet (one of `SEAT_AREAS`). */
|
|
139
191
|
seatArea: number;
|
|
140
192
|
fit: "fits" | "tight";
|
|
141
193
|
}
|
|
142
194
|
/**
|
|
143
195
|
* The plan of a stadium's bowl for a configuration: the building's outline turned so that the field lies along x, a first row round a football field's footprint as a gridiron's stands have it, the largest bowl-shaped
|
|
144
|
-
* outline that fits the building round the middle of the field, and between the two the bowls, the outer edge of each placed so that its band holds the share of the
|
|
145
|
-
* between them. The bowl is the same building for
|
|
196
|
+
* outline that fits the building round the middle of the field, and between the two the bowls, the outer edge of each placed so that its band holds the share of the seats the sources give it, with a walk
|
|
197
|
+
* between them. The bowl is the same building for every configuration (the seats of the configuration differ, and the field); where a bowl holds more seats than a configuration opens, the rest are closed.
|
|
146
198
|
* @example
|
|
147
199
|
* ```ts
|
|
148
200
|
* import { planStadium, stadiumById } from "@johnmorrisdotca/kyogijo-stadiums/stadiums";
|
|
149
201
|
* const plan = planStadium(stadiumById("bc-place")!, "soccer", { x: 0, y: 0, w: 344.5, h: 223.1 });
|
|
150
|
-
* console.log(plan.orientation.basis, plan.seats.map((t) => t.tier).join(" "), plan.total);
|
|
151
|
-
* // opening lower upper 52497
|
|
202
|
+
* console.log(plan.orientation.basis, plan.seats.map((t) => `${t.tier} ${t.seats}`).join(", "), plan.total);
|
|
203
|
+
* // opening lower 27695, upper 24802 52497
|
|
152
204
|
* ```
|
|
153
205
|
*/
|
|
154
206
|
export declare function planStadium(stadium: RealStadium, config: StadiumConfigId, box: Box): StadiumPlan;
|
|
@@ -156,3 +208,32 @@ export declare function planStadium(stadium: RealStadium, config: StadiumConfigI
|
|
|
156
208
|
export declare const stadiumBowl: (stadium: RealStadium, config: StadiumConfigId, box: Box) => RealBowl;
|
|
157
209
|
/** Whether the bowl lies inside the building: every corner of every bowl's outer edge is inside the footprint. */
|
|
158
210
|
export declare function stadiumBowlInside(plan: StadiumPlan): boolean;
|
|
211
|
+
/** The plan of a ballpark: its usual stands (`parkLayout`), the depth of each band set so that it holds the seats the article gives for its level, and the building, the way home plate faces and the roof round them. */
|
|
212
|
+
export interface ParkPlan {
|
|
213
|
+
stadium: RealStadium;
|
|
214
|
+
config: StadiumConfigId;
|
|
215
|
+
orientation: StadiumOrientation;
|
|
216
|
+
/** The building's outline in feet, in the park's frame (home plate at its origin, x to the right of the way out to centre field, y minus the way out). */
|
|
217
|
+
footprint: Pt[];
|
|
218
|
+
/** The stands, with the building and the roof (`real`). */
|
|
219
|
+
stands: CrowdStands;
|
|
220
|
+
/** The depth of each band of stands the seats gave, in feet. */
|
|
221
|
+
depths: {
|
|
222
|
+
lower: number;
|
|
223
|
+
upper: number;
|
|
224
|
+
bleachers: number;
|
|
225
|
+
};
|
|
226
|
+
/** The seats each band holds, and in all, against the configuration's. */
|
|
227
|
+
seats: {
|
|
228
|
+
tier: string;
|
|
229
|
+
seats: number;
|
|
230
|
+
}[];
|
|
231
|
+
total: number;
|
|
232
|
+
/** Whether the stands hold the configuration's seats to within two percent. */
|
|
233
|
+
fit: "fits" | "tight";
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The plan of a ballpark from what its stands are laid from (`input`: the park's fence, the backstop and the crowd): the usual stands with each band as deep as its seats need, the building's outline turned so that home
|
|
237
|
+
* plate faces where OpenStreetMap's infield and the outline say, and the roof: sections far over the field, closed over the stands and the field or resting over the east side where the article says they rest.
|
|
238
|
+
*/
|
|
239
|
+
export declare function planPark(stadium: RealStadium, input: ParkInput): ParkPlan;
|
package/dist/stadiums.data.js
CHANGED
|
@@ -5,7 +5,32 @@ export const STADIUM_DATA = [
|
|
|
5
5
|
"ground": "bc-place",
|
|
6
6
|
"en": { "title": "BC Place", "revision": 1372065649 },
|
|
7
7
|
"configs": { "cfl": { "sport": "cfl", "seats": 54500, "source": "wikipedia", "read": "the infobox's first capacity, 54,500, which Wikidata's capacity (P1083) also gives; the article's stand for football of the CFL's club" }, "soccer": { "sport": "soccer", "seats": 52497, "source": "wikipedia", "read": "the infobox's capacity for the 2026 world cup of soccer, 52,497, 2,003 fewer than its first" } },
|
|
8
|
+
"wikidataSeats": [54500],
|
|
8
9
|
"tiers": { "count": 2, "basis": "documented", "lowerSeats": 27695, "read": "the infobox's lower bowl of 27,695 seats, and the article's lower and upper bowls (the club that plays soccer there sold only the lower bowl)" },
|
|
9
10
|
"roof": { "kind": "cable", "basis": "documented", "read": "the article: a retractable roof supported by cables, the largest of its kind, its opening 100 by 85 metres (the size of the field), the fabric retracting into a pod in the middle of the opening above the suspended video board", "opening": { "length": 100, "width": 85, "unit": "m" } },
|
|
10
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"id": "lumen-field",
|
|
14
|
+
"kind": "stadium",
|
|
15
|
+
"wikidata": "Q612736",
|
|
16
|
+
"ground": "lumen-field",
|
|
17
|
+
"gridiron": "nfl",
|
|
18
|
+
"en": { "title": "Lumen Field", "revision": 1373998493 },
|
|
19
|
+
"configs": { "nfl": { "sport": "nfl", "seats": 68740, "source": "wikipedia", "read": "the infobox's capacity for the NFL, 68,740 (expandable to 72,000 for special events)" }, "soccer": { "sport": "soccer", "seats": 37722, "source": "wikipedia", "read": "the infobox's capacity for MLS, 37,722, the upper and lower sections tarped off beyond it" } },
|
|
20
|
+
"bowlSeats": 68740,
|
|
21
|
+
"wikidataSeats": [67000, 72000],
|
|
22
|
+
"tiers": { "count": 3, "basis": "documented", "read": "the article names the lower concourse levels, the club and suite levels and the Summit levels above them" },
|
|
23
|
+
"roof": { "kind": "canopy", "basis": "documented", "cover": 0.7, "read": "the article: a roof of 200,000 square feet that covers 70 percent of the seats and leaves the field open, spanning 720 feet between concrete pylons at the north and south ends" },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "t-mobile-park",
|
|
27
|
+
"kind": "park",
|
|
28
|
+
"wikidata": "Q1193117",
|
|
29
|
+
"park": "t-mobile-park",
|
|
30
|
+
"en": { "title": "T-Mobile Park", "revision": 1379419810 },
|
|
31
|
+
"configs": { "mlb": { "sport": "mlb", "seats": 47368, "source": "wikipedia", "read": "the infobox's capacity for baseball, 47,368" } },
|
|
32
|
+
"wikidataSeats": [47476],
|
|
33
|
+
"tiers": { "count": 4, "basis": "documented", "tierSeats": { "lower": 20521, "upper": 22075, "bleachers": 3666 }, "read": "the article's levels: the Main Level of 20,521 seats (most of the lower bowl), the Club Level of 4,123, the Suite Level of 2,050 and the View Level of 15,902, and two bleacher sections of 3,666" },
|
|
34
|
+
"roof": { "kind": "panels", "basis": "documented", "bottom": 217, "top": 269, "read": "the article: a retractable roof in four sections that does not fully enclose the ballpark, its bottom 217 feet and its top 269 feet above the field, resting over the railway tracks to the east when open" },
|
|
35
|
+
},
|
|
11
36
|
];
|
package/dist/stadiums.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
// The real stadiums: for a stadium that stages football and soccer, the outline of its building and the way its field lies in it (from OpenStreetMap, ODbL 1.0), and a bowl of the seats of each configuration,
|
|
2
|
-
// number of bowls and the kind of roof the sources give (Wikidata, CC0, and the English Wikipedia article each fact cites), fitted inside the outline. It is the stadium's counterpart of `venues.ts`, for a surface that is a
|
|
1
|
+
// The real stadiums: for a stadium that stages football and soccer, or a ballpark, the outline of its building and the way its field lies in it (from OpenStreetMap, ODbL 1.0), and a bowl of the seats of each configuration,
|
|
2
|
+
// the number of bowls and the kind of roof the sources give (Wikidata, CC0, and the English Wikipedia article each fact cites), fitted inside the outline. It is the stadium's counterpart of `venues.ts`, for a surface that is a
|
|
3
3
|
// field and not a rink: level one is the building, level two a plausible bowl, and it stops there (no team's, ticket seller's or federation's seat map is read or copied, no section or row is named, and no mark of a club, a
|
|
4
|
-
// league or a tournament is drawn). The drawing is `drawKyogijo({ sport: "cfl", stadium, crowd: { real: true } })` (or `sport: "soccer"` with a `ground` or a `stadium`) and, in 3D, the same options
|
|
4
|
+
// league or a tournament is drawn). The drawing is `drawKyogijo({ sport: "cfl", stadium, crowd: { real: true } })` (or `sport: "soccer"` with a `ground` or a `stadium`, `sport: "mlb"` with a `park`) and, in 3D, the same options
|
|
5
|
+
// in `buildTurn`.
|
|
5
6
|
import { NORTH_ROOM, aislesOf, areaOfOutline, outlinePoints } from "./crowd.js";
|
|
7
|
+
import { parkLayout } from "./crowd.park.js";
|
|
6
8
|
import { FEET_PER_METRE, FOOTBALL } from "./geometry.js";
|
|
7
9
|
import { groundById } from "./grounds.js";
|
|
10
|
+
import { PARKS } from "./parks.js";
|
|
8
11
|
import { GAP, HALL, MAX_RAKE, SEAT_AREAS, WALL, VENUE_ATTRIBUTION } from "./venues.js";
|
|
9
12
|
import { boxOf, edgeDistance, inside, metresFrom, middleOf, northOn, turnedTo } from "./venues.geo.js";
|
|
13
|
+
import { clipConvex } from "./venue.geom.js";
|
|
10
14
|
import { STADIUM_DATA } from "./stadiums.data.js";
|
|
11
15
|
import { STADIUM_OSM } from "./stadiums.osm.data.js";
|
|
12
16
|
export { VENUE_ATTRIBUTION };
|
|
@@ -18,30 +22,49 @@ export const STADIUM_IDS = Object.freeze(STADIUM_DATA.map((data) => data.id));
|
|
|
18
22
|
export const stadiumById = (id) => (id !== undefined && Object.prototype.hasOwnProperty.call(STADIUMS, id) ? STADIUMS[id] : undefined);
|
|
19
23
|
/** The stadium whose Wikidata item is `Q612227`, or `undefined`. */
|
|
20
24
|
export const stadiumByWikidata = (wikidata) => Object.values(STADIUMS).find((s) => s.wikidata === wikidata);
|
|
21
|
-
/** The
|
|
25
|
+
/** The kind of a stadium: a `stadium` unless it is a ballpark. */
|
|
26
|
+
export const stadiumKind = (stadium) => stadium.kind ?? "stadium";
|
|
27
|
+
/** The configurations a stadium has, in the order of its data. */
|
|
28
|
+
export const stadiumConfigs = (stadium) => Object.keys(stadium.configs);
|
|
29
|
+
/** The ground a stadium is (its name, place and pitch). Throws for a ballpark, which has a park. */
|
|
22
30
|
export const groundOfStadium = (stadium) => {
|
|
23
31
|
const ground = groundById(stadium.ground);
|
|
24
32
|
if (ground === undefined)
|
|
25
|
-
throw new Error(`${stadium.id} has no ground ${stadium.ground}`);
|
|
33
|
+
throw new Error(`${stadium.id} has no ground ${String(stadium.ground)}`);
|
|
26
34
|
return ground;
|
|
27
35
|
};
|
|
36
|
+
/** The names a stadium goes by in both languages, of its ground or its park. */
|
|
37
|
+
export function stadiumNames(stadium) {
|
|
38
|
+
if (stadiumKind(stadium) === "park") {
|
|
39
|
+
const park = PARKS[stadium.park ?? ""];
|
|
40
|
+
if (park === undefined)
|
|
41
|
+
throw new Error(`${stadium.id} has no park ${String(stadium.park)}`);
|
|
42
|
+
return { en: park.name, ja: park.nameJa };
|
|
43
|
+
}
|
|
44
|
+
const ground = groundOfStadium(stadium);
|
|
45
|
+
return { en: ground.name, ja: ground.nameJa };
|
|
46
|
+
}
|
|
28
47
|
const round = (value, places = 1) => Math.round(value * 10 ** places) / 10 ** places;
|
|
29
48
|
const lonLat = (ring) => ring.map(([lon, lat]) => [lon, lat]);
|
|
49
|
+
/** The heading the field's frame runs along, degrees clockwise from north: the long side's, and for a ballpark (whose frame has home plate at its origin, x to the right of the way out and y minus the way out) a quarter turn from the way out to centre field. */
|
|
50
|
+
export const stadiumFrameHeading = (stadium) => (stadiumKind(stadium) === "park" ? (stadium.osm.axis.heading + 90) % 360 : stadium.osm.axis.heading);
|
|
30
51
|
/** How the field lies in a stadium's building (see `StadiumOsm.axis`). */
|
|
31
|
-
export const stadiumOrientation = (stadium) => ({ heading: stadium.osm.axis.heading, basis: stadium.osm.axis.basis, north: round(northOn(stadium
|
|
32
|
-
/** The configuration a sport is: `cfl` or `
|
|
33
|
-
export const stadiumConfigOf = (sport) => (sport === "cfl" || sport === "soccer" ? sport : undefined);
|
|
34
|
-
/** The seats of a stadium for a configuration. */
|
|
35
|
-
export const stadiumSeats = (stadium, config) => stadium.configs[config]
|
|
52
|
+
export const stadiumOrientation = (stadium) => ({ heading: stadium.osm.axis.heading, basis: stadium.osm.axis.basis, north: round(northOn(stadiumFrameHeading(stadium))) });
|
|
53
|
+
/** The configuration a sport is: `cfl`, `nfl`, `soccer` or `mlb`, or `undefined` for a sport a stadium has no configuration for. */
|
|
54
|
+
export const stadiumConfigOf = (sport) => (sport === "cfl" || sport === "nfl" || sport === "soccer" || sport === "mlb" ? sport : undefined);
|
|
55
|
+
/** The seats of a stadium for a configuration (0 for one it has none of). */
|
|
56
|
+
export const stadiumSeats = (stadium, config) => stadium.configs[config]?.seats ?? 0;
|
|
36
57
|
// ---- The bowl. ----
|
|
37
|
-
/** How far the first row is from a football field's footprint (the
|
|
38
|
-
const FIRST_ROW = { side: 42, end: 28, corner: 60 };
|
|
58
|
+
/** How far the first row is from a football field's footprint (the league's field and its end zones) along a side and at an end, and the corner's radius: the package's own gridiron profile (`crowd.ts`), so that a real stadium's first row is the usual one. */
|
|
59
|
+
const FIRST_ROW = { cfl: { side: 42, end: 28, corner: 60 }, nfl: { side: 40, end: 26, corner: 60 } };
|
|
39
60
|
/** The height of the first row above the field, in feet, the rise of a bowl's rows by the number of bowls (what the lowest, a middle and the upper rise, at most), and the rise of the walk between two bowls (a suite level): ours, no source gives them. */
|
|
40
61
|
export const STADIUM_FIRST_HEIGHT = 5;
|
|
41
62
|
export const STADIUM_RISES = { 1: [60], 2: [38, 62], 3: [32, 26, 56] };
|
|
42
63
|
export const STADIUM_WALK_RISE = 24;
|
|
43
64
|
/** The height a roof's rim stands over the top row, at the least, in feet. */
|
|
44
65
|
export const STADIUM_CLEARANCE = 22;
|
|
66
|
+
/** How far a canopy's low edge stands over the top row at the least, in feet: a bowl under a canopy is lowered to keep it. */
|
|
67
|
+
export const CANOPY_CLEARANCE = 6;
|
|
45
68
|
/** The share of the seats in the lowest bowl where no source says. */
|
|
46
69
|
const LOWER_SHARE = 0.5;
|
|
47
70
|
const CORNER = 0.7;
|
|
@@ -68,17 +91,21 @@ function solveFor(f, lo, hi, target) {
|
|
|
68
91
|
const PLANS = new WeakMap();
|
|
69
92
|
/**
|
|
70
93
|
* The plan of a stadium's bowl for a configuration: the building's outline turned so that the field lies along x, a first row round a football field's footprint as a gridiron's stands have it, the largest bowl-shaped
|
|
71
|
-
* outline that fits the building round the middle of the field, and between the two the bowls, the outer edge of each placed so that its band holds the share of the
|
|
72
|
-
* between them. The bowl is the same building for
|
|
94
|
+
* outline that fits the building round the middle of the field, and between the two the bowls, the outer edge of each placed so that its band holds the share of the seats the sources give it, with a walk
|
|
95
|
+
* between them. The bowl is the same building for every configuration (the seats of the configuration differ, and the field); where a bowl holds more seats than a configuration opens, the rest are closed.
|
|
73
96
|
* @example
|
|
74
97
|
* ```ts
|
|
75
98
|
* import { planStadium, stadiumById } from "@johnmorrisdotca/kyogijo-stadiums/stadiums";
|
|
76
99
|
* const plan = planStadium(stadiumById("bc-place")!, "soccer", { x: 0, y: 0, w: 344.5, h: 223.1 });
|
|
77
|
-
* console.log(plan.orientation.basis, plan.seats.map((t) => t.tier).join(" "), plan.total);
|
|
78
|
-
* // opening lower upper 52497
|
|
100
|
+
* console.log(plan.orientation.basis, plan.seats.map((t) => `${t.tier} ${t.seats}`).join(", "), plan.total);
|
|
101
|
+
* // opening lower 27695, upper 24802 52497
|
|
79
102
|
* ```
|
|
80
103
|
*/
|
|
81
104
|
export function planStadium(stadium, config, box) {
|
|
105
|
+
if (stadiumKind(stadium) !== "stadium")
|
|
106
|
+
throw new Error(`${stadium.id} is a ballpark: planPark plans it`);
|
|
107
|
+
if (stadium.configs[config] === undefined)
|
|
108
|
+
throw new Error(`${stadium.id} has no configuration ${config}`);
|
|
82
109
|
const key = `${config}|${box.x}|${box.y}|${box.w}|${box.h}`;
|
|
83
110
|
const mine = PLANS.get(stadium) ?? new Map();
|
|
84
111
|
PLANS.set(stadium, mine);
|
|
@@ -102,9 +129,11 @@ function plan(stadium, config, rink) {
|
|
|
102
129
|
const openingRing = stadium.osm.roof?.opening?.ring;
|
|
103
130
|
const opening = openingRing === undefined ? undefined : place(turned(openingRing));
|
|
104
131
|
// The first row round a football field's footprint, and the largest bowl-shaped outline that fits inside the building round the middle of the field.
|
|
105
|
-
const
|
|
132
|
+
const kind = stadium.gridiron ?? "cfl";
|
|
133
|
+
const gridiron = FOOTBALL[kind];
|
|
134
|
+
const row = FIRST_ROW[kind];
|
|
106
135
|
const length = gridiron.length + 2 * gridiron.endZone;
|
|
107
|
-
const first = { kind: "rrect", x0: at[0] - length / 2 -
|
|
136
|
+
const first = { kind: "rrect", x0: at[0] - length / 2 - row.end, y0: at[1] - gridiron.width / 2 - row.side, x1: at[0] + length / 2 + row.end, y1: at[1] + gridiron.width / 2 + row.side, r: row.corner };
|
|
108
137
|
const shapedAs = (long, s) => ({ kind: "rrect", x0: at[0] - long * s, y0: at[1] - s, x1: at[0] + long * s, y1: at[1] + s, r: CORNER * s });
|
|
109
138
|
const largest = (long) => {
|
|
110
139
|
let [lo, hi] = [60, Math.max(fb.x1 - fb.x0, fb.y1 - fb.y0)];
|
|
@@ -123,12 +152,15 @@ function plan(stadium, config, rink) {
|
|
|
123
152
|
const outer = { ...wall, x0: Math.min(wall.x0, first.x0 - 40), y0: Math.min(wall.y0, first.y0 - 40), x1: Math.max(wall.x1, first.x1 + 40), y1: Math.max(wall.y1, first.y1 + 40) };
|
|
124
153
|
const depth = (first.x0 - outer.x0) + (first.y0 - outer.y0) + (outer.x1 - first.x1) + (outer.y1 - first.y1);
|
|
125
154
|
const gap = GAP / Math.max(20, depth / 4);
|
|
126
|
-
const
|
|
127
|
-
const
|
|
155
|
+
const open = stadium.configs[config]?.seats ?? 0;
|
|
156
|
+
const seatsTotal = stadium.bowlSeats ?? open;
|
|
157
|
+
const count = Math.min(3, stadium.tiers.count);
|
|
128
158
|
const lower = stadium.tiers.lowerSeats === undefined ? (count === 1 ? 1 : count === 2 ? LOWER_SHARE : 0.36) : stadium.tiers.lowerSeats / seatsTotal;
|
|
129
159
|
const shares = count === 1 ? [1] : count === 2 ? [lower, 1 - lower] : [lower, (1 - lower) * 0.36, (1 - lower) * 0.64];
|
|
130
160
|
const outlineAt = (t) => between(first, outer, t);
|
|
131
|
-
|
|
161
|
+
// A canopy's low edge is the least height the top row may reach (less the clearance): a bowl under a canopy is lowered, by the factor `vscale`, if it would not clear.
|
|
162
|
+
const low = stadium.osm.canopies === undefined ? Infinity : Math.min(...stadium.osm.canopies.map((c) => (c.height - c.rise) * FEET_PER_METRE));
|
|
163
|
+
const build = (area, vscale) => {
|
|
132
164
|
const seatsOf = (region, hole) => Math.round((areaOfOutline(region) - areaOfOutline(hole)) / area);
|
|
133
165
|
const rings = [];
|
|
134
166
|
let t0 = 0;
|
|
@@ -143,8 +175,8 @@ function plan(stadium, config, rink) {
|
|
|
143
175
|
const hole = outlineAt(t0);
|
|
144
176
|
const tier = TIER_IDS[count][k];
|
|
145
177
|
const run = (region.y1 - hole.y1 + (hole.y0 - region.y0)) / 2;
|
|
146
|
-
const rise = Math.min(STADIUM_RISES[count][k], Math.max(6, run * Math.tan((MAX_RAKE * Math.PI) / 180)));
|
|
147
|
-
const h0 = k === 0 ? STADIUM_FIRST_HEIGHT : up + STADIUM_WALK_RISE;
|
|
178
|
+
const rise = Math.min(STADIUM_RISES[count][k] * vscale, Math.max(6, run * Math.tan((MAX_RAKE * Math.PI) / 180)));
|
|
179
|
+
const h0 = k === 0 ? STADIUM_FIRST_HEIGHT : up + STADIUM_WALK_RISE * vscale;
|
|
148
180
|
const h1 = h0 + rise;
|
|
149
181
|
up = h1;
|
|
150
182
|
rings.push({
|
|
@@ -164,27 +196,43 @@ function plan(stadium, config, rink) {
|
|
|
164
196
|
});
|
|
165
197
|
return { rings, tight };
|
|
166
198
|
};
|
|
199
|
+
const heightOf = (made) => made.rings[made.rings.length - 1].height[1];
|
|
167
200
|
let seatArea = SEAT_AREAS[0];
|
|
168
|
-
let made = build(seatArea);
|
|
201
|
+
let made = build(seatArea, 1);
|
|
169
202
|
for (const area of SEAT_AREAS.slice(1)) {
|
|
170
203
|
if (!made.tight)
|
|
171
204
|
break;
|
|
172
205
|
seatArea = area;
|
|
173
|
-
made = build(area);
|
|
206
|
+
made = build(area, 1);
|
|
207
|
+
}
|
|
208
|
+
// (A bowl's rise is capped by its rake, so lowering it is not in proportion: a few rounds bring the top row under the canopy.)
|
|
209
|
+
let vscale = 1;
|
|
210
|
+
for (let pass = 0; pass < 10 && heightOf(made) > low - CANOPY_CLEARANCE + 0.5; pass += 1) {
|
|
211
|
+
vscale = Math.max(0.1, vscale * ((low - CANOPY_CLEARANCE - STADIUM_FIRST_HEIGHT) / (heightOf(made) - STADIUM_FIRST_HEIGHT)));
|
|
212
|
+
made = build(seatArea, vscale);
|
|
174
213
|
}
|
|
175
214
|
const { rings, tight } = made;
|
|
176
|
-
// The rounding of each bowl's seats leaves the total a seat or two off the
|
|
215
|
+
// The rounding of each bowl's seats leaves the total a seat or two off the bowl's: the last bowl takes the difference.
|
|
177
216
|
const missing = seatsTotal - rings.reduce((sum, ring) => sum + ring.seats, 0);
|
|
178
217
|
if (!tight && Math.abs(missing) <= 3)
|
|
179
218
|
rings[rings.length - 1].seats += missing;
|
|
180
219
|
const total = rings.reduce((sum, ring) => sum + ring.seats, 0);
|
|
220
|
+
// A configuration that opens fewer seats than the bowl holds opens them from the lowest bowl up: a bowl's people are its open share of them, and a closed one is empty.
|
|
221
|
+
if (open < seatsTotal) {
|
|
222
|
+
let before = 0;
|
|
223
|
+
for (const ring of rings) {
|
|
224
|
+
ring.fill = round(ring.fill * Math.min(1, Math.max(0, (open - before) / Math.max(1, ring.seats))), 3);
|
|
225
|
+
before += ring.seats;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
181
228
|
const top = rings[rings.length - 1].height[1];
|
|
182
229
|
const roof = stadium.osm.roof;
|
|
183
230
|
const rise = roof?.rise === undefined ? 0 : round(roof.rise * FEET_PER_METRE);
|
|
184
|
-
// The rim of
|
|
231
|
+
// The rim of a roof stands where OpenStreetMap maps it (its height less the roof's own rise), and at the least over the top row by a roof's clearance; a canopy's building has walls to the top row and a little over it.
|
|
185
232
|
const mapped = roof?.height === undefined ? 0 : round((roof.height - (roof.rise ?? 0)) * FEET_PER_METRE);
|
|
186
|
-
const edge = Math.max(mapped, top + STADIUM_CLEARANCE);
|
|
233
|
+
const edge = stadium.roof.kind === "canopy" ? round(top + CANOPY_CLEARANCE) : Math.max(mapped, top + STADIUM_CLEARANCE);
|
|
187
234
|
const cable = stadium.roof.kind === "cable" && opening !== undefined && rise > 0;
|
|
235
|
+
const canopies = stadium.roof.kind === "canopy" && stadium.osm.canopies !== undefined ? stadium.osm.canopies.map((c) => ({ ring: place(turned(c.ring)), low: round((c.height - c.rise) * FEET_PER_METRE), rise: round(c.rise * FEET_PER_METRE) })) : undefined;
|
|
188
236
|
const north = orientation.basis === "none" ? undefined : orientation.north;
|
|
189
237
|
const room = north === undefined ? 0 : NORTH_ROOM;
|
|
190
238
|
const bowl = {
|
|
@@ -195,12 +243,13 @@ function plan(stadium, config, rink) {
|
|
|
195
243
|
footprint,
|
|
196
244
|
hall: { ...outer, x0: outer.x0 - HALL, y0: outer.y0 - HALL, x1: outer.x1 + HALL, y1: outer.y1 + HALL, r: outer.r + HALL },
|
|
197
245
|
...(north === undefined ? {} : { north }),
|
|
198
|
-
roof: cable ? "cable" : "fixed",
|
|
246
|
+
roof: cable ? "cable" : canopies === undefined ? "fixed" : "canopy",
|
|
199
247
|
edge,
|
|
200
248
|
...(cable ? { crown: { rise, opening } } : {}),
|
|
249
|
+
...(canopies === undefined ? {} : { canopies }),
|
|
201
250
|
},
|
|
202
251
|
};
|
|
203
|
-
return { stadium, config, orientation, box: rink, footprint, opening, bowl, seats: rings.map((ring) => ({ tier: ring.tier, seats: ring.seats })), total, seatArea, fit: tight ? "tight" : "fits" };
|
|
252
|
+
return { stadium, config, orientation, box: rink, footprint, opening, bowl, seats: rings.map((ring) => ({ tier: ring.tier, seats: ring.seats })), total, open, seatArea, fit: tight ? "tight" : "fits" };
|
|
204
253
|
}
|
|
205
254
|
/** The stands of a stadium for `layoutOf` and the 3D view, round a field: `planStadium`'s bowl. */
|
|
206
255
|
export const stadiumBowl = (stadium, config, box) => planStadium(stadium, config, box).bowl;
|
|
@@ -208,3 +257,83 @@ export const stadiumBowl = (stadium, config, box) => planStadium(stadium, config
|
|
|
208
257
|
export function stadiumBowlInside(plan) {
|
|
209
258
|
return outlinePoints(plan.bowl.rings[plan.bowl.rings.length - 1].region).every((p) => inside(plan.footprint, p));
|
|
210
259
|
}
|
|
260
|
+
const PARK_PLANS = new WeakMap();
|
|
261
|
+
/** The share of a ballpark's building the closed roof covers, and the share of its width along the east side the open roof's sections rest over (ours: the article says where they rest, not how much). */
|
|
262
|
+
const PARK_COVER = 0.92;
|
|
263
|
+
const PARK_PARKED = 0.3;
|
|
264
|
+
/**
|
|
265
|
+
* The plan of a ballpark from what its stands are laid from (`input`: the park's fence, the backstop and the crowd): the usual stands with each band as deep as its seats need, the building's outline turned so that home
|
|
266
|
+
* plate faces where OpenStreetMap's infield and the outline say, and the roof: sections far over the field, closed over the stands and the field or resting over the east side where the article says they rest.
|
|
267
|
+
*/
|
|
268
|
+
export function planPark(stadium, input) {
|
|
269
|
+
if (stadiumKind(stadium) !== "park")
|
|
270
|
+
throw new Error(`${stadium.id} is not a ballpark`);
|
|
271
|
+
const key = JSON.stringify([input.fence, input.backstop, input.crowd.shape, input.crowd.awayEnd, input.crowd.tifo, input.crowd.gaps, input.seatsOnWalls ?? null]);
|
|
272
|
+
const mine = PARK_PLANS.get(stadium) ?? new Map();
|
|
273
|
+
PARK_PLANS.set(stadium, mine);
|
|
274
|
+
const known = mine.get(key);
|
|
275
|
+
if (known !== undefined)
|
|
276
|
+
return known;
|
|
277
|
+
const made = park(stadium, input);
|
|
278
|
+
mine.set(key, made);
|
|
279
|
+
return made;
|
|
280
|
+
}
|
|
281
|
+
function park(stadium, input) {
|
|
282
|
+
const config = "mlb";
|
|
283
|
+
const seats = stadium.configs.mlb?.seats ?? 0;
|
|
284
|
+
const bands = stadium.tiers.tierSeats ?? { lower: seats * 0.45, upper: seats * 0.47, bleachers: seats * 0.08 };
|
|
285
|
+
const sum = bands.lower + bands.upper + bands.bleachers;
|
|
286
|
+
const wanted = { lower: (bands.lower / sum) * seats, upper: (bands.upper / sum) * seats, bleachers: (bands.bleachers / sum) * seats };
|
|
287
|
+
// Each band is as deep as holds its seats: the usual depths scaled by what the seats of the band are to what the band holds, a few rounds over (a band's seats grow with its depth).
|
|
288
|
+
const depths = { lower: 60, upper: 56, bleachers: 54 };
|
|
289
|
+
let stands = parkLayout({ ...input, depths });
|
|
290
|
+
for (let i = 0; i < 12; i += 1) {
|
|
291
|
+
const held = (id) => stands.rings.find((r) => r.id === id)?.seats ?? 0;
|
|
292
|
+
const next = { lower: depths.lower * (wanted.lower / Math.max(1, held("lower"))), upper: depths.upper * (wanted.upper / Math.max(1, held("upper"))), bleachers: depths.bleachers * (wanted.bleachers / Math.max(1, held("bleachers"))) };
|
|
293
|
+
for (const k of ["lower", "upper", "bleachers"])
|
|
294
|
+
depths[k] = Math.min(160, Math.max(20, next[k]));
|
|
295
|
+
stands = parkLayout({ ...input, depths });
|
|
296
|
+
}
|
|
297
|
+
const total = stands.rings.reduce((n, r) => n + r.seats, 0);
|
|
298
|
+
const orientation = stadiumOrientation(stadium);
|
|
299
|
+
const frameHeading = stadiumFrameHeading(stadium);
|
|
300
|
+
// The building in the park's frame, in feet: home plate at the origin.
|
|
301
|
+
const origin = middleOf(lonLat(stadium.osm.ring));
|
|
302
|
+
const world = metresFrom(origin, lonLat(stadium.osm.ring));
|
|
303
|
+
const [home] = metresFrom(origin, [stadium.osm.axis.centre]);
|
|
304
|
+
const frameOf = (p) => {
|
|
305
|
+
const [x, y] = turnedTo(frameHeading, [p[0] - home[0], p[1] - home[1]]);
|
|
306
|
+
return [round(x * FEET_PER_METRE, 2), round(y * FEET_PER_METRE, 2)];
|
|
307
|
+
};
|
|
308
|
+
const footprint = world.map(frameOf);
|
|
309
|
+
const fb = boxOf(footprint);
|
|
310
|
+
const box = stands.box;
|
|
311
|
+
const c = [box.x + box.w / 2, box.y + box.h / 2];
|
|
312
|
+
const cover = footprint.map((p) => [c[0] + (p[0] - c[0]) * PARK_COVER, c[1] + (p[1] - c[1]) * PARK_COVER]);
|
|
313
|
+
// Where the sections rest: the east side of the building, over the railway there (the article says so): the strip of the outline east of a share of its width.
|
|
314
|
+
const [wx0, wx1] = [Math.min(...world.map((p) => p[0])), Math.max(...world.map((p) => p[0]))];
|
|
315
|
+
const [wy0, wy1] = [Math.min(...world.map((p) => p[1])), Math.max(...world.map((p) => p[1]))];
|
|
316
|
+
const strip = clipConvex(world, [[wx1 - (wx1 - wx0) * PARK_PARKED, wy0 - 1], [wx1 + 1, wy0 - 1], [wx1 + 1, wy1 + 1], [wx1 - (wx1 - wx0) * PARK_PARKED, wy1 + 1]]);
|
|
317
|
+
const parked = strip.map(frameOf);
|
|
318
|
+
const north = orientation.basis === "none" ? undefined : orientation.north;
|
|
319
|
+
const room = north === undefined ? 0 : NORTH_ROOM;
|
|
320
|
+
const wallFeet = round((stadium.osm.height ?? 20) * FEET_PER_METRE);
|
|
321
|
+
const hall = { kind: "rrect", x0: fb.x0 - 30, y0: fb.y0 - 30, x1: fb.x1 + 30, y1: fb.y1 + 30, r: 0 };
|
|
322
|
+
const shell = {
|
|
323
|
+
footprint,
|
|
324
|
+
hall,
|
|
325
|
+
...(north === undefined ? {} : { north }),
|
|
326
|
+
roof: "panels",
|
|
327
|
+
edge: wallFeet,
|
|
328
|
+
wall: wallFeet,
|
|
329
|
+
panels: { cover, bottom: stadium.roof.bottom ?? 217, top: stadium.roof.top ?? 269, parked },
|
|
330
|
+
};
|
|
331
|
+
const reach = {
|
|
332
|
+
left: Math.max(stands.reach.left, box.x - fb.x0),
|
|
333
|
+
right: Math.max(stands.reach.right, fb.x1 - box.x - box.w),
|
|
334
|
+
top: Math.max(stands.reach.top, box.y - fb.y0) + room,
|
|
335
|
+
bottom: Math.max(stands.reach.bottom, fb.y1 - box.y - box.h),
|
|
336
|
+
};
|
|
337
|
+
const real = { ...stands, real: shell, reach };
|
|
338
|
+
return { stadium, config, orientation, footprint, stands: real, depths: { lower: round(depths.lower), upper: round(depths.upper), bleachers: round(depths.bleachers) }, seats: stands.rings.map((r) => ({ tier: r.id, seats: r.seats })), total, fit: Math.abs(total - seats) / seats <= 0.02 ? "fits" : "tight" };
|
|
339
|
+
}
|