@johnmorrisdotca/kyogijo-stadiums 0.57.0 → 0.58.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/NOTICE.md +2 -0
  3. package/README.md +1 -1
  4. package/catalogue.json +1 -1
  5. package/dist/areas.d.ts +8 -3
  6. package/dist/areas.js +14 -5
  7. package/dist/catalogue.data.js +1 -1
  8. package/dist/circuits-entry.d.ts +1 -1
  9. package/dist/circuits-entry.js +1 -1
  10. package/dist/circuits.draw.js +7 -4
  11. package/dist/circuits.held.d.ts +1 -0
  12. package/dist/circuits.held.js +13 -0
  13. package/dist/circuits.index.d.ts +15 -0
  14. package/dist/circuits.index.js +28 -0
  15. package/dist/circuits.race.dom.d.ts +25 -5
  16. package/dist/circuits.race.dom.js +22 -7
  17. package/dist/circuits.race.draw.d.ts +1 -1
  18. package/dist/circuits.race.draw.js +4 -4
  19. package/dist/circuits.turn.d.ts +8 -0
  20. package/dist/circuits.turn.js +29 -21
  21. package/dist/circuits.types.d.ts +2 -0
  22. package/dist/circuits.words.d.ts +4 -0
  23. package/dist/circuits.words.js +6 -0
  24. package/dist/cityctx-entry.d.ts +1 -1
  25. package/dist/cityctx-entry.js +1 -1
  26. package/dist/cityctx.d.ts +2 -2
  27. package/dist/cityctx.data.d.ts +2 -1
  28. package/dist/cityctx.data.js +3 -2
  29. package/dist/cityctx.js +9 -3
  30. package/dist/cityctx.las-vegas.data.d.ts +3 -0
  31. package/dist/cityctx.las-vegas.data.js +1387 -0
  32. package/dist/context.d.ts +11 -1
  33. package/dist/context.js +18 -4
  34. package/dist/district-entry.d.ts +1 -0
  35. package/dist/district-entry.js +2 -0
  36. package/dist/district.api.d.ts +9 -2
  37. package/dist/district.api.js +7 -1
  38. package/dist/district.circuit.d.ts +34 -0
  39. package/dist/district.circuit.js +13 -0
  40. package/dist/district.d.ts +27 -3
  41. package/dist/district.js +85 -15
  42. package/dist/district.race.d.ts +40 -0
  43. package/dist/district.race.js +60 -0
  44. package/dist/district.turn.js +27 -1
  45. package/dist/landmarks.d.ts +105 -0
  46. package/dist/landmarks.js +160 -0
  47. package/dist/turn.dom.js +5 -5
  48. package/dist/turn.style.js +1 -0
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +2 -1
package/dist/context.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type LonLat } from "./venues.geo.ts";
2
2
  import { type Plane, type Upright } from "./turn.kit.ts";
3
3
  import type { Pt } from "./crowd.ts";
4
+ import { type ContextLandmark, type PlacedLandmark } from "./landmarks.ts";
4
5
  /** One layer of an area's data: its rows, and where they came from and under what terms, since a layer has a source of its own (buildings from PLATEAU; trees and land cover from another). */
5
6
  export interface ContextLayer {
6
7
  /** What it was made from: the dataset and its page. */
@@ -44,6 +45,11 @@ export interface ContextArea {
44
45
  trees?: ContextLayer;
45
46
  landCover?: ContextLayer;
46
47
  };
48
+ /** The most of this area's buildings the 3D view raises, when that is fewer than the tier's count (`CONTEXT_BLOCKS` at its best): a city of big, many-sided buildings (the casinos of the Las Vegas Strip) raises fewer, to stay within the
49
+ * district's budget of planes. Missing: the tier's count. */
50
+ blocks?: number;
51
+ /** The area's famous landmarks, each drawn by a shape of its own and named (`landmarks.ts`): the Sphere, the High Roller and the Fountains of Bellagio round the Las Vegas Strip Circuit. Missing: none. */
52
+ landmarks?: readonly ContextLandmark[];
47
53
  }
48
54
  /** One building as the district holds it: its outline in feet in the district's frame (x to the right, y down the page), its height and the rise of its roof in feet. */
49
55
  export interface ContextBlock {
@@ -91,6 +97,10 @@ export interface ContextPlan {
91
97
  trees?: ContextTree[];
92
98
  /** The buildings of the model that are a venue (the Dome's own parts), left out: the venue draws them. */
93
99
  owned: number;
100
+ /** The landmarks of the areas in the frame, each with its own shape; missing when the areas have none. */
101
+ landmarks?: PlacedLandmark[];
102
+ /** The most buildings the areas let the 3D view raise (the least of their `blocks`); missing when none sets it. */
103
+ most?: number;
94
104
  /** The credits of the areas used, in each language. */
95
105
  attribution: {
96
106
  en: string[];
@@ -172,6 +182,6 @@ export declare function slabPlanes(ring: readonly Pt[], level: number, thickness
172
182
  side: string;
173
183
  }, look: "chosen" | "faint" | "plain"): Plane[];
174
184
  /** The blocks of the 3D view: the `most` buildings that matter most (the tallest and widest, by height times the side of their ground), each a few planes, in the order the plan lists them. */
175
- export declare function contextUprights(plan: ContextPlan, most?: number): Upright[];
185
+ export declare function contextUprights(plan: ContextPlan, asked?: number): Upright[];
176
186
  /** The building a raised venue stands in as a block: its outline in feet, its height, `xray` the middle of what is seen through its walls (none for a solid one). */
177
187
  export declare function shellBlock(key: string, ring: readonly Pt[], height: number, xray: Pt | undefined): ContextBlock;
package/dist/context.js CHANGED
@@ -6,6 +6,7 @@ import { FEET_PER_METRE } from "./geometry.js";
6
6
  import { boxOf, hullOf, inside, metresFrom } from "./venues.geo.js";
7
7
  import { n, shy, wall } from "./turn.kit.js";
8
8
  import { pointList, raked } from "./venue.geom.js";
9
+ import { landmarksFloor, ownedByLandmark, placeLandmarks, underLandmark } from "./landmarks.js";
9
10
  /** How many buildings are raised as blocks in the 3D view (the biggest and the tallest of those on the plan): the rest are the floor's own, flat. Every block is a few elements of the page. */
10
11
  export const CONTEXT_BLOCKS = 130;
11
12
  /** Feet of the plan's frame beyond the venues' buildings that a district with context shows, so that the streets round them are in it: 150 metres. */
@@ -79,6 +80,7 @@ export function placeContext(areas, origin, turn, frame, venues) {
79
80
  let owned = 0;
80
81
  const hulls = venues.map((v) => hullOf([...v.footprint]));
81
82
  const [fx0, fy0, fx1, fy1] = [frame.x, frame.y, frame.x + frame.w, frame.y + frame.h];
83
+ const landmarks = placeLandmarks(areas, origin, turn, frame);
82
84
  for (const area of areas) {
83
85
  const [east, north] = metresFrom(origin, [area.origin])[0];
84
86
  (area.layers.buildings?.rows ?? []).forEach((row, index) => {
@@ -96,6 +98,11 @@ export function placeContext(areas, origin, turn, frame, venues) {
96
98
  owned += 1;
97
99
  return;
98
100
  }
101
+ // A building that is a landmark's own (the Sphere's, drawn as a dome) is left out too.
102
+ if (ownedByLandmark(landmarks, ring)) {
103
+ owned += 1;
104
+ return;
105
+ }
99
106
  blocks.push({ id: `${area.key}-${index}`, ring, height: b.height * FEET_PER_METRE, rise: b.rise * FEET_PER_METRE, area: Math.abs(signedArea(ring)), ghost: holds, whole: box.x0 >= fx0 && box.x1 <= fx1 && box.y0 >= fy0 && box.y1 <= fy1 });
100
107
  });
101
108
  }
@@ -147,7 +154,8 @@ export function placeContext(areas, origin, turn, frame, venues) {
147
154
  // An empty credit (a layer whose credit another layer of the area has said) adds nothing.
148
155
  const layers = areas.flatMap((a) => Object.values(a.layers));
149
156
  const credits = (language) => [...new Set(layers.map((l) => l.attribution[language]).filter((text) => text !== ""))];
150
- return { blocks, streets, land, trees, owned, attribution: { en: credits("en"), ja: credits("ja") } };
157
+ const most = areas.reduce((least, a) => (a.blocks === undefined ? least : Math.min(least, a.blocks)), Infinity);
158
+ return { blocks, streets, land, trees, owned, ...(landmarks.length === 0 ? {} : { landmarks }), ...(Number.isFinite(most) ? { most } : {}), attribution: { en: credits("en"), ja: credits("ja") } };
151
159
  }
152
160
  /** A share of the trees, spread evenly over the lot: every tree at 1, none at 0, and at 0.5 every other. The same trees every time, so that a view drawn again at a tier keeps the ones it kept. */
153
161
  export function thinTrees(trees, share) {
@@ -209,12 +217,12 @@ export function treesFloor(trees) {
209
217
  export function contextFloor(plan) {
210
218
  const under = `${landFloor(plan.land ?? [])}${streetsFloor(plan.streets ?? [])}${treesFloor(plan.trees ?? [])}`;
211
219
  if (plan.blocks.length === 0)
212
- return under;
220
+ return `${under}${landmarksFloor(plan.landmarks ?? [])}`;
213
221
  const paths = [[], [], [], [], []];
214
222
  for (const b of plan.blocks)
215
223
  paths[b.ghost ? 4 : toneOf(b.height)].push(`M${b.ring.map(([x, y]) => `${num(x)} ${num(y)}`).join("L")}Z`);
216
224
  const classes = ["ky-dist-b0", "ky-dist-b1", "ky-dist-b2", "ky-dist-b3", "ky-dist-ghost"];
217
- return `${under}<g class="ky-dist-context" aria-hidden="true">${paths.map((d, i) => (d.length === 0 ? "" : `<path class="ky-dist-block ${classes[i]}" d="${d.join("")}"/>`)).join("")}</g>`;
225
+ return `${under}<g class="ky-dist-context" aria-hidden="true">${paths.map((d, i) => (d.length === 0 ? "" : `<path class="ky-dist-block ${classes[i]}" d="${d.join("")}"/>`)).join("")}</g>${landmarksFloor(plan.landmarks ?? [])}`;
218
226
  }
219
227
  // ---- The blocks of the 3D view. ----
220
228
  /** How much a building matters to the view: tall and wide first. */
@@ -368,8 +376,14 @@ export function slabPlanes(ring, level, thickness, ids, look) {
368
376
  return planes;
369
377
  }
370
378
  /** The blocks of the 3D view: the `most` buildings that matter most (the tallest and widest, by height times the side of their ground), each a few planes, in the order the plan lists them. */
371
- export function contextUprights(plan, most = CONTEXT_BLOCKS) {
379
+ export function contextUprights(plan, asked = CONTEXT_BLOCKS) {
380
+ const most = Math.min(asked, plan.most ?? Infinity);
372
381
  const wanted = new Set(plan.blocks.filter((b) => b.whole).sort((a, b) => weight(b) - weight(a) || (a.id < b.id ? -1 : 1)).slice(0, Math.max(0, most)).map((b) => b.id));
382
+ // The buildings of a named landmark stand whatever the count (at a tier that raises any).
383
+ if (most > 0)
384
+ for (const b of plan.blocks)
385
+ if (b.whole && underLandmark(plan.landmarks ?? [], b.ring))
386
+ wanted.add(b.id);
373
387
  const items = [];
374
388
  for (const b of plan.blocks) {
375
389
  if (!wanted.has(b.id))
@@ -1,4 +1,5 @@
1
1
  import "./full.ts";
2
2
  import "./venues.all.ts";
3
3
  import "./walk.full.ts";
4
+ import "./circuits.held.ts";
4
5
  export * from "./district.api.ts";
@@ -5,4 +5,6 @@ import "./full.js";
5
5
  import "./venues.all.js";
6
6
  // The walkable ground of every district, for the crowd outside, as the district has always had it (the lean district reads one when `loadOutside` is asked to).
7
7
  import "./walk.full.js";
8
+ // Every circuit, as a district's `circuit` option asks for one (the lean district reads them when `loadCircuits` is asked to).
9
+ import "./circuits.held.js";
8
10
  export * from "./district.api.js";
@@ -1,8 +1,15 @@
1
- export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE, describeDistrict, districtId, districtMember, districtRing, drawDistrict, isDistrictId, metresText, planDistrict, scaleOf, unknownVenues } from "./district.ts";
2
- export type { DistrictMember, DistrictMode, DistrictOptions, DistrictPart, DistrictPlaced, DistrictPlan, DistrictVenueInput, PlacedPart } from "./district.ts";
1
+ export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE, circuitPointIn, describeDistrict, districtId, districtMember, districtRing, drawDistrict, isDistrictId, metresText, planDistrict, scaleOf, unknownVenues } from "./district.ts";
2
+ export type { DistrictCircuit, DistrictMember, DistrictMode, DistrictOptions, DistrictPart, DistrictPlaced, DistrictPlan, DistrictVenueInput, PlacedPart } from "./district.ts";
3
3
  export { buildDistrictTurn, districtPresets, districtTurnMarkup, mountDistrictTurn } from "./district.turn.ts";
4
4
  export type { DistrictTurnSettings } from "./district.turn.ts";
5
5
  export type { ContextArea } from "./context.ts";
6
+ export { CIRCUIT_INDEX, loadCircuits } from "./circuits.index.ts";
7
+ export { circuitsHeld } from "./district.circuit.ts";
8
+ export { drawDistrictRace, mountDistrictRace } from "./district.race.ts";
9
+ export type { DistrictRaceDrawOptions, DistrictRaceOptions, DistrictRaceSettings } from "./district.race.ts";
10
+ export type { KyogijoRace, RaceState } from "./circuits.race.dom.ts";
11
+ export { LANDMARK_SHAPES } from "./landmarks.ts";
12
+ export type { ContextLandmark, LandmarkShape, PlacedLandmark } from "./landmarks.ts";
6
13
  export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_FLOOR_IDS, KODOKAN_FLOORS, KODOKAN_ROOM_HEIGHT, kodokanLevel, kodokanMember } from "./kodokan.ts";
7
14
  export type { KodokanFloor, KodokanRoom } from "./kodokan.ts";
8
15
  export { VENUE_ATTRIBUTION } from "./venues.ts";
@@ -1,6 +1,12 @@
1
1
  // A district: several real venues of one city on one plan at their real relative positions and orientations (OpenStreetMap, ODbL 1.0), in 2D and in 3D, tight round neighbours or at the real distances of a city.
2
- export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE, describeDistrict, districtId, districtMember, districtRing, drawDistrict, isDistrictId, metresText, planDistrict, scaleOf, unknownVenues } from "./district.js";
2
+ export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE, circuitPointIn, describeDistrict, districtId, districtMember, districtRing, drawDistrict, isDistrictId, metresText, planDistrict, scaleOf, unknownVenues } from "./district.js";
3
3
  export { buildDistrictTurn, districtPresets, districtTurnMarkup, mountDistrictTurn } from "./district.turn.js";
4
+ // A race circuit on the same ground (`circuit`: the district entry has every circuit, the lean district reads them with `loadCircuits`), and the famous landmarks an area of context has round it.
5
+ export { CIRCUIT_INDEX, loadCircuits } from "./circuits.index.js";
6
+ export { circuitsHeld } from "./district.circuit.js";
7
+ // A race on that circuit, among the venues and the city (the circuits' race, made-up cars or a feed's): drawn at a moment, or played in a page flat or in 3D.
8
+ export { drawDistrictRace, mountDistrictRace } from "./district.race.js";
9
+ export { LANDMARK_SHAPES } from "./landmarks.js";
4
10
  export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_FLOOR_IDS, KODOKAN_FLOORS, KODOKAN_ROOM_HEIGHT, kodokanLevel, kodokanMember } from "./kodokan.js";
5
11
  export { VENUE_ATTRIBUTION } from "./venues.js";
6
12
  export { mountDistrict } from "./district.dom.js";
@@ -0,0 +1,34 @@
1
+ import type { CircuitDrawing } from "./circuits.draw.ts";
2
+ import type { carRadius, carsMarkup } from "./circuits.race.draw.ts";
3
+ import type { mountRaceIn } from "./circuits.race.dom.ts";
4
+ import type { raceAt } from "./circuits.race.moment.ts";
5
+ import type { simulateRace } from "./circuits.race.ts";
6
+ import type { RaceWords } from "./circuits.race.words.ts";
7
+ import type { CircuitPlan } from "./circuits.ts";
8
+ import type { Circuit, CircuitOptions } from "./circuits.types.ts";
9
+ import type { CircuitWords } from "./circuits.words.ts";
10
+ import type { Language } from "./types.ts";
11
+ import type { Upright } from "./turn.kit.ts";
12
+ /** The circuits' code a district uses. */
13
+ export interface CircuitSupport {
14
+ circuitOf: (id: string) => Circuit;
15
+ circuitPlan: (circuit: Circuit) => CircuitPlan;
16
+ circuitDrawing: (options: CircuitOptions) => CircuitDrawing;
17
+ circuitUprights: (drawn: CircuitDrawing, thin: number) => Upright[];
18
+ words: Readonly<Record<Language, CircuitWords>>;
19
+ /** A race on the circuit (`mountDistrictRace`, `drawDistrictRace`): the simulation, its moments, its cars' markup and its view in a page. */
20
+ race: {
21
+ simulateRace: typeof simulateRace;
22
+ raceAt: typeof raceAt;
23
+ carsMarkup: typeof carsMarkup;
24
+ carRadius: typeof carRadius;
25
+ mountRaceIn: typeof mountRaceIn;
26
+ words: Readonly<Record<Language, RaceWords>>;
27
+ };
28
+ }
29
+ /** Fill the table: `circuits.held.ts` does it when it is imported. */
30
+ export declare function holdCircuits(support: CircuitSupport): void;
31
+ /** The circuits' code, or an error that says how to have it. */
32
+ export declare function circuitSupport(): CircuitSupport;
33
+ /** Whether the circuits are held (the district entry, or the lean district after `loadCircuits()`). */
34
+ export declare const circuitsHeld: () => boolean;
@@ -0,0 +1,13 @@
1
+ let held;
2
+ /** Fill the table: `circuits.held.ts` does it when it is imported. */
3
+ export function holdCircuits(support) {
4
+ held = support;
5
+ }
6
+ /** The circuits' code, or an error that says how to have it. */
7
+ export function circuitSupport() {
8
+ if (held === undefined)
9
+ throw new Error("a district with a circuit needs the circuits: the district entry has them, and the lean district reads them with `await loadCircuits()` first");
10
+ return held;
11
+ }
12
+ /** Whether the circuits are held (the district entry, or the lean district after `loadCircuits()`). */
13
+ export const circuitsHeld = () => held !== undefined;
@@ -3,6 +3,8 @@ import { type StadiumConfigId } from "./stadiums.ts";
3
3
  import type { OutsideOptions } from "./outside.ts";
4
4
  import type { Pt } from "./crowd.ts";
5
5
  import type { Language, SurfaceOptions } from "./types.ts";
6
+ import type { CircuitPlan } from "./circuits.ts";
7
+ import type { CircuitOptions } from "./circuits.types.ts";
6
8
  /** How a district is fitted: `neighbours` tight round venues that stand close together, `city` at the real distances between venues that are kilometres apart. */
7
9
  export type DistrictMode = "neighbours" | "city";
8
10
  export declare const DISTRICT_MODES: readonly DistrictMode[];
@@ -13,8 +15,13 @@ export type DistrictVenueInput = string | {
13
15
  };
14
16
  /** What can be asked of a district. */
15
17
  export interface DistrictOptions {
16
- /** The venues, by key: any key of `VENUES` (the arenas) and `STADIUMS` (the stadiums); at least one. */
18
+ /** The venues, by key: any key of `VENUES` (the arenas) and `STADIUMS` (the stadiums); at least one, unless the district has a `circuit`. */
17
19
  venues: readonly DistrictVenueInput[];
20
+ /**
21
+ * A race circuit laid on the same ground (a key of `CIRCUIT_IDS`: `las-vegas`): its lap, pit lane, ground and pit building where OpenStreetMap maps them, under the venues, turned as it lies; in 3D its pit building and its turns'
22
+ * numbers stand up and a lap that crosses itself stands its pass over on a bridge, as in the circuit's own view. Missing: no circuit.
23
+ */
24
+ circuit?: string;
18
25
  /** `neighbours` (the default) or `city`. */
19
26
  mode?: DistrictMode;
20
27
  /** How far the plan is turned, in degrees clockwise: 0 (the default) has north at the top; `"auto"` turns it so that the district's long way lies across the page. */
@@ -171,6 +178,19 @@ export interface DistrictGate {
171
178
  angle: number;
172
179
  basis: "documented" | "plausible";
173
180
  }
181
+ /** A circuit placed in a district: its own plan (feet round its own middle, its long side along x), and where that middle is put and how far the plan is turned to lie as it does in the world. */
182
+ export interface DistrictCircuit {
183
+ plan: CircuitPlan;
184
+ /** Where the circuit's own origin (the middle of its plan) is in the district's frame, in feet. */
185
+ at: Pt;
186
+ /** The degrees the circuit's plan is turned clockwise to lie in the district (its heading less a quarter turn, and the map's turn). */
187
+ angle: number;
188
+ /** The outline of everything it draws on the ground (its plan's box), and of its pit building, in the district's frame. */
189
+ footprint: Pt[];
190
+ building?: Pt[];
191
+ }
192
+ /** A point of a circuit's own plan (feet, `circuitPlan`) where the district puts it: for a page that draws something on the lap (a race's cars) in a district's frame. */
193
+ export declare function circuitPointIn(placed: Pick<DistrictCircuit, "at" | "angle">, p: Pt): Pt;
174
194
  /** What a district is: its venues placed, the box that fits them, and how the map lies. */
175
195
  export interface DistrictPlan {
176
196
  options: DistrictOptions;
@@ -197,8 +217,10 @@ export interface DistrictPlan {
197
217
  b: string;
198
218
  metres: number;
199
219
  }[];
200
- /** The `[longitude, latitude]` the plan's metres are measured from: the middle of the venues' surfaces. */
220
+ /** The `[longitude, latitude]` the plan's metres are measured from: the middle of the venues' surfaces (and of the circuit's plan, where there is one). */
201
221
  origin: readonly [number, number];
222
+ /** The circuit on the ground, where the options name one. */
223
+ circuit?: DistrictCircuit;
202
224
  }
203
225
  /** The margin round the buildings, as a share of the longer side of the district: tight for neighbours, wide for a city's grid and names. */
204
226
  export declare const DISTRICT_MARGIN: Readonly<Record<DistrictMode, number>>;
@@ -217,7 +239,7 @@ export declare const OUTSIDE_ROOM = 160;
217
239
  */
218
240
  export declare function planDistrict(options: DistrictOptions): DistrictPlan;
219
241
  /** The colours and the type of a district's drawing, light and dark. Defaults sit in `:where()`, so a page's own rule wins. */
220
- export declare const KYOGIJO_DISTRICT_STYLE = "\n:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8;--ky-dist-water:#c7d5dc;--ky-dist-green:#d2dcc4;--ky-dist-wood:#c3d0b6;--ky-dist-tree:#b3c2a3;--ky-dist-floor:#2f6db0}\n@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}}\n:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}\n.ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}\n.ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}\n.ky-district .ky-dist-block{stroke:var(--ky-dist-bline);stroke-width:.5;stroke-linejoin:round}\n.ky-district .ky-dist-b0{fill:var(--ky-dist-b0)}\n.ky-district .ky-dist-b1{fill:var(--ky-dist-b1)}\n.ky-district .ky-dist-b2{fill:var(--ky-dist-b2)}\n.ky-district .ky-dist-b3{fill:var(--ky-dist-b3)}\n.ky-district .ky-dist-street{stroke-linejoin:round;stroke-linecap:butt}\n.ky-district .ky-dist-street-0{stroke:var(--ky-dist-road)}\n.ky-district .ky-dist-street-1{stroke:var(--ky-dist-deck)}\n.ky-district .ky-dist-street-2{stroke:var(--ky-dist-rail);stroke-dasharray:6 3}\n.ky-district .ky-dist-street-3{stroke:var(--ky-dist-walk)}\n.ky-district .ky-dist-land{stroke:none}\n.ky-district .ky-dist-water{fill:var(--ky-dist-water)}\n.ky-district .ky-dist-green{fill:var(--ky-dist-green)}\n.ky-district .ky-dist-wood{fill:var(--ky-dist-wood)}\n.ky-district .ky-dist-tree{fill:none;stroke:var(--ky-dist-tree);stroke-linecap:round}\n.ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}\n.ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}\n.ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}\n.ky-district .ky-dist-ghost{fill:var(--ky-dist-b1);fill-opacity:.35}\n.ky-district .ky-dist-floor{stroke:var(--ky-dist-floor);stroke-width:2}\n.ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}\n.ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}";
242
+ export declare const KYOGIJO_DISTRICT_STYLE = "\n:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8;--ky-dist-water:#c7d5dc;--ky-dist-green:#d2dcc4;--ky-dist-wood:#c3d0b6;--ky-dist-tree:#b3c2a3;--ky-dist-floor:#2f6db0}\n@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}}\n:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}\n.ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}\n.ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}\n.ky-district .ky-dist-block{stroke:var(--ky-dist-bline);stroke-width:.5;stroke-linejoin:round}\n.ky-district .ky-dist-b0{fill:var(--ky-dist-b0)}\n.ky-district .ky-dist-b1{fill:var(--ky-dist-b1)}\n.ky-district .ky-dist-b2{fill:var(--ky-dist-b2)}\n.ky-district .ky-dist-b3{fill:var(--ky-dist-b3)}\n.ky-district .ky-dist-street{stroke-linejoin:round;stroke-linecap:butt}\n.ky-district .ky-dist-street-0{stroke:var(--ky-dist-road)}\n.ky-district .ky-dist-street-1{stroke:var(--ky-dist-deck)}\n.ky-district .ky-dist-street-2{stroke:var(--ky-dist-rail);stroke-dasharray:6 3}\n.ky-district .ky-dist-street-3{stroke:var(--ky-dist-walk)}\n.ky-district .ky-dist-land{stroke:none}\n.ky-district .ky-dist-water{fill:var(--ky-dist-water)}\n.ky-district .ky-dist-green{fill:var(--ky-dist-green)}\n.ky-district .ky-dist-wood{fill:var(--ky-dist-wood)}\n.ky-district .ky-dist-tree{fill:none;stroke:var(--ky-dist-tree);stroke-linecap:round}\n.ky-district .ky-dist-jet{fill:none;stroke:var(--ky-dist-halo);stroke-linecap:round}\n.ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}\n.ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}\n.ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}\n.ky-district .ky-dist-ghost{fill:var(--ky-dist-b1);fill-opacity:.35}\n.ky-district .ky-dist-floor{stroke:var(--ky-dist-floor);stroke-width:2}\n.ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}\n.ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}";
221
243
  /**
222
244
  * A venue's drawing as a part of the district's: its root made a nested `svg` that keeps the class, the sport and the colours the drawing's stylesheet reads, its box laid in its own frame (so that the group's turn
223
245
  * and shift put it in place), and its title, description, apron, arrow and stylesheet taken out (the stylesheet is given back in `styles`).
@@ -263,5 +285,7 @@ export declare function drawMember(member: DistrictMember, options: DistrictOpti
263
285
  * ```
264
286
  */
265
287
  export declare function drawDistrict(options: DistrictOptions, plan?: DistrictPlan, drawing?: (member: DistrictMember, index: number, part: number) => string): string;
288
+ /** The options a district's circuit is drawn from: the circuit's own drawing in the district's language and theme, without its frame, and for the floor of the 3D view without its turns' numbers (they stand up). */
289
+ export declare function circuitInDistrict(options: DistrictOptions, id: string, floor: boolean): CircuitOptions;
266
290
  /** The venues a district was asked for that the package has no key for: none when it can be drawn. */
267
291
  export declare const unknownVenues: (venues: readonly DistrictVenueInput[]) => string[];
package/dist/district.js CHANGED
@@ -16,6 +16,7 @@ import { VENUE_ATTRIBUTION, VENUE_IDS, venueById } from "./venues.js";
16
16
  import { hullOf, metresFrom, middleOf, smallestBox } from "./venues.geo.js";
17
17
  import { venueGates } from "./gates.js";
18
18
  import { walkIdFor } from "./walk.js";
19
+ import { circuitSupport } from "./district.circuit.js";
19
20
  export const DISTRICT_MODES = ["neighbours", "city"];
20
21
  const venueMember = (venue, language) => {
21
22
  const arena = heldArena(venue.id);
@@ -111,6 +112,11 @@ export function districtRing(input) {
111
112
  const RAD = Math.PI / 180;
112
113
  /** Turn a point of the map (east, down the page) by `degrees` clockwise on the page. */
113
114
  export const turnBy = ([x, y], degrees) => [x * Math.cos(degrees * RAD) - y * Math.sin(degrees * RAD), x * Math.sin(degrees * RAD) + y * Math.cos(degrees * RAD)];
115
+ /** A point of a circuit's own plan (feet, `circuitPlan`) where the district puts it: for a page that draws something on the lap (a race's cars) in a district's frame. */
116
+ export function circuitPointIn(placed, p) {
117
+ const [x, y] = turnBy(p, placed.angle);
118
+ return [x + placed.at[0], y + placed.at[1]];
119
+ }
114
120
  /** The margin round the buildings, as a share of the longer side of the district: tight for neighbours, wide for a city's grid and names. */
115
121
  export const DISTRICT_MARGIN = { neighbours: 0.09, city: 0.12 };
116
122
  /** The ground kept round the buildings for the crowd outside, in metres, when a district asks for one. */
@@ -133,8 +139,10 @@ const haversine = (a, b) => {
133
139
  * ```
134
140
  */
135
141
  export function planDistrict(options) {
136
- if (options.venues.length === 0)
137
- throw new Error("a district needs a venue");
142
+ // (An unknown circuit throws here, naming the keys.)
143
+ const circuit = options.circuit === undefined ? undefined : circuitSupport().circuitOf(options.circuit);
144
+ if (options.venues.length === 0 && circuit === undefined)
145
+ throw new Error("a district needs a venue or a circuit");
138
146
  const language = options.language === "ja" ? "ja" : "en";
139
147
  const mode = options.mode === "city" ? "city" : "neighbours";
140
148
  const members = options.venues.map((v) => districtMember(v, language, options.floor));
@@ -144,15 +152,23 @@ export function planDistrict(options) {
144
152
  throw new Error(`${m.key} is in the district twice`);
145
153
  keys.add(m.key);
146
154
  }
147
- const origin = middleOf(members.map((m) => m.centre));
155
+ const lap = circuit === undefined ? undefined : circuitSupport().circuitPlan(circuit);
156
+ const origin = middleOf([...members.map((m) => m.centre), ...(lap === undefined ? [] : [lap.origin])]);
148
157
  const ground = (ring) => metresFrom(origin, ring).map(([east, north]) => [east * FEET_PER_METRE, -north * FEET_PER_METRE]);
149
158
  const raw = members.map((m) => ({ at: ground([m.centre])[0], footprint: ground(m.ring) }));
150
- // The map's turn: as asked, or the one that lays the district's long way across the page (the smallest box round every building).
151
- const all = raw.flatMap((r) => r.footprint);
159
+ // The circuit, unturned: its plan's middle on the ground, and its own frame turned back to east and down the page (its plan lies with its long side along x, turned `90 - heading` from the map).
160
+ const lapBox = lap === undefined ? [] : [[lap.box.x0, lap.box.y0], [lap.box.x1, lap.box.y0], [lap.box.x1, lap.box.y1], [lap.box.x0, lap.box.y1]];
161
+ const lapAt = lap === undefined ? undefined : ground([lap.origin])[0];
162
+ const lapGround = (p) => {
163
+ const [x, y] = turnBy(p, lap.heading - 90);
164
+ return [x + lapAt[0], y + lapAt[1]];
165
+ };
166
+ // The map's turn: as asked, or the one that lays the district's long way across the page (the smallest box round every building, and the circuit's ground).
167
+ const all = [...raw.flatMap((r) => r.footprint), ...lapBox.map(lapGround)];
152
168
  let turn = 0;
153
169
  if (typeof options.turn === "number" && Number.isFinite(options.turn))
154
170
  turn = ((options.turn % 360) + 360) % 360;
155
- else if (options.turn === "auto" && members.length > 1) {
171
+ else if (options.turn === "auto" && members.length + (lap === undefined ? 0 : 1) > 1) {
156
172
  // `smallestBox` reads its points as east and north; these are east and down the page, so its heading is 90 degrees less the long side's angle on the page, and turning the map by (heading - 90) lays that side along x.
157
173
  const box = smallestBox(all);
158
174
  turn = (((box.heading - 90) % 360) + 360) % 360;
@@ -163,8 +179,17 @@ export function planDistrict(options) {
163
179
  const parts = (member.parts ?? [{ key: member.key, options: member.options, centre: member.centre, heading: member.heading, ...(member.elevation === undefined ? {} : { elevation: member.elevation }) }]).map((part) => ({ part, at: turnBy(ground([part.centre])[0], turn), angle: part.heading - 90 + turn }));
164
180
  return { member, parts, at: turnBy(raw[i].at, turn), angle: member.heading - 90 + turn, footprint, hull: hullOf(footprint) };
165
181
  });
166
- const xs = placed.flatMap((p) => p.footprint.map((q) => q[0]));
167
- const ys = placed.flatMap((p) => p.footprint.map((q) => q[1]));
182
+ const placedCircuit = lap === undefined
183
+ ? undefined
184
+ : (() => {
185
+ const at = turnBy(lapAt, turn);
186
+ const angle = lap.heading - 90 + turn;
187
+ const put = (p) => circuitPointIn({ at, angle }, p);
188
+ return { plan: lap, at, angle, footprint: lapBox.map(put), ...(lap.building === undefined ? {} : { building: lap.building.ring.map(put) }) };
189
+ })();
190
+ const outlines = [...placed.map((p) => p.footprint), ...(placedCircuit === undefined ? [] : [placedCircuit.footprint])];
191
+ const xs = outlines.flatMap((f) => f.map((q) => q[0]));
192
+ const ys = outlines.flatMap((f) => f.map((q) => q[1]));
168
193
  const [x0, x1, y0, y1] = [Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys)];
169
194
  const context = options.context !== undefined && options.context.length > 0;
170
195
  // A district that asks for a crowd outside (and has a mask to put it on) keeps room round its buildings for it, whatever the level, so that the plan stays where it is as the level changes.
@@ -177,9 +202,11 @@ export function planDistrict(options) {
177
202
  const view = { x: framed.x0, y: framed.y0, w: framed.x1 - framed.x0, h: framed.y1 - framed.y0 };
178
203
  const between = [];
179
204
  members.forEach((a, i) => members.slice(i + 1).forEach((b) => between.push({ a: a.key, b: b.key, metres: Math.round(haversine(a.centre, b.centre) * 10) / 10 })));
180
- const around = context ? placeContext(options.context, origin, turn, view, placed.map((p) => ({ footprint: p.footprint, shell: p.member.shell !== undefined }))) : undefined;
205
+ // (The circuit's pit building is the circuit's: a building of the context that is the same one is left out. Its ground and its lap are not buildings, and every building on them stays.)
206
+ const owners = [...placed.map((p) => ({ footprint: p.footprint, shell: p.member.shell !== undefined })), ...(placedCircuit?.building === undefined ? [] : [{ footprint: placedCircuit.building }])];
207
+ const around = context ? placeContext(options.context, origin, turn, view, owners) : undefined;
181
208
  const gates = options.gates !== true ? [] : placed.flatMap((p) => venueGates(p.member.id).map((g) => ({ key: g.key, venue: p.member.key, name: g.name, at: turnBy(ground([g.at])[0], turn), angle: ((g.bearing + turn) % 360 + 360) % 360, basis: g.basis })));
182
- return { options, mode, turn, north: turn, members: placed, origin, context: around, gates, view, between };
209
+ return { options, mode, turn, north: turn, members: placed, origin, context: around, gates, view, between, ...(placedCircuit === undefined ? {} : { circuit: placedCircuit }) };
183
210
  }
184
211
  // ---- The drawing. ----
185
212
  /** The colours and the type of a district's drawing, light and dark. Defaults sit in `:where()`, so a page's own rule wins. */
@@ -204,6 +231,7 @@ export const KYOGIJO_DISTRICT_STYLE = `
204
231
  .ky-district .ky-dist-green{fill:var(--ky-dist-green)}
205
232
  .ky-district .ky-dist-wood{fill:var(--ky-dist-wood)}
206
233
  .ky-district .ky-dist-tree{fill:none;stroke:var(--ky-dist-tree);stroke-linecap:round}
234
+ .ky-district .ky-dist-jet{fill:none;stroke:var(--ky-dist-halo);stroke-linecap:round}
207
235
  .ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}
208
236
  .ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}
209
237
  .ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}
@@ -260,19 +288,25 @@ export function scaleOf(view) {
260
288
  /** The distance a scale says, as it is read: metres under a kilometre, else kilometres. */
261
289
  export const metresText = (metres) => (metres >= 1000 ? `${metres / 1000} km` : `${metres} m`);
262
290
  /** The id of a district's drawing: the option's, or a hash of the options. */
263
- export const districtId = (options) => (typeof options.id === "string" && options.id !== "" ? options.id : `ky-district-${hash(JSON.stringify([options.venues, options.mode, options.turn, options.language, options.theme, options.stands, ...(options.context === undefined || options.context.length === 0 ? [] : [options.context.map((a) => a.key)]), ...(options.building === undefined ? [] : [options.building]), ...(options.floor === undefined ? [] : [`floor${options.floor}`])]))}`);
291
+ export const districtId = (options) => (typeof options.id === "string" && options.id !== "" ? options.id : `ky-district-${hash(JSON.stringify([options.venues, options.mode, options.turn, options.language, options.theme, options.stands, ...(options.context === undefined || options.context.length === 0 ? [] : [options.context.map((a) => a.key)]), ...(options.building === undefined ? [] : [options.building]), ...(options.floor === undefined ? [] : [`floor${options.floor}`]), ...(options.circuit === undefined ? [] : [`circuit:${options.circuit}`])]))}`);
264
292
  /** The title and the description of a district, in its language: what is drawn, where each venue stands from the first, how far apart they are, and the credit. */
265
293
  export function describeDistrict(options, plan = planDistrict(options)) {
266
294
  const language = options.language === "ja" ? "ja" : "en";
267
- const names = plan.members.map((p) => p.member.name[language]);
295
+ const lap = plan.circuit?.plan.circuit;
296
+ const names = [...(lap === undefined ? [] : [lap.names[language]]), ...plan.members.map((p) => p.member.name[language])];
268
297
  const words = KYOGIJO_STRINGS[language].said;
269
298
  const list = language === "ja" ? (names.length > 1 ? `${names.slice(0, -1).join("、")}と${names[names.length - 1]}` : names[0]) : names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : names[0];
270
- const title = language === "ja" ? `${list}の位置関係` : `${list}: where they stand`;
299
+ const title = plan.members.length === 0 && lap !== undefined ? fillWords(circuitSupport().words[language].districtTitle, { name: lap.names[language] }) : language === "ja" ? `${list}の位置関係` : `${list}: where they stand`;
271
300
  const gap = language === "ja" ? "" : " ";
272
301
  const first = plan.between[0];
273
302
  const nameOf = (key) => plan.members.find((p) => p.member.key === key).member.name[language];
274
303
  const apart = first === undefined ? "" : language === "ja" ? `${gap}${nameOf(first.a)}と${nameOf(first.b)}の中心は約${Math.round(first.metres)}m離れています。` : `${gap}The middles of ${nameOf(first.a)} and ${nameOf(first.b)} are about ${Math.round(first.metres)} metres apart.`;
275
- const text = fillWords(words.district, { count: String(plan.members.length), mode: words[plan.mode === "city" ? "districtCity" : "districtNeighbours"] });
304
+ const cw = lap === undefined ? undefined : circuitSupport().words[language];
305
+ const km = lap === undefined ? "" : (lap.length.value / 1000).toLocaleString(language === "ja" ? "ja-JP" : "en-US", { minimumFractionDigits: 3, maximumFractionDigits: 3 });
306
+ // A district of a circuit alone says what the circuit's own plan does; with venues, the venues' sentence, then the circuit's.
307
+ const text = plan.members.length === 0 && lap !== undefined
308
+ ? fillWords(cw.districtAlone, { name: lap.names[language], km })
309
+ : `${fillWords(words.district, { count: String(plan.members.length), mode: words[plan.mode === "city" ? "districtCity" : "districtNeighbours"] })}${lap === undefined ? "" : `${gap}${fillWords(cw.inDistrict, { name: lap.names[language], km })}`}`;
276
310
  const level = options.outside?.level ?? 0;
277
311
  const standing = level > 0 ? Math.round(plan.members.filter((p) => walkIdFor([p.member.id]) !== undefined).reduce((sum, p) => sum + p.member.seats, 0) * level / 100) : 0;
278
312
  const crowd = standing > 0 ? `${gap}${fillWords(words.districtOutside, { percent: String(level), people: String(standing).replace(/\B(?=(\d{3})+(?!\d))/g, ",") })}` : "";
@@ -342,6 +376,14 @@ export function drawDistrict(options, plan = planDistrict(options), drawing) {
342
376
  }
343
377
  }
344
378
  const styles = [];
379
+ // The circuit, on the ground under the venues: its own drawing without its frame (no ground, name, arrow, scale or credit), turned and put where it lies.
380
+ if (plan.circuit !== undefined) {
381
+ const { markup, styles: own } = nestedDrawing(circuitSupport().circuitDrawing(circuitInDistrict(options, id, drawing !== undefined)).svg);
382
+ for (const block of own)
383
+ if (!styles.includes(block))
384
+ styles.push(block);
385
+ parts.push(`<g class="ky-dist-circuit" data-circuit="${esc(plan.circuit.plan.circuit.id)}" transform="translate(${num(plan.circuit.at[0])} ${num(plan.circuit.at[1])}) rotate(${num(plan.circuit.angle)})">${markup}</g>`);
386
+ }
345
387
  plan.members.forEach((placed, i) => {
346
388
  placed.parts.forEach((pp, j) => {
347
389
  const svg = drawing === undefined ? drawMember(placed.member, options, i, id, {}, j) : drawing(placed.member, i, j);
@@ -363,6 +405,18 @@ export function drawDistrict(options, plan = planDistrict(options), drawing) {
363
405
  }
364
406
  if (labels) {
365
407
  const size = diag * (plan.mode === "city" ? 0.018 : 0.026);
408
+ // The landmarks' names, smaller, under each.
409
+ for (const l of plan.context?.landmarks ?? []) {
410
+ const bottom = Math.max(...l.ring.map((p) => p[1]));
411
+ parts.push(`<text class="ky-dist-text" data-name="${esc(l.key)}" x="${num(l.label[0])}" y="${num(l.shape === "named" ? l.label[1] : bottom + size)}" text-anchor="middle" font-size="${num(size * 0.72)}" stroke-width="${num(size * 0.16)}">${esc(l.name[language])}</text>`);
412
+ }
413
+ if (plan.circuit !== undefined) {
414
+ // The circuit's name under its ground, as a venue's is under its building.
415
+ const bottom = Math.max(...plan.circuit.footprint.map((p) => p[1]));
416
+ const x = plan.circuit.footprint.reduce((sum, p) => sum + p[0], 0) / plan.circuit.footprint.length;
417
+ const y = Math.min(bottom + size * 1.3, plan.view.y + plan.view.h - size * 0.4);
418
+ parts.push(`<text class="ky-dist-text" data-name="circuit" x="${num(x)}" y="${num(y)}" text-anchor="middle" font-size="${num(size)}" stroke-width="${num(size * 0.22)}">${esc(plan.circuit.plan.circuit.names[language])}</text>`);
419
+ }
366
420
  plan.members.forEach((placed) => {
367
421
  const bottom = Math.max(...placed.footprint.map((p) => p[1]));
368
422
  const x = placed.footprint.reduce((s, p) => s + p[0], 0) / placed.footprint.length;
@@ -380,11 +434,27 @@ export function drawDistrict(options, plan = planDistrict(options), drawing) {
380
434
  parts.push(`<g class="ky-dist-north" aria-hidden="true"><path class="ky-dist-ink" stroke-width="${num(arrow * 0.06)}" d="M${num(ax + tail[0])} ${num(ay + tail[1])}L${num(ax + tip[0])} ${num(ay + tip[1])}M${num(ax + left[0])} ${num(ay + left[1])}L${num(ax + tip[0])} ${num(ay + tip[1])}L${num(ax + right[0])} ${num(ay + right[1])}"/><text class="ky-dist-text" x="${num(ax + nPos[0])}" y="${num(ay + nPos[1] + arrow * 0.18)}" text-anchor="middle" font-size="${num(arrow * 0.5)}" stroke-width="${num(arrow * 0.1)}">N</text></g>`);
381
435
  const metres = scaleOf(view);
382
436
  const bar = metres * FEET_PER_METRE;
383
- const [sx, sy] = [view.x + diag * 0.03, view.y + view.h - diag * 0.04];
437
+ // At the bottom left, unless a venue or its name is there (T-Mobile Arena, at the south-west corner of the Las Vegas district): then at the bottom right.
438
+ const named = (placed) => [...placed.footprint, ...(labels ? [[placed.footprint.reduce((sum, p) => sum + p[0], 0) / placed.footprint.length, Math.max(...placed.footprint.map((p) => p[1])) + diag * 0.03]] : [])];
439
+ const corner = { x0: view.x, x1: view.x + diag * 0.06 + bar, y0: view.y + view.h - diag * 0.08 };
440
+ const crowded = plan.members.some((placed) => named(placed).some(([x, y]) => x >= corner.x0 && x <= corner.x1 && y >= corner.y0));
441
+ const [sx, sy] = [crowded ? view.x + view.w - diag * 0.03 - bar : view.x + diag * 0.03, view.y + view.h - diag * 0.04];
384
442
  parts.push(`<g class="ky-dist-scale" aria-hidden="true"><path class="ky-dist-ink" stroke-width="${num(diag * 0.004)}" d="M${num(sx)} ${num(sy - diag * 0.01)}V${num(sy)}H${num(sx + bar)}V${num(sy - diag * 0.01)}"/><text class="ky-dist-text" x="${num(sx + bar / 2)}" y="${num(sy - diag * 0.018)}" text-anchor="middle" font-size="${num(diag * 0.022)}" stroke-width="${num(diag * 0.005)}">${metresText(metres)}</text></g>`);
385
443
  const sheet = options.style === "none" ? "" : `<style>${KYOGIJO_DISTRICT_STYLE}</style>${styles.join("")}`;
386
444
  const theme = options.theme === "light" || options.theme === "dark" ? ` data-theme="${options.theme}"` : "";
387
445
  return `<svg xmlns="http://www.w3.org/2000/svg" class="kyogijo ky-district" viewBox="${num(view.x)} ${num(view.y)} ${num(view.w)} ${num(view.h)}" role="img" aria-labelledby="${id}-t ${id}-d" data-sport="district" data-mode="${plan.mode}"${(options.outside?.level ?? 0) > 0 ? ` data-outside="${options.outside?.level}"` : ""}${theme} lang="${language}"><title id="${id}-t">${esc(title)}</title><desc id="${id}-d">${esc(description)}</desc>${sheet}${parts.join("")}</svg>`;
388
446
  }
447
+ /** The options a district's circuit is drawn from: the circuit's own drawing in the district's language and theme, without its frame, and for the floor of the 3D view without its turns' numbers (they stand up). */
448
+ export function circuitInDistrict(options, id, floor) {
449
+ return {
450
+ circuit: options.circuit,
451
+ language: options.language === "ja" ? "ja" : "en",
452
+ ...(options.theme === undefined ? {} : { theme: options.theme }),
453
+ id: `${id}-ci`,
454
+ label: false,
455
+ frame: false,
456
+ ...(floor ? { turns: false } : {}),
457
+ };
458
+ }
389
459
  /** The venues a district was asked for that the package has no key for: none when it can be drawn. */
390
460
  export const unknownVenues = (venues) => venues.map((v) => (typeof v === "string" ? v.split(":")[0] : v.id)).filter((id) => !isDistrictId(id));
@@ -0,0 +1,40 @@
1
+ import type { KyogijoRace, RaceViewSettings } from "./circuits.race.dom.ts";
2
+ import type { CircuitRace, RaceOptions } from "./circuits.race.ts";
3
+ import { type DistrictOptions } from "./district.ts";
4
+ import { type DistrictTurnSettings } from "./district.turn.ts";
5
+ /** What a race on a district's ground is drawn from: the district's options, its `circuit` named, and the race's (`seed`, `laps`, `cars`). */
6
+ export type DistrictRaceOptions = DistrictOptions & {
7
+ circuit: string;
8
+ } & Omit<RaceOptions, "circuit">;
9
+ /** A race on a district's ground at a moment: the district's and the race's options, and the seconds from the start (0, the grid, by default). */
10
+ export type DistrictRaceDrawOptions = DistrictRaceOptions & {
11
+ t?: number;
12
+ };
13
+ /** How a race on a district's ground is shown and played: as a circuit's race, with the district's 3D view's settings (its viewpoint, the walls and roofs of its venues, its quality). */
14
+ export type DistrictRaceSettings = RaceViewSettings & {
15
+ turn?: DistrictTurnSettings;
16
+ };
17
+ /**
18
+ * A race on a district's ground at a moment, as one SVG string: the district's plan with a dot for each car where the race has it then, on the circuit where the district lays it, in its team's colour with the position it holds
19
+ * written on it. The race is the simulation the options make (made-up cars, from a seed), or the one given (from a feed, `raceFromFeed`).
20
+ * @example
21
+ * ```ts
22
+ * import { drawDistrictRace } from "@johnmorrisdotca/kyogijo-stadiums/district";
23
+ * const svg = drawDistrictRace({ venues: ["t-mobile-arena"], circuit: "las-vegas", laps: 3, cars: 6, t: 120 });
24
+ * console.log(svg.match(/class="ky-ci-car"/g)!.length, svg.includes('data-circuit="las-vegas"'));
25
+ * // 6 true
26
+ * ```
27
+ */
28
+ export declare function drawDistrictRace(options: DistrictRaceDrawOptions, given?: CircuitRace): string;
29
+ /**
30
+ * Mount a race on a district's ground in a page: the district flat (or in 3D, with its venues, its landmarks and the circuit's pit building standing) with a dot for each car on the circuit, and the race's clock, controls and
31
+ * timing board, as `mountCircuitRace` has them. It does nothing until it is played, and nothing while it is paused, hidden or out of sight; in 3D the view's turn and the race share one frame. The lean district reads the
32
+ * circuits first (`await loadCircuits()`), with the venues' records and the area round them as for any district.
33
+ * @example
34
+ * ```ts no-run
35
+ * import { mountDistrictRace } from "@johnmorrisdotca/kyogijo-stadiums/district";
36
+ * const race = mountDistrictRace(document.querySelector("#strip")!, { venues: ["t-mobile-arena"], circuit: "las-vegas", labels: true }, { view: "3d", speed: 10 });
37
+ * race.play();
38
+ * ```
39
+ */
40
+ export declare function mountDistrictRace(host: HTMLElement, options: DistrictRaceOptions, settings?: DistrictRaceSettings): KyogijoRace<DistrictRaceOptions, DistrictOptions>;
@@ -0,0 +1,60 @@
1
+ import { circuitSupport } from "./district.circuit.js";
2
+ import { circuitPointIn, drawDistrict, planDistrict } from "./district.js";
3
+ import { buildDistrictTurn } from "./district.turn.js";
4
+ import { esc } from "./format.js";
5
+ import { fill } from "./strings.core.js";
6
+ /** Where a point of the circuit's plan lies on a district's ground. */
7
+ function placer(plan) {
8
+ const placed = plan.circuit;
9
+ if (placed === undefined)
10
+ throw new Error("a race on a district needs the district's `circuit`");
11
+ return (p) => circuitPointIn(placed, p);
12
+ }
13
+ /** The size of the cars' dots on a district: a thousandth of its longer side, as on a circuit's own drawing, so that they read as large on the page. */
14
+ const sizeOf = (plan) => Math.max(plan.view.w, plan.view.h) / 1000;
15
+ /**
16
+ * A race on a district's ground at a moment, as one SVG string: the district's plan with a dot for each car where the race has it then, on the circuit where the district lays it, in its team's colour with the position it holds
17
+ * written on it. The race is the simulation the options make (made-up cars, from a seed), or the one given (from a feed, `raceFromFeed`).
18
+ * @example
19
+ * ```ts
20
+ * import { drawDistrictRace } from "@johnmorrisdotca/kyogijo-stadiums/district";
21
+ * const svg = drawDistrictRace({ venues: ["t-mobile-arena"], circuit: "las-vegas", laps: 3, cars: 6, t: 120 });
22
+ * console.log(svg.match(/class="ky-ci-car"/g)!.length, svg.includes('data-circuit="las-vegas"'));
23
+ * // 6 true
24
+ * ```
25
+ */
26
+ export function drawDistrictRace(options, given) {
27
+ const { simulateRace, raceAt, carsMarkup, carRadius, words } = circuitSupport().race;
28
+ const language = options.language === "ja" ? "ja" : "en";
29
+ const plan = planDistrict(options);
30
+ const put = placer(plan);
31
+ const race = given ?? simulateRace(options);
32
+ const moment = raceAt(race, options.t ?? 0);
33
+ const onGround = { ...moment, standings: moment.standings.map((line) => ({ ...line, at: put(line.at) })) };
34
+ const w = words[language];
35
+ const extra = fill(given === undefined ? w.description : w.fromFeed, { cars: race.cars.length, laps: race.laps });
36
+ return drawDistrict(options, plan)
37
+ .replace(/<desc id="([^"]+)">([^<]*)<\/desc>/, (_, id, text) => `<desc id="${id}">${text}${language === "ja" ? "" : " "}${esc(extra)}</desc>`)
38
+ .replace(/<\/svg>$/, `${carsMarkup(onGround, carRadius(sizeOf(plan)), language)}</svg>`);
39
+ }
40
+ /**
41
+ * Mount a race on a district's ground in a page: the district flat (or in 3D, with its venues, its landmarks and the circuit's pit building standing) with a dot for each car on the circuit, and the race's clock, controls and
42
+ * timing board, as `mountCircuitRace` has them. It does nothing until it is played, and nothing while it is paused, hidden or out of sight; in 3D the view's turn and the race share one frame. The lean district reads the
43
+ * circuits first (`await loadCircuits()`), with the venues' records and the area round them as for any district.
44
+ * @example
45
+ * ```ts no-run
46
+ * import { mountDistrictRace } from "@johnmorrisdotca/kyogijo-stadiums/district";
47
+ * const race = mountDistrictRace(document.querySelector("#strip")!, { venues: ["t-mobile-arena"], circuit: "las-vegas", labels: true }, { view: "3d", speed: 10 });
48
+ * race.play();
49
+ * ```
50
+ */
51
+ export function mountDistrictRace(host, options, settings = {}) {
52
+ const support = circuitSupport();
53
+ const scene = {
54
+ flat: (o, t, given) => drawDistrictRace({ ...o, t }, given),
55
+ view: (o) => o,
56
+ build: buildDistrictTurn,
57
+ point: (o) => placer(planDistrict(o)),
58
+ };
59
+ return support.race.mountRaceIn(host, options, scene, settings);
60
+ }