@johnmorrisdotca/kyogijo-stadiums 0.57.0 → 0.59.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 (62) hide show
  1. package/CHANGELOG.md +14 -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/athletics/plan.d.ts +2 -0
  8. package/dist/athletics/plan.js +20 -4
  9. package/dist/catalogue.data.js +1 -1
  10. package/dist/circuits-entry.d.ts +1 -1
  11. package/dist/circuits-entry.js +1 -1
  12. package/dist/circuits.draw.js +7 -4
  13. package/dist/circuits.held.d.ts +1 -0
  14. package/dist/circuits.held.js +13 -0
  15. package/dist/circuits.index.d.ts +15 -0
  16. package/dist/circuits.index.js +28 -0
  17. package/dist/circuits.race.dom.d.ts +25 -5
  18. package/dist/circuits.race.dom.js +22 -7
  19. package/dist/circuits.race.draw.d.ts +1 -1
  20. package/dist/circuits.race.draw.js +4 -4
  21. package/dist/circuits.turn.d.ts +8 -0
  22. package/dist/circuits.turn.js +29 -21
  23. package/dist/circuits.types.d.ts +2 -0
  24. package/dist/circuits.words.d.ts +4 -0
  25. package/dist/circuits.words.js +6 -0
  26. package/dist/cityctx-entry.d.ts +1 -1
  27. package/dist/cityctx-entry.js +1 -1
  28. package/dist/cityctx.d.ts +2 -2
  29. package/dist/cityctx.data.d.ts +2 -1
  30. package/dist/cityctx.data.js +3 -2
  31. package/dist/cityctx.js +9 -3
  32. package/dist/cityctx.las-vegas.data.d.ts +3 -0
  33. package/dist/cityctx.las-vegas.data.js +1387 -0
  34. package/dist/context.d.ts +11 -1
  35. package/dist/context.js +18 -4
  36. package/dist/crowd.d.ts +18 -0
  37. package/dist/crowd.js +88 -18
  38. package/dist/district-entry.d.ts +1 -0
  39. package/dist/district-entry.js +2 -0
  40. package/dist/district.api.d.ts +9 -2
  41. package/dist/district.api.js +7 -1
  42. package/dist/district.circuit.d.ts +34 -0
  43. package/dist/district.circuit.js +13 -0
  44. package/dist/district.d.ts +27 -3
  45. package/dist/district.js +85 -15
  46. package/dist/district.race.d.ts +40 -0
  47. package/dist/district.race.js +60 -0
  48. package/dist/district.turn.js +27 -1
  49. package/dist/draw.js +22 -7
  50. package/dist/landmarks.d.ts +105 -0
  51. package/dist/landmarks.js +160 -0
  52. package/dist/surfaces/artistic.js +3 -1
  53. package/dist/surfaces/athletics.js +2 -0
  54. package/dist/surfaces/hockey.js +3 -1
  55. package/dist/surfaces/surface.d.ts +7 -0
  56. package/dist/surfaces/surface.js +9 -0
  57. package/dist/turn.dom.js +5 -5
  58. package/dist/turn.markup.js +12 -1
  59. package/dist/turn.style.js +1 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +2 -1
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
+ }
@@ -2,7 +2,9 @@
2
2
  // (`buildTurn`: the stands, the walls and the roof, the goals and posts of its field), its planes are turned about the middle of its surface and moved to where the plan puts it, and the floor is the district's flat
3
3
  // plan drawn with each venue's building only. The model it makes is a `TurnModel`, so the camera, the controls, the pointer, the auto-turn and the steady fit of every other 3D view are this one's; its
4
4
  // buildings are stacked building by building (`Plane.group`), the far one first, so that a roof never shows through a nearer building.
5
- import { describeDistrict, districtId, drawDistrict, drawMember, nestedDrawing, planDistrict, surfaceCentre } from "./district.js";
5
+ import { circuitInDistrict, circuitPointIn, describeDistrict, districtId, drawDistrict, drawMember, nestedDrawing, planDistrict, surfaceCentre } from "./district.js";
6
+ import { circuitSupport } from "./district.circuit.js";
7
+ import { landmarkUprights } from "./landmarks.js";
6
8
  import { blockPlanes, contextUprights, shellBlock, slabPlanes, thinTrees } from "./context.js";
7
9
  import { TIER_PROFILES, tierOf } from "./quality.js";
8
10
  import { dojo } from "./budo/dojo.api.js";
@@ -193,10 +195,27 @@ export function buildDistrictTurn(options, settings = {}) {
193
195
  items.push({ id: `${prefix}plate-name`, kind: "scoreboard", at: placed.at, height: level + size * 1.7, planes: [sign(`${prefix}plate-name`, label, placed.at, level + size * 0.4, size, i)] });
194
196
  }
195
197
  });
198
+ // The circuit's pit building and its turns' numbers, standing where the district puts the circuit, apart from every venue's group.
199
+ if (plan.circuit !== undefined) {
200
+ const placed = plan.circuit;
201
+ const { circuitUprights, circuitDrawing } = circuitSupport();
202
+ for (const item of circuitUprights(circuitDrawing(circuitInDistrict(options, base, true)), Math.hypot(plan.view.w, plan.view.h) * 0.0012)) {
203
+ items.push({ ...item, id: `ci-${item.id}`, at: circuitPointIn(placed, [item.at[0], item.at[1]]), planes: item.planes.map((p) => placeInDistrict(p, placed, [0, 0], "ci-", APART)) });
204
+ }
205
+ }
196
206
  // The buildings round the venues stand apart from them: each block's planes are of no venue's group.
197
207
  if (plan.context !== undefined)
198
208
  for (const item of contextUprights(plan.context, profile.blocks))
199
209
  items.push({ ...item, planes: item.planes.map((p) => ({ ...p, group: APART })) });
210
+ // The landmarks round them, each by its own shape (a dome, a wheel, the jets of a fountain), apart from every venue too; and with names, each one's name over it.
211
+ const landmarks = plan.context?.landmarks ?? [];
212
+ for (const item of landmarkUprights(landmarks, profile.simple))
213
+ items.push({ ...item, planes: item.planes.map((p) => ({ ...p, group: APART })) });
214
+ if (labels) {
215
+ const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.013 : 0.018);
216
+ for (const l of landmarks)
217
+ items.push({ id: `lm-${l.key}-name`, kind: "scoreboard", at: l.label, height: l.height + size * 2.3, planes: [sign(`lm-${l.key}-name`, l.name[language], l.label, l.height + size * 0.5, size, APART)] });
218
+ }
200
219
  if (labels) {
201
220
  const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.016 : 0.022);
202
221
  plan.members.forEach((placed, i) => {
@@ -205,6 +224,13 @@ export function buildDistrictTurn(options, settings = {}) {
205
224
  items.push({ id: `m${i}-name`, kind: "scoreboard", at: middle, height: top + size * 1.7, planes: [sign(`m${i}-name`, placed.member.name[language], middle, top + size * 0.6, size, i)] });
206
225
  });
207
226
  }
227
+ if (labels && plan.circuit !== undefined) {
228
+ const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.016 : 0.022);
229
+ // Over the start, where the pits are, so that it does not stand in front of what the lap goes round.
230
+ const at = circuitPointIn(plan.circuit, plan.circuit.plan.start.at);
231
+ const top = (plan.circuit.plan.building?.height ?? 0) + size * 0.6;
232
+ items.push({ id: "ci-name", kind: "scoreboard", at, height: top + size * 1.7, planes: [sign("ci-name", plan.circuit.plan.circuit.names[language], at, top, size, APART)] });
233
+ }
208
234
  const view = plan.view;
209
235
  const scale = WORLD_PX / Math.max(view.w, view.h);
210
236
  const centre = [view.x + view.w / 2, view.y + view.h / 2];
package/dist/draw.js CHANGED
@@ -436,7 +436,7 @@ function realStadiumOf(o, sport, real) {
436
436
  return stadium !== undefined && stadium.configs[config] !== undefined && (stadiumKind(stadium) === "park") === (config === "mlb") ? { stadium, config } : undefined;
437
437
  }
438
438
  /** The stands round a surface, laid out from its box (or, for a ballpark, from its fence). */
439
- function standsFor(o, surface, crowd) {
439
+ function standsFor(o, surface, crowd, floor) {
440
440
  const { sport } = chooseLeague(o);
441
441
  const awaySide = resolveHomeSide({ ...o, sport }) === "left" ? "right" : "left";
442
442
  if (sport === "mlb") {
@@ -469,7 +469,19 @@ function standsFor(o, surface, crowd) {
469
469
  // A dojo's crowd sits in its gallery, from the room's balcony data (in feet here); the arenas (the Budokan, the Kokugikan) keep their bowls.
470
470
  const hall = sport === "dojo" ? dojoGallery(o) : undefined;
471
471
  const gallery = hall === undefined ? undefined : { offset: hall.offset / 0.3048, depth: hall.depth / 0.3048, height: [hall.height[0] / 0.3048, hall.height[1] / 0.3048], rows: hall.rows, aisles: hall.aisles };
472
- return crowdLayout({ sport, box, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }) });
472
+ return crowdLayout({ sport, box, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }), ...(floor === undefined ? {} : { floor }) });
473
+ }
474
+ /** A surface's floor with no margin (its view, and its outline where it has one); none for a close-up. */
475
+ function floorOf(probe) {
476
+ if (probe.whole !== undefined)
477
+ return undefined;
478
+ const view = wideView(probe);
479
+ if (probe.floor === undefined)
480
+ return { box: view };
481
+ const xs = [view.x, view.x + view.w, ...probe.floor.map((p) => p[0])];
482
+ const ys = [view.y, view.y + view.h, ...probe.floor.map((p) => p[1])];
483
+ const [x0, y0] = [Math.min(...xs), Math.min(...ys)];
484
+ return { box: { x: x0, y: y0, w: Math.max(...xs) - x0, h: Math.max(...ys) - y0 }, points: probe.floor };
473
485
  }
474
486
  /** True when the drawing is asked to be small: `detail: "simple"` or a `width` under 480 pixels. */
475
487
  const smallDrawing = (o) => {
@@ -493,17 +505,20 @@ function venueFrame(o, crowd, surface) {
493
505
  /** A surface with the stands and the crowd round it: the ground is made as wide as the stands need, then the stands are laid on it under the surface. */
494
506
  function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
495
507
  let margin = extra.margin;
508
+ // The floor the stands' opening clears and is centred on.
509
+ const probe = buildWhole({ ...o, margin: 0 }, portrait, id, score, scored, { ...extra, margin: undefined });
510
+ const floor = floorOf(probe);
496
511
  if (!(typeof o.margin === "number" && Number.isFinite(o.margin) && o.margin >= 0)) {
497
512
  // How much ground the stands need beyond what the surface has with none, then the surface is made again with it.
498
- const probe = buildWhole({ ...o, margin: 0 }, portrait, id, score, scored, { ...extra, margin: undefined });
499
- const stands = standsFor(o, probe, crowd);
513
+ const stands = standsFor(o, probe, crowd, floor);
500
514
  const view = wideView(probe);
501
515
  const have = { left: stands.box.x - view.x, right: view.x + view.w - stands.box.x - stands.box.w, top: stands.box.y - view.y, bottom: view.y + view.h - stands.box.y - stands.box.h };
502
516
  const need = Math.max(stands.reach.left - have.left, stands.reach.right - have.right, stands.reach.top - have.top, stands.reach.bottom - have.bottom);
503
- margin = Math.max(margin ?? 0, need + 6);
517
+ // (and the roof's shade, 9 feet beyond the upper tier)
518
+ margin = Math.max(margin ?? 0, need + (crowd.roof ? 10 : 6));
504
519
  }
505
520
  const surface = buildWhole(o, portrait, id, score, scored, { ...extra, margin });
506
- const stands = standsFor(o, surface, crowd);
521
+ const stands = standsFor(o, surface, crowd, floor);
507
522
  const seats = stands.seats;
508
523
  const ground = heldGround(o.ground)?.capacity;
509
524
  const fine = crowd.detail === "fine" || (crowd.detail === "auto" && !smallDrawing(o));
@@ -1220,5 +1235,5 @@ export function crowdStands(o) {
1220
1235
  if (crowd === undefined)
1221
1236
  return undefined;
1222
1237
  const probe = buildWhole({ ...o, margin: 0 }, false, drawingId(o), false, false, { features: undefined });
1223
- return standsFor(o, probe, crowd);
1238
+ return standsFor(o, probe, crowd, floorOf(probe));
1224
1239
  }
@@ -0,0 +1,105 @@
1
+ import type { Pt } from "./crowd.ts";
2
+ import { type Plane, type Upright } from "./turn.kit.ts";
3
+ import { type LonLat } from "./venues.geo.ts";
4
+ /** How a landmark is drawn. */
5
+ export type LandmarkShape = "dome" | "wheel" | "fountains" | "named";
6
+ export declare const LANDMARK_SHAPES: readonly LandmarkShape[];
7
+ /**
8
+ * A landmark of an area, as its data holds it:
9
+ * - `dome`: a sphere cut by the ground (the Sphere), `width` across at its widest and `height` tall; its outline is its footprint, and the building the area has there is the dome's own.
10
+ * - `wheel`: a big wheel standing upright along the long way of its outline (OpenStreetMap maps the High Roller's footprint), `width` across, its top `height` over the ground.
11
+ * - `fountains`: a row of jets along the long way of the water its outline is, reaching up to `height` (the most a published figure says they reach); the water is the area's land cover.
12
+ * - `named`: a name over the buildings inside its outline (they are the area's own, standing at their own heights), at `height`, the top of the tallest.
13
+ */
14
+ export interface ContextLandmark {
15
+ key: string;
16
+ /** The Wikidata item its names come from. */
17
+ wikidata: string;
18
+ name: {
19
+ en: string;
20
+ ja: string;
21
+ };
22
+ shape: LandmarkShape;
23
+ /** The OpenStreetMap element its outline is, `way/<id>`. */
24
+ source: string;
25
+ /** Its outline, metres east and north of the area's origin. */
26
+ ring: readonly (readonly [number, number])[];
27
+ /** Its height over the ground, in metres. */
28
+ height: number;
29
+ /** A dome's or a wheel's diameter, in metres. */
30
+ width?: number;
31
+ /** A named landmark's tallest building, where its name stands: metres east and north of the area's origin. */
32
+ top?: readonly [number, number];
33
+ /** Where the figures were read, in our own words. */
34
+ read: string;
35
+ }
36
+ /** A landmark placed in a district's frame: its outline in feet (x to the right, y down the page), its middle, its height and width in feet, and for a wheel or fountains the unit direction its long way runs. */
37
+ export interface PlacedLandmark {
38
+ key: string;
39
+ name: {
40
+ en: string;
41
+ ja: string;
42
+ };
43
+ shape: LandmarkShape;
44
+ ring: Pt[];
45
+ at: Pt;
46
+ /** Where its name stands: over its middle, or a named landmark's tallest building. */
47
+ label: Pt;
48
+ height: number;
49
+ width?: number;
50
+ along: Pt;
51
+ }
52
+ /** The landmarks of some areas placed in a district: turned as the plan is and kept where they stand in the frame (`frame` is the plan's box in feet). */
53
+ export declare function placeLandmarks(areas: readonly {
54
+ origin: readonly [number, number];
55
+ landmarks?: readonly ContextLandmark[];
56
+ }[], origin: LonLat, turn: number, frame: {
57
+ x: number;
58
+ y: number;
59
+ w: number;
60
+ h: number;
61
+ }): PlacedLandmark[];
62
+ /** Whether a building's outline is a landmark's own (most of it inside a dome's footprint): the dome draws it, and the block is left out. */
63
+ export declare const ownedByLandmark: (landmarks: readonly PlacedLandmark[], ring: readonly Pt[]) => boolean;
64
+ /** Whether a building stands under a named landmark (its middle inside the landmark's outline): it is raised in the 3D view whatever the count of blocks. */
65
+ export declare const underLandmark: (landmarks: readonly PlacedLandmark[], ring: readonly Pt[]) => boolean;
66
+ /** The jets of fountains, in feet: where each stands along the long way of the water (from its middle) and how high it reaches, as a share of the most. Ours: the published figure is the most they reach, the row is a pattern. */
67
+ export declare const FOUNTAIN_JETS: readonly (readonly [number, number])[];
68
+ /** What the floor has of the landmarks: a dome's footprint and a wheel's line on the ground, in the tone of the tallest buildings, and the jets' feet as dots on the water. Empty for none. */
69
+ export declare function landmarksFloor(landmarks: readonly PlacedLandmark[]): string;
70
+ /** How finely a dome is cut: sides round it and bands up it. Fewer in the lower tiers. */
71
+ export declare const DOME_CUT: Readonly<{
72
+ high: {
73
+ sides: number;
74
+ bands: number;
75
+ };
76
+ simple: {
77
+ sides: number;
78
+ bands: number;
79
+ };
80
+ }>;
81
+ /** The planes of a dome: a sphere `width` across whose top is `height` over the ground, cut by the ground, as bands of flat facets, each lit by the way it faces. */
82
+ export declare function domePlanes(prefix: string, at: Pt, height: number, width: number, cut: {
83
+ sides: number;
84
+ bands: number;
85
+ }): Plane[];
86
+ /** How a wheel is drawn: segments round its rim and spokes to its hub. */
87
+ export declare const WHEEL_CUT: Readonly<{
88
+ high: {
89
+ rim: number;
90
+ spokes: number;
91
+ };
92
+ simple: {
93
+ rim: number;
94
+ spokes: number;
95
+ };
96
+ }>;
97
+ /** The planes of a big wheel standing upright along `along`: its rim, its spokes and the two legs from its hub to the ground, each a thin bar seen from every side. */
98
+ export declare function wheelPlanes(prefix: string, at: Pt, along: Pt, height: number, width: number, cut: {
99
+ rim: number;
100
+ spokes: number;
101
+ }): Plane[];
102
+ /** The planes of fountains: a row of jets along the long way of the water, each a thin upright strip reaching to its share of `height`. */
103
+ export declare function fountainPlanes(prefix: string, l: Pick<PlacedLandmark, "ring" | "at" | "along" | "height">): Plane[];
104
+ /** The 3D view's landmarks: each one's shape standing where it is, cut more coarsely in a simple tier. A named landmark has no shape of its own: its buildings are the area's. */
105
+ export declare function landmarkUprights(landmarks: readonly PlacedLandmark[], simple: boolean): Upright[];