@johnmorrisdotca/kyogijo-stadiums 0.23.0 → 0.25.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.
@@ -0,0 +1,16 @@
1
+ import { type Pt, type RealShell, type StandRing } from "./crowd.ts";
2
+ import { type Upright } from "./turn.kit.ts";
3
+ /** How far a lid rises from its edge towards the middle, and how far a saddle's high ends stand over its low sides, in feet (ours: the sources give a roof's kind, not its height). */
4
+ export declare const LID_RISE = 22;
5
+ export declare const SADDLE_RISE = 30;
6
+ /** The saddle's share of its rise at a point (0 at the middle of a side, 1 at the middle of an end): half a saddle's way from one to the other, as a hyperbolic paraboloid (u squared less v squared) lies over the building's box. */
7
+ export declare function saddleAt(real: Pick<RealShell, "footprint">, [x, y]: Pt): number;
8
+ /** The height of the roof over a point of the building's edge, above the surface, in feet. */
9
+ export declare const roofAt: (real: RealShell, p: Pt) => number;
10
+ /**
11
+ * The walls and the roof of a real venue. The hall is the building's outline cut to the bowl's wall and a margin (`RealShell.hall`): its rim is where a ray from the middle meets the one or the other first, in `steps`
12
+ * rays. A wall stands on each stretch of the rim, up to the roof's height over it; the roof is a band from the rim in to the inner edge of the top tier (the middle stays open, as the 3D view's roofs are).
13
+ */
14
+ export declare function realBuilding(real: RealShell, top: StandRing | undefined, centre: readonly [number, number], lift: number, steps: number): Items;
15
+ type Items = Upright[];
16
+ export {};
@@ -0,0 +1,103 @@
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 or a saddle that rises at the two ends and falls at the two
2
+ // sides. Both are asked for with the roof (`venue: { roof: true }`) and give way to the eye as the other walls do. Pure: data in, planes out.
3
+ import { rayAt } from "./crowd.js";
4
+ import { n, shy, wall } from "./turn.kit.js";
5
+ import { pointList, raked } from "./venue.geom.js";
6
+ /** How far a lid rises from its edge towards the middle, and how far a saddle's high ends stand over its low sides, in feet (ours: the sources give a roof's kind, not its height). */
7
+ export const LID_RISE = 22;
8
+ export const SADDLE_RISE = 30;
9
+ /** The saddle's share of its rise at a point (0 at the middle of a side, 1 at the middle of an end): half a saddle's way from one to the other, as a hyperbolic paraboloid (u squared less v squared) lies over the building's box. */
10
+ export function saddleAt(real, [x, y]) {
11
+ const xs = real.footprint.map((p) => p[0]);
12
+ const ys = real.footprint.map((p) => p[1]);
13
+ const [cx, cy] = [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2];
14
+ const [hx, hy] = [(Math.max(...xs) - Math.min(...xs)) / 2, (Math.max(...ys) - Math.min(...ys)) / 2];
15
+ const s = ((x - cx) / hx) ** 2 - ((y - cy) / hy) ** 2;
16
+ return (1 + Math.max(-1, Math.min(1, s))) / 2;
17
+ }
18
+ /** The height of the roof over a point of the building's edge, above the surface, in feet. */
19
+ export const roofAt = (real, p) => real.edge + (real.roof === "saddle" ? SADDLE_RISE * saddleAt(real, p) : 0);
20
+ /** The point where a ray from `from` first meets a polygon, or `undefined`. */
21
+ function firstHit(poly, from, angle) {
22
+ const [dx, dy] = [Math.cos(angle), Math.sin(angle)];
23
+ let best;
24
+ poly.forEach(([ax, ay], i) => {
25
+ const [bx, by] = poly[(i + 1) % poly.length];
26
+ const [ex, ey] = [bx - ax, by - ay];
27
+ const den = dx * ey - dy * ex;
28
+ if (Math.abs(den) < 1e-12)
29
+ return;
30
+ const t = ((ax - from[0]) * ey - (ay - from[1]) * ex) / den;
31
+ const u = ((ax - from[0]) * dy - (ay - from[1]) * dx) / den;
32
+ if (t > 1e-6 && u >= 0 && u <= 1 && (best === undefined || t < best.t))
33
+ best = { t, p: [from[0] + dx * t, from[1] + dy * t] };
34
+ });
35
+ return best?.p;
36
+ }
37
+ /** The plane a quad of points with heights lies nearest to, as a slope: least squares through its corners. */
38
+ function slopeThrough(points) {
39
+ const m = points.length;
40
+ const [mx, my, mz] = [points.reduce((s, p) => s + p[0], 0) / m, points.reduce((s, p) => s + p[1], 0) / m, points.reduce((s, p) => s + p[2], 0) / m];
41
+ let [sxx, sxy, syy, sxz, syz] = [0, 0, 0, 0, 0];
42
+ for (const [x, y, z] of points) {
43
+ sxx += (x - mx) ** 2;
44
+ sxy += (x - mx) * (y - my);
45
+ syy += (y - my) ** 2;
46
+ sxz += (x - mx) * (z - mz);
47
+ syz += (y - my) * (z - mz);
48
+ }
49
+ const det = sxx * syy - sxy * sxy;
50
+ const [gx, gy] = Math.abs(det) < 1e-9 ? [0, 0] : [(sxz * syy - syz * sxy) / det, (syz * sxx - sxz * sxy) / det];
51
+ return { x0: mx, y0: my, z0: mz, gx, gy };
52
+ }
53
+ /**
54
+ * The walls and the roof of a real venue. The hall is the building's outline cut to the bowl's wall and a margin (`RealShell.hall`): its rim is where a ray from the middle meets the one or the other first, in `steps`
55
+ * rays. A wall stands on each stretch of the rim, up to the roof's height over it; the roof is a band from the rim in to the inner edge of the top tier (the middle stays open, as the 3D view's roofs are).
56
+ */
57
+ export function realBuilding(real, top, centre, lift, steps) {
58
+ const items = [];
59
+ const c = [centre[0], centre[1]];
60
+ const rim = Array.from({ length: steps }, (_, k) => {
61
+ const angle = (2 * Math.PI * k) / steps;
62
+ const cap = rayAt(real.hall, c[0], c[1], angle);
63
+ const out = firstHit(real.footprint, c, angle);
64
+ return out !== undefined && Math.hypot(out[0] - c[0], out[1] - c[1]) < Math.hypot(cap[0] - c[0], cap[1] - c[1]) ? out : cap;
65
+ });
66
+ const planes = [];
67
+ rim.forEach((a, i) => {
68
+ const b = rim[(i + 1) % steps];
69
+ const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
70
+ if (long < 1)
71
+ return;
72
+ const high = lift + roofAt(real, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
73
+ 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 });
74
+ });
75
+ if (planes.length > 0)
76
+ items.push({ id: "shell", kind: "walls", at: centre, height: Math.max(...planes.map((p) => p.o[2])), planes });
77
+ if (top?.hole == null)
78
+ return items;
79
+ const roof = [];
80
+ const hole = top.hole;
81
+ const inner = rim.map((_, k) => rayAt(hole, c[0], c[1], (2 * Math.PI * k) / steps));
82
+ const rise = real.roof === "saddle" ? 0 : LID_RISE;
83
+ const zOf = (p, inward) => lift + roofAt(real, p) + (inward ? rise : 0);
84
+ for (let k = 0; k < steps; k += 1) {
85
+ const [a, b] = [rim[k], rim[(k + 1) % steps]];
86
+ const [ai, bi] = [inner[k], inner[(k + 1) % steps]];
87
+ const quad = [ai, bi, b, a];
88
+ const heights = [zOf(ai, true), zOf(bi, true), zOf(b, false), zOf(a, false)];
89
+ const slope = slopeThrough(quad.map((p, i) => [p[0], p[1], heights[i]]));
90
+ const [ex, ey] = [b[0] - a[0], b[1] - a[1]];
91
+ const el = Math.hypot(ex, ey);
92
+ if (el < 1e-6)
93
+ continue;
94
+ const mid = [(a[0] + b[0] + ai[0] + bi[0]) / 4, (a[1] + b[1] + ai[1] + bi[1]) / 4];
95
+ const away = Math.hypot(mid[0] - c[0], mid[1] - c[1]);
96
+ const piece = raked(`roof-${k}`, "roof", quad, slope, [ex / el, ey / el], (corners) => `<polygon class="u-roof" points="${pointList(corners)}"/>`);
97
+ if (piece !== undefined)
98
+ roof.push({ ...piece, shy: [(mid[0] - c[0]) / away, (mid[1] - c[1]) / away] });
99
+ }
100
+ if (roof.length > 0)
101
+ 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 });
102
+ return items;
103
+ }
@@ -0,0 +1,2 @@
1
+ export { VENUE_ATTRIBUTION, VENUE_IDS, VENUES, arenaOfVenue, bowlInside, planVenue, venueBowl, venueById, venueByWikidata, venueOrientation, venueRink, venuesIn } from "./venues.ts";
2
+ export type { RealVenue, RealVenueData, VenueBasis, VenueDivision, VenueOrientation, VenueOsm, VenuePlan, VenueRoof } from "./venues.ts";
@@ -0,0 +1,2 @@
1
+ // The real venues of the NHL's arenas: the outline of each building and the way its rink lies (OpenStreetMap, ODbL 1.0) and a plausible bowl from the seats, tiers and roof the sources give (Wikidata, Wikipedia).
2
+ export { VENUE_ATTRIBUTION, VENUE_IDS, VENUES, arenaOfVenue, bowlInside, planVenue, venueBowl, venueById, venueByWikidata, venueOrientation, venueRink, venuesIn } from "./venues.js";
@@ -0,0 +1,153 @@
1
+ import { type Arena, type ArenaSource } from "./arenas.ts";
2
+ import { type Box, type Pt, type RealBowl } from "./crowd.ts";
3
+ import { VENUE_ATTRIBUTION } from "./venues.osm.data.ts";
4
+ export { VENUE_ATTRIBUTION };
5
+ /** How a fact is known: `documented` is what a source cited with it says; `plausible` is ours, for a bowl no source describes. */
6
+ export type VenueBasis = "documented" | "plausible";
7
+ /** The kinds of roof the venues have: `fixed` is a lid over the bowl, `saddle` a roof that rises at the two ends and falls at the two sides (the Scotiabank Saddledome's). */
8
+ export type VenueRoof = "fixed" | "saddle";
9
+ /** The NHL's divisions that have venues so far. */
10
+ export type VenueDivision = "pacific";
11
+ /** The facts of a venue: the seats and the number of seating levels and the kind of roof, each with what a source says. No text of an article is kept. */
12
+ export interface RealVenueData {
13
+ /** The key of the arena in `ARENAS`: `honda-center`. */
14
+ id: string;
15
+ /** The Wikidata item of the arena. */
16
+ wikidata: string;
17
+ division: VenueDivision;
18
+ /** The English Wikipedia article the facts that are not Wikidata's were read from, at a revision. */
19
+ en: ArenaSource;
20
+ seats: {
21
+ /** The seats for ice hockey: Wikidata's where its item gives a figure for ice hockey (`wikidata`), else the article's (`wikipedia`). */
22
+ hockey: number;
23
+ source: "wikidata" | "wikipedia";
24
+ /** Wikidata's own capacity (P1083), kept beside it: it is often for another event, or an older year. */
25
+ wikidata: number;
26
+ /** What was read, in a few words of ours. */
27
+ read: string;
28
+ };
29
+ tiers: {
30
+ /** The seating levels (bowls), with the suites and the clubs that lie between them counted as walks, not tiers. */
31
+ count: 1 | 2 | 3;
32
+ basis: VenueBasis;
33
+ /** The share of the seats that are in the lowest tier, where a source gives it. */
34
+ lowerShare?: number;
35
+ read: string;
36
+ };
37
+ roof: {
38
+ kind: VenueRoof;
39
+ basis: VenueBasis;
40
+ read: string;
41
+ };
42
+ }
43
+ /** What OpenStreetMap gave of a venue: ODbL 1.0 data, "(c) OpenStreetMap contributors". */
44
+ export interface VenueOsm {
45
+ /** The element the outline is: `way/120623151`. */
46
+ source: string;
47
+ /** The day it was read and the time of the OpenStreetMap data the answer was made from. */
48
+ read: string;
49
+ base: string;
50
+ licence: "ODbL-1.0";
51
+ attribution: string;
52
+ /** The building's height in metres, where OpenStreetMap maps one. */
53
+ height?: number;
54
+ /** The building's outline as `[longitude, latitude]`, not repeating its first point. */
55
+ ring: [number, number][];
56
+ /** The ice, where OpenStreetMap maps it inside the building: the element and its outline. */
57
+ ice?: {
58
+ source: string;
59
+ ring: [number, number][];
60
+ };
61
+ /**
62
+ * How the rink lies, worked out when the data was made: the heading of its long side (degrees clockwise from north, 0 to under 180), how that is known (`mapped`: from the ice OpenStreetMap maps; `building`: from the long
63
+ * axis of the building's main hall, the largest rectangle that fits its outline, which a rink usually follows, an inference; `none`: the building has no direction, 90 is stood in), how much of a direction the building has
64
+ * (`strength`, 1 for a round one) and how much longer than wide its main hall is (`elongation`), and the middle of the ice or of the main hall as `[longitude, latitude]`.
65
+ */
66
+ axis: {
67
+ heading: number;
68
+ basis: "mapped" | "building" | "none";
69
+ strength: number;
70
+ elongation: number;
71
+ centre: [number, number];
72
+ };
73
+ }
74
+ /** A venue: its facts and what OpenStreetMap gave. */
75
+ export interface RealVenue extends RealVenueData {
76
+ osm: VenueOsm;
77
+ }
78
+ /** How the rink lies in the building: its long side's heading (degrees clockwise from north, 0 to under 180), how that is known, and the way north points on the page (degrees clockwise from the top). */
79
+ export interface VenueOrientation {
80
+ heading: number;
81
+ /** `mapped`: from the ice OpenStreetMap maps in the building. `building`: from the building's long axis, which a rink usually follows (an inference). `none`: the building is near round, nothing is known, the plan has north at the top. */
82
+ basis: "mapped" | "building" | "none";
83
+ north: number;
84
+ }
85
+ /** The venues by key: every arena of the Pacific division of the NHL, with its facts and its outline. */
86
+ export declare const VENUES: Readonly<Record<string, RealVenue>>;
87
+ /** The keys of `VENUES`, in the order of the data. */
88
+ export declare const VENUE_IDS: readonly string[];
89
+ /** The venue of an arena (by its key in `ARENAS`), or `undefined`. */
90
+ export declare const venueById: (id: string | undefined) => RealVenue | undefined;
91
+ /** The venue whose arena is the Wikidata item `Q774138`, or `undefined`. */
92
+ export declare const venueByWikidata: (wikidata: string | undefined) => RealVenue | undefined;
93
+ /** The venues of a division of the NHL, in the order of the data. */
94
+ export declare const venuesIn: (division: VenueDivision) => RealVenue[];
95
+ /** The arena a venue is of. */
96
+ export declare const arenaOfVenue: (venue: RealVenue) => Arena;
97
+ /** How the rink lies in a venue's building (see `VenueOsm.axis`). */
98
+ export declare const venueOrientation: (venue: RealVenue) => VenueOrientation;
99
+ /** The rink's box as the drawing has it: the arena's own where its sources publish a size, else the NHL's. */
100
+ export declare function venueRink(venue: RealVenue): Box;
101
+ /** The floor a seat takes in plan, in square feet (ours): a bowl of today's arenas is steeper than the usual drawing's, whose seat is 2 by 2.3 feet (4.6), so that a seat takes less of the floor's area. Tried in turn: a building
102
+ * with room for the seats at the first has the first; one with less has a steeper bowl, to the last, which is the least a seat takes. */
103
+ export declare const SEAT_AREAS: readonly number[];
104
+ /** How far the bowl's outer edge keeps from the building's wall, in feet (ours: the wall and the walk round the bowl). */
105
+ export declare const WALL = 10;
106
+ /** How far the hall's wall stands beyond the bowl's outer edge, in feet, where the building's outline allows: the concourse and the wall, ours. */
107
+ export declare const HALL = 30;
108
+ /** The walk between two tiers, in feet (ours: a suite level or a club between two bowls). */
109
+ export declare const GAP = 10;
110
+ /** The height of the first row above the ice, in feet, the rise of a tier's rows by the number of tiers (what the lowest, a middle and the upper bowl rise, at most), the rise of the walk between two tiers (a suite level), and the steepest a bowl is raked, in degrees (ours: no source gives them). */
111
+ export declare const FIRST_HEIGHT = 4;
112
+ export declare const RISES: Readonly<Record<1 | 2 | 3, readonly number[]>>;
113
+ export declare const WALK_RISE = 16;
114
+ export declare const MAX_RAKE = 36;
115
+ /** The height a roof's edge stands over the top row, at the least, in feet. */
116
+ export declare const CLEARANCE = 22;
117
+ /** The plan of a venue: its building in the drawing's frame, the bowl fitted inside, and how it lies. */
118
+ export interface VenuePlan {
119
+ venue: RealVenue;
120
+ orientation: VenueOrientation;
121
+ /** The rink, in the drawing's frame. */
122
+ box: Box;
123
+ /** The building's outline in feet, in the drawing's frame (the rink along x, in the middle of the building's box). */
124
+ footprint: Pt[];
125
+ /** The stands for `layoutOf`: tiers, first row, ground, and the building and roof. */
126
+ bowl: RealBowl;
127
+ /** The seats the bowl's tiers hold, by tier, and in all: equal to the venue's seats where the building has room (`fit` is `fits`) and fewer where it has not (`tight`). */
128
+ seats: {
129
+ tier: string;
130
+ seats: number;
131
+ }[];
132
+ total: number;
133
+ /** The floor a seat takes in the plan, in square feet (one of `SEAT_AREAS`). */
134
+ seatArea: number;
135
+ fit: "fits" | "tight";
136
+ }
137
+ /**
138
+ * The plan of a venue's bowl: the building's outline turned so that the rink lies along x, a first row round the rink as an NHL arena has it, the largest bowl-shaped outline that fits the building round the middle of its main
139
+ * hall, and between the two the tiers, the outer edge of each placed so that its band holds the share of the venue's seats the sources give it (at `SEAT_AREAS` square feet a seat), with a walk between the tiers. The
140
+ * bowl is as long and wide as the building lets it be and as round at the corners as a bowl is; the rest is the building's.
141
+ * @example
142
+ * ```ts
143
+ * import { planVenue, venueById } from "@johnmorrisdotca/kyogijo-stadiums/venues";
144
+ * const plan = planVenue(venueById("honda-center")!);
145
+ * console.log(plan.orientation.basis, plan.seats.map((t) => t.tier).join(" "), plan.total);
146
+ * // building lower upper 17174
147
+ * ```
148
+ */
149
+ export declare function planVenue(venue: RealVenue, rink?: Box): VenuePlan;
150
+ /** The stands of a venue for `layoutOf` and the 3D view, round a rink: `planVenue`'s bowl. */
151
+ export declare const venueBowl: (venue: RealVenue, rink?: Box) => RealBowl;
152
+ /** Whether the bowl lies inside the building: every corner of every tier's outer edge is inside the footprint. */
153
+ export declare function bowlInside(plan: VenuePlan): boolean;
@@ -0,0 +1,2 @@
1
+ import type { RealVenueData } from "./venues.ts";
2
+ export declare const VENUE_DATA: RealVenueData[];
@@ -0,0 +1,74 @@
1
+ export const VENUE_DATA = [
2
+ {
3
+ "id": "honda-center",
4
+ "wikidata": "Q774138",
5
+ "division": "pacific",
6
+ "en": { "title": "Honda Center", "revision": 1380000927 },
7
+ "seats": { "hockey": 17174, "source": "wikipedia", "wikidata": 17174, "read": "the article's seating line for the Ducks, 17,174, which the infobox gives for ice hockey too; Wikidata's one capacity (P1083) is the same" },
8
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls (a lower and an upper) are drawn, the usual for an arena of 17,000 seats; the article speaks of two full levels of ribbon displays, one on each bowl's fascia, which agrees" },
9
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
10
+ },
11
+ {
12
+ "id": "scotiabank-saddledome",
13
+ "wikidata": "Q1140083",
14
+ "division": "pacific",
15
+ "en": { "title": "Scotiabank Saddledome", "revision": 1377341466 },
16
+ "seats": { "hockey": 19289, "source": "wikipedia", "wikidata": 16605, "read": "the infobox's present figure for hockey (19,289, from 1995), which the article's amenities line gives too; Wikidata's 16,605 is the 1983 figure and the first the infobox lists" },
17
+ "tiers": { "count": 2, "basis": "documented", "read": "the article speaks of the lower bowl, a second level and upper loges above it: two seating levels with suites at the top of the lower bowl" },
18
+ "roof": { "kind": "saddle", "basis": "documented", "read": "the article says the roof is a reverse hyperbolic paraboloid, shaped like a horse saddle; which way the saddle lies over the ice is not given: it is drawn high at the two ends" },
19
+ },
20
+ {
21
+ "id": "rogers-place",
22
+ "wikidata": "Q5339036",
23
+ "division": "pacific",
24
+ "en": { "title": "Rogers Place", "revision": 1380110483 },
25
+ "seats": { "hockey": 18641, "source": "wikidata", "wikidata": 18641, "read": "Wikidata's capacity for ice hockey (P1083 with the sport P641 ice hockey); the article's own line gives 18,347 for a hockey arena, 1.6 percent fewer, and OpenStreetMap has 18,641" },
26
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn" },
27
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
28
+ },
29
+ {
30
+ "id": "crypto-com-arena",
31
+ "wikidata": "Q186080",
32
+ "division": "pacific",
33
+ "en": { "title": "Crypto.com Arena", "revision": 1380458624 },
34
+ "seats": { "hockey": 18145, "source": "wikipedia", "wikidata": 19060, "read": "the article's figure for ice hockey, 18,145, which the infobox gives too; Wikidata's one capacity (19,060) is not for hockey" },
35
+ "tiers": { "count": 2, "basis": "documented", "lowerShare": 0.667, "read": "the article says two thirds of the seating, with the club seats, is in the lower bowl, and that three levels of suites lie between the lower and upper bowls" },
36
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
37
+ },
38
+ {
39
+ "id": "sap-center",
40
+ "wikidata": "Q261863",
41
+ "division": "pacific",
42
+ "en": { "title": "SAP Center", "revision": 1380804230 },
43
+ "seats": { "hockey": 17435, "source": "wikipedia", "wikidata": 19190, "read": "the article says the hockey capacity changed from 17,562 to 17,435 when the new premium seating was finished (2023); Wikidata's one capacity (19,190) is the concert one" },
44
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article speaks of club levels and a penthouse, which are not seating bowls" },
45
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
46
+ },
47
+ {
48
+ "id": "climate-pledge-arena",
49
+ "wikidata": "Q977529",
50
+ "division": "pacific",
51
+ "en": { "title": "Climate Pledge Arena", "revision": 1380851727 },
52
+ "seats": { "hockey": 17151, "source": "wikipedia", "wikidata": 18000, "read": "the article's seating capacity for ice hockey, 17,151, which the infobox gives too; Wikidata's one capacity (18,000) is near the basketball one (18,300)" },
53
+ "tiers": { "count": 2, "basis": "documented", "read": "the article tells of the upper level and the lower level, most of the lower level being below street level since the 2021 rebuild: two seating levels" },
54
+ "roof": { "kind": "fixed", "basis": "documented", "read": "the article says the original 1962 roof's shape was kept and carried on a fixed roof of steel trusses (the first, cable-hung, roof being taken down), and that the roof is landmarked" },
55
+ },
56
+ {
57
+ "id": "rogers-arena",
58
+ "wikidata": "Q517318",
59
+ "division": "pacific",
60
+ "en": { "title": "Rogers Arena", "revision": 1379918849 },
61
+ "seats": { "hockey": 18871, "source": "wikipedia", "wikidata": 18422, "read": "the infobox's present figure for ice hockey (18,871, from 2018); Wikidata's 18,422 is the 1995 figure and the first the infobox lists" },
62
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article speaks of a lower bowl; OpenStreetMap maps four building levels, which are floors, not seating bowls" },
63
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
64
+ },
65
+ {
66
+ "id": "t-mobile-arena",
67
+ "wikidata": "Q3338973",
68
+ "division": "pacific",
69
+ "en": { "title": "T-Mobile Arena", "revision": 1379155307 },
70
+ "seats": { "hockey": 17500, "source": "wikipedia", "wikidata": 20000, "read": "the infobox's figure for ice hockey, 17,500 (20,000 with standing room); Wikidata's one capacity (20,000) is the one with standing room" },
71
+ "tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn" },
72
+ "roof": { "kind": "fixed", "basis": "plausible", "read": "an indoor arena: a fixed roof, as nothing says otherwise" },
73
+ },
74
+ ];
@@ -0,0 +1,60 @@
1
+ import type { Pt } from "./crowd.ts";
2
+ /** A point as `[longitude, latitude]`, in degrees, the order OpenStreetMap's GeoJSON gives. */
3
+ export type LonLat = readonly [number, number];
4
+ /** Points in degrees as `[east, north]` metres from `origin`, on a plane tangent to the earth there (good to a few centimetres over a building's size). */
5
+ export declare function metresFrom(origin: LonLat, ring: readonly LonLat[]): Pt[];
6
+ /** The inverse of `metresFrom`: a point given as east and north metres from `origin`, in degrees. */
7
+ export declare function lonLatFrom(origin: LonLat, [east, north]: Pt): LonLat;
8
+ /** The mean of a ring's longitudes and latitudes: a point to measure from. */
9
+ export declare const middleOf: (ring: readonly LonLat[]) => LonLat;
10
+ /** The area of a polygon, unsigned. */
11
+ export declare function areaOf(points: readonly Pt[]): number;
12
+ /** The centre of mass of a polygon's area. */
13
+ export declare function centroidOf(points: readonly Pt[]): Pt;
14
+ /** The convex hull of some points (Andrew's monotone chain), counter-clockwise. */
15
+ export declare function hullOf(points: readonly Pt[]): Pt[];
16
+ /** The heading, in degrees clockwise from north, of a direction given as east and north components, taken modulo 180 (an axis has no head): 0 to under 180. */
17
+ export declare const axisHeading: (east: number, north: number) => number;
18
+ /** The smallest rectangle (by area) that encloses some points: its long side's heading, its length and its width, in the units of the points. */
19
+ export declare function smallestBox(points: readonly Pt[]): {
20
+ heading: number;
21
+ length: number;
22
+ width: number;
23
+ };
24
+ /**
25
+ * A point given as east and north metres, in the drawing's frame, for a rink whose long side runs along `heading` (degrees clockwise from north, 0 to under 180): x along the rink's long side, y across it and down the page,
26
+ * so that the picture is the map turned and not mirrored. Metres stay metres.
27
+ */
28
+ export declare function turnedTo(heading: number, [east, north]: Pt): Pt;
29
+ /** The way north points in the drawing, in degrees clockwise from the top of the page, for a rink along `heading`. 90 less the heading: a rink that runs east-west has north at the top. */
30
+ export declare const northOn: (heading: number) => number;
31
+ /** A ring with points that lie within `tolerance` of the line between their neighbours taken out (Douglas and Peucker), the ends kept: a ring is a list of points not repeating its first. */
32
+ export declare function simplified(ring: readonly Pt[], tolerance: number): Pt[];
33
+ /** Whether a point is in a polygon (even-odd). */
34
+ export declare function inside(points: readonly Pt[], [x, y]: Pt): boolean;
35
+ /** The smallest box round some points. */
36
+ export declare function boxOf(points: readonly Pt[]): {
37
+ x0: number;
38
+ y0: number;
39
+ x1: number;
40
+ y1: number;
41
+ };
42
+ /** The distance from a point to the nearest edge of a polygon. */
43
+ export declare function edgeDistance(points: readonly Pt[], [x, y]: Pt): number;
44
+ /** What `coreOf` finds: the building's main hall, as the largest rectangle that lies inside its outline. */
45
+ export interface Core {
46
+ /** The heading of the rectangle's long side, 0 to under 180, degrees clockwise from north. */
47
+ heading: number;
48
+ /** The rectangle's long and short sides, in the units of the points. */
49
+ length: number;
50
+ width: number;
51
+ /** The middle of the rectangle, as east and north from the points' origin. */
52
+ centre: Pt;
53
+ /** How much the best direction beats the worst: the largest rectangle's area over the smallest, over every direction. A round building has 1; a building with a direction has more. */
54
+ strength: number;
55
+ }
56
+ /**
57
+ * The main hall of a building: the largest rectangle inside its outline over every direction (`STEP` degrees apart), which does not see a wing, a plaza or a ramp that a rectangle cannot fill. Its long side is the
58
+ * building's axis, its middle the middle of the hall, and `strength` how much of a direction the building has (a round one has none). Ties between a direction and the one at a right angle go to the longer rectangle.
59
+ */
60
+ export declare function coreOf(points: readonly Pt[]): Core;
@@ -0,0 +1,211 @@
1
+ const EARTH = 6371008.8;
2
+ const RAD = Math.PI / 180;
3
+ /** Points in degrees as `[east, north]` metres from `origin`, on a plane tangent to the earth there (good to a few centimetres over a building's size). */
4
+ export function metresFrom(origin, ring) {
5
+ const k = Math.cos(origin[1] * RAD);
6
+ return ring.map(([lon, lat]) => [(lon - origin[0]) * RAD * EARTH * k, (lat - origin[1]) * RAD * EARTH]);
7
+ }
8
+ /** The inverse of `metresFrom`: a point given as east and north metres from `origin`, in degrees. */
9
+ export function lonLatFrom(origin, [east, north]) {
10
+ return [origin[0] + east / (RAD * EARTH * Math.cos(origin[1] * RAD)), origin[1] + north / (RAD * EARTH)];
11
+ }
12
+ /** The mean of a ring's longitudes and latitudes: a point to measure from. */
13
+ export const middleOf = (ring) => [ring.reduce((s, p) => s + p[0], 0) / ring.length, ring.reduce((s, p) => s + p[1], 0) / ring.length];
14
+ /** The area of a polygon, unsigned. */
15
+ export function areaOf(points) {
16
+ let s = 0;
17
+ points.forEach(([x, y], i) => {
18
+ const [x2, y2] = points[(i + 1) % points.length];
19
+ s += x * y2 - x2 * y;
20
+ });
21
+ return Math.abs(s) / 2;
22
+ }
23
+ /** The centre of mass of a polygon's area. */
24
+ export function centroidOf(points) {
25
+ let a = 0;
26
+ let cx = 0;
27
+ let cy = 0;
28
+ points.forEach(([x, y], i) => {
29
+ const [x2, y2] = points[(i + 1) % points.length];
30
+ const f = x * y2 - x2 * y;
31
+ a += f;
32
+ cx += (x + x2) * f;
33
+ cy += (y + y2) * f;
34
+ });
35
+ return a === 0 ? [points[0][0], points[0][1]] : [cx / (3 * a), cy / (3 * a)];
36
+ }
37
+ /** The convex hull of some points (Andrew's monotone chain), counter-clockwise. */
38
+ export function hullOf(points) {
39
+ const sorted = [...points].sort((p, q) => p[0] - q[0] || p[1] - q[1]);
40
+ if (sorted.length < 3)
41
+ return sorted;
42
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
43
+ const half = (list) => {
44
+ const out = [];
45
+ for (const p of list) {
46
+ while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], p) <= 0)
47
+ out.pop();
48
+ out.push(p);
49
+ }
50
+ out.pop();
51
+ return out;
52
+ };
53
+ return [...half(sorted), ...half([...sorted].reverse())];
54
+ }
55
+ /** The heading, in degrees clockwise from north, of a direction given as east and north components, taken modulo 180 (an axis has no head): 0 to under 180. */
56
+ export const axisHeading = (east, north) => (((Math.atan2(east, north) / RAD) % 180) + 180) % 180;
57
+ /** The smallest rectangle (by area) that encloses some points: its long side's heading, its length and its width, in the units of the points. */
58
+ export function smallestBox(points) {
59
+ const hull = hullOf(points);
60
+ let best;
61
+ hull.forEach((p, i) => {
62
+ const q = hull[(i + 1) % hull.length];
63
+ const d = Math.hypot(q[0] - p[0], q[1] - p[1]);
64
+ if (d < 1e-9)
65
+ return;
66
+ const ux = (q[0] - p[0]) / d;
67
+ const uy = (q[1] - p[1]) / d;
68
+ let lo = Infinity;
69
+ let hi = -Infinity;
70
+ let left = Infinity;
71
+ let right = -Infinity;
72
+ for (const [x, y] of hull) {
73
+ const along = x * ux + y * uy;
74
+ const across = -x * uy + y * ux;
75
+ lo = Math.min(lo, along);
76
+ hi = Math.max(hi, along);
77
+ left = Math.min(left, across);
78
+ right = Math.max(right, across);
79
+ }
80
+ const [a, b] = [hi - lo, right - left];
81
+ if (best === undefined || a * b < best.area - 1e-9)
82
+ best = a >= b ? { area: a * b, heading: axisHeading(ux, uy), length: a, width: b } : { area: a * b, heading: axisHeading(-uy, ux), length: b, width: a };
83
+ });
84
+ if (best === undefined)
85
+ throw new Error("a rectangle needs points that are not all the same");
86
+ return { heading: best.heading, length: best.length, width: best.width };
87
+ }
88
+ /**
89
+ * A point given as east and north metres, in the drawing's frame, for a rink whose long side runs along `heading` (degrees clockwise from north, 0 to under 180): x along the rink's long side, y across it and down the page,
90
+ * so that the picture is the map turned and not mirrored. Metres stay metres.
91
+ */
92
+ export function turnedTo(heading, [east, north]) {
93
+ const a = heading * RAD;
94
+ return [east * Math.sin(a) + north * Math.cos(a), east * Math.cos(a) - north * Math.sin(a)];
95
+ }
96
+ /** The way north points in the drawing, in degrees clockwise from the top of the page, for a rink along `heading`. 90 less the heading: a rink that runs east-west has north at the top. */
97
+ export const northOn = (heading) => 90 - heading;
98
+ /** A ring with points that lie within `tolerance` of the line between their neighbours taken out (Douglas and Peucker), the ends kept: a ring is a list of points not repeating its first. */
99
+ export function simplified(ring, tolerance) {
100
+ const keep = new Array(ring.length).fill(false);
101
+ // The two farthest-apart points anchor the ring; each half is then simplified as a line.
102
+ let [a, b, far] = [0, 0, -1];
103
+ ring.forEach((p, i) => ring.forEach((q, j) => {
104
+ const d = Math.hypot(p[0] - q[0], p[1] - q[1]);
105
+ if (j > i && d > far)
106
+ [a, b, far] = [i, j, d];
107
+ }));
108
+ keep[a] = true;
109
+ keep[b] = true;
110
+ const run = (from, to, step) => {
111
+ const [p, q] = [ring[from], ring[to]];
112
+ const len = Math.hypot(q[0] - p[0], q[1] - p[1]);
113
+ let at = -1;
114
+ let most = tolerance;
115
+ for (let i = step(from); i !== to; i = step(i)) {
116
+ const r = ring[i];
117
+ const d = len < 1e-9 ? Math.hypot(r[0] - p[0], r[1] - p[1]) : Math.abs((q[0] - p[0]) * (p[1] - r[1]) - (p[0] - r[0]) * (q[1] - p[1])) / len;
118
+ if (d > most)
119
+ [at, most] = [i, d];
120
+ }
121
+ if (at >= 0) {
122
+ keep[at] = true;
123
+ run(from, at, step);
124
+ run(at, to, step);
125
+ }
126
+ };
127
+ const next = (i) => (i + 1) % ring.length;
128
+ run(a, b, next);
129
+ run(b, a, next);
130
+ return ring.filter((_, i) => keep[i]);
131
+ }
132
+ /** Whether a point is in a polygon (even-odd). */
133
+ export function inside(points, [x, y]) {
134
+ let hit = false;
135
+ for (let i = 0, j = points.length - 1; i < points.length; j = i, i += 1) {
136
+ const [xi, yi] = points[i];
137
+ const [xj, yj] = points[j];
138
+ if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)
139
+ hit = !hit;
140
+ }
141
+ return hit;
142
+ }
143
+ /** The smallest box round some points. */
144
+ export function boxOf(points) {
145
+ return { x0: Math.min(...points.map((p) => p[0])), y0: Math.min(...points.map((p) => p[1])), x1: Math.max(...points.map((p) => p[0])), y1: Math.max(...points.map((p) => p[1])) };
146
+ }
147
+ /** The distance from a point to the nearest edge of a polygon. */
148
+ export function edgeDistance(points, [x, y]) {
149
+ let best = Infinity;
150
+ points.forEach(([ax, ay], i) => {
151
+ const [bx, by] = points[(i + 1) % points.length];
152
+ const [dx, dy] = [bx - ax, by - ay];
153
+ const len = dx * dx + dy * dy;
154
+ const t = len === 0 ? 0 : Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / len));
155
+ best = Math.min(best, Math.hypot(x - (ax + t * dx), y - (ay + t * dy)));
156
+ });
157
+ return best;
158
+ }
159
+ /** How coarse the raster `coreOf` lays on the outline is, in the units of the points, and how many degrees apart the directions it tries are. */
160
+ const CELL = 2;
161
+ const STEP = 3;
162
+ /** The largest rectangle with its sides along x and y inside a polygon: width, height and the middle, found on a raster of `CELL` (the largest rectangle in each row's histogram). */
163
+ function largestRectangle(poly) {
164
+ const b = boxOf(poly);
165
+ const nx = Math.max(1, Math.ceil((b.x1 - b.x0) / CELL));
166
+ const ny = Math.max(1, Math.ceil((b.y1 - b.y0) / CELL));
167
+ const column = new Array(nx).fill(0);
168
+ let best = { area: 0, w: 0, h: 0, at: [0, 0] };
169
+ for (let j = 0; j < ny; j += 1) {
170
+ for (let i = 0; i < nx; i += 1)
171
+ column[i] = inside(poly, [b.x0 + (i + 0.5) * CELL, b.y0 + (j + 0.5) * CELL]) ? column[i] + 1 : 0;
172
+ const stack = [];
173
+ for (let i = 0; i <= nx; i += 1) {
174
+ const h = i === nx ? 0 : column[i];
175
+ let start = i;
176
+ while (stack.length > 0 && stack[stack.length - 1][1] >= h) {
177
+ const [from, height] = stack.pop();
178
+ const area = height * (i - from);
179
+ if (area > best.area)
180
+ best = { area, w: (i - from) * CELL, h: height * CELL, at: [b.x0 + (from + (i - from) / 2) * CELL, b.y0 + (j + 1 - height / 2) * CELL] };
181
+ start = from;
182
+ }
183
+ stack.push([start, h]);
184
+ }
185
+ }
186
+ return { w: best.w, h: best.h, at: best.at };
187
+ }
188
+ /**
189
+ * The main hall of a building: the largest rectangle inside its outline over every direction (`STEP` degrees apart), which does not see a wing, a plaza or a ramp that a rectangle cannot fill. Its long side is the
190
+ * building's axis, its middle the middle of the hall, and `strength` how much of a direction the building has (a round one has none). Ties between a direction and the one at a right angle go to the longer rectangle.
191
+ */
192
+ export function coreOf(points) {
193
+ const areas = [];
194
+ let best;
195
+ for (let angle = 0; angle < 180; angle += STEP) {
196
+ const a = angle * RAD;
197
+ // The outline turned so that the direction `angle` (counter-clockwise from east) lies along x.
198
+ const turned = points.map(([x, y]) => [x * Math.cos(a) + y * Math.sin(a), -x * Math.sin(a) + y * Math.cos(a)]);
199
+ const { w, h, at } = largestRectangle(turned);
200
+ areas.push(w * h);
201
+ if (best === undefined || w * h > best.area * 1.005 || (w * h >= best.area * 0.995 && w / h > best.w / best.h))
202
+ best = { area: w * h, angle, w, h, at };
203
+ }
204
+ if (best === undefined || best.area === 0)
205
+ throw new Error("a building needs room for a rectangle");
206
+ const a = best.angle * RAD;
207
+ const centre = [best.at[0] * Math.cos(a) - best.at[1] * Math.sin(a), best.at[0] * Math.sin(a) + best.at[1] * Math.cos(a)];
208
+ // The long side runs along `angle` when it is the width, and at a right angle to it when it is the height; a heading is clockwise from north.
209
+ const along = best.w >= best.h ? best.angle : best.angle + 90;
210
+ return { heading: (((90 - along) % 180) + 180) % 180, length: Math.max(best.w, best.h), width: Math.min(best.w, best.h), centre, strength: Math.max(...areas) / Math.min(...areas) };
211
+ }