@johnmorrisdotca/kyogijo-stadiums 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/NOTICE.md +4 -0
- package/README.md +71 -16
- package/dist/annotate.js +1 -1
- package/dist/athletics/describe.d.ts +8 -0
- package/dist/athletics/describe.js +41 -0
- package/dist/athletics/events.d.ts +17 -0
- package/dist/athletics/events.js +48 -0
- package/dist/athletics/facilities.d.ts +80 -0
- package/dist/athletics/facilities.js +176 -0
- package/dist/athletics/infield.d.ts +79 -0
- package/dist/athletics/infield.js +166 -0
- package/dist/athletics/layout.d.ts +13 -0
- package/dist/athletics/layout.js +98 -0
- package/dist/athletics/oval.d.ts +57 -0
- package/dist/athletics/oval.js +116 -0
- package/dist/athletics/plan.d.ts +97 -0
- package/dist/athletics/plan.js +355 -0
- package/dist/athletics/races.d.ts +102 -0
- package/dist/athletics/races.js +355 -0
- package/dist/athletics/steeplechase.d.ts +72 -0
- package/dist/athletics/steeplechase.js +196 -0
- package/dist/athletics/stencil.d.ts +13 -0
- package/dist/athletics/stencil.js +85 -0
- package/dist/athletics/track.d.ts +54 -0
- package/dist/athletics/track.js +105 -0
- package/dist/athletics/types.d.ts +77 -0
- package/dist/athletics/types.js +1 -0
- package/dist/athletics-entry.d.ts +17 -0
- package/dist/athletics-entry.js +11 -0
- package/dist/athletics.style.d.ts +16 -0
- package/dist/athletics.style.js +51 -0
- package/dist/atlas-entry.d.ts +4 -2
- package/dist/atlas-entry.js +3 -2
- package/dist/atlas.d.ts +83 -7
- package/dist/atlas.js +108 -8
- package/dist/charts-entry.d.ts +6 -1
- package/dist/charts-entry.js +3 -0
- package/dist/charts.athletics.d.ts +183 -0
- package/dist/charts.athletics.js +455 -0
- package/dist/charts.curling.heat.d.ts +24 -0
- package/dist/charts.curling.heat.js +116 -0
- package/dist/charts.curling.js +14 -3
- package/dist/charts.pool.d.ts +67 -0
- package/dist/charts.pool.js +76 -0
- package/dist/charts.skating.d.ts +204 -0
- package/dist/charts.skating.js +426 -0
- package/dist/charts.types.d.ts +53 -1
- package/dist/court.d.ts +85 -2
- package/dist/court.js +102 -7
- package/dist/court.more.d.ts +46 -0
- package/dist/court.more.js +219 -0
- package/dist/courts.style.d.ts +14 -2
- package/dist/courts.style.js +51 -4
- package/dist/crowd.d.ts +154 -0
- package/dist/crowd.draw.d.ts +17 -0
- package/dist/crowd.draw.js +198 -0
- package/dist/crowd.js +465 -0
- package/dist/crowd.park.d.ts +19 -0
- package/dist/crowd.park.js +153 -0
- package/dist/curling.stats.d.ts +271 -0
- package/dist/curling.stats.js +301 -0
- package/dist/draw.d.ts +16 -0
- package/dist/draw.js +318 -35
- package/dist/element-entry.d.ts +1 -1
- package/dist/element-entry.js +1 -1
- package/dist/element.d.ts +13 -2
- package/dist/element.js +77 -3
- package/dist/features.js +131 -0
- package/dist/fields.kit.d.ts +2 -2
- package/dist/fields.types.d.ts +18 -4
- package/dist/frames.providers.js +63 -8
- package/dist/geojson.js +3 -0
- package/dist/geometry-entry.d.ts +2 -1
- package/dist/geometry-entry.js +2 -1
- package/dist/geometry.d.ts +20 -0
- package/dist/geometry.js +78 -1
- package/dist/grounds.d.ts +4 -0
- package/dist/grounds.data.js +12 -0
- package/dist/hardwood.d.ts +31 -0
- package/dist/hardwood.js +157 -0
- package/dist/heat.js +4 -3
- package/dist/heat.types.d.ts +5 -0
- package/dist/heat.zones.d.ts +2 -2
- package/dist/heat.zones.js +154 -6
- package/dist/homeside.js +17 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +10 -1
- package/dist/layers.d.ts +1 -1
- package/dist/layers.env.d.ts +3 -1
- package/dist/layers.env.js +4 -2
- package/dist/layers.js +4 -4
- package/dist/leagues/athletics.d.ts +63 -0
- package/dist/leagues/athletics.js +87 -0
- package/dist/leagues/courts.d.ts +269 -4
- package/dist/leagues/courts.js +155 -1
- package/dist/leagues/skating.d.ts +145 -0
- package/dist/leagues/skating.js +73 -0
- package/dist/leagues/sources.js +20 -0
- package/dist/leagues/types.d.ts +1 -1
- package/dist/leagues-entry.d.ts +4 -2
- package/dist/leagues-entry.js +2 -1
- package/dist/leagues.d.ts +14 -4
- package/dist/leagues.js +146 -5
- package/dist/look.d.ts +3 -3
- package/dist/look.js +39 -6
- package/dist/outline.d.ts +1 -1
- package/dist/outline.js +30 -2
- package/dist/pageant.d.ts +120 -0
- package/dist/pageant.draw.d.ts +21 -0
- package/dist/pageant.draw.js +171 -0
- package/dist/pageant.js +207 -0
- package/dist/pageant.style.d.ts +8 -0
- package/dist/pageant.style.js +42 -0
- package/dist/png.d.ts +8 -1
- package/dist/png.js +21 -1
- package/dist/pool.d.ts +176 -0
- package/dist/pool.figures.d.ts +304 -0
- package/dist/pool.figures.js +144 -0
- package/dist/pool.js +304 -0
- package/dist/pool.kit.d.ts +48 -0
- package/dist/pool.kit.js +141 -0
- package/dist/pool.labels.d.ts +20 -0
- package/dist/pool.labels.js +257 -0
- package/dist/pool.race.d.ts +65 -0
- package/dist/pool.race.js +140 -0
- package/dist/pool.replay.d.ts +46 -0
- package/dist/pool.replay.js +127 -0
- package/dist/pool.section.d.ts +13 -0
- package/dist/pool.section.js +83 -0
- package/dist/pool.style.d.ts +20 -0
- package/dist/pool.style.js +169 -0
- package/dist/pool.types.d.ts +145 -0
- package/dist/pool.types.js +3 -0
- package/dist/pool.words.d.ts +13 -0
- package/dist/pool.words.js +101 -0
- package/dist/ranges.names.js +47 -3
- package/dist/replay-entry.d.ts +3 -0
- package/dist/replay-entry.js +2 -0
- package/dist/restyle.js +1 -1
- package/dist/score.js +70 -5
- package/dist/sheet.d.ts +3 -0
- package/dist/sheet.js +7 -3
- package/dist/skating/describe.d.ts +7 -0
- package/dist/skating/describe.js +44 -0
- package/dist/skating/events.d.ts +24 -0
- package/dist/skating/events.js +47 -0
- package/dist/skating/figure.d.ts +24 -0
- package/dist/skating/figure.js +37 -0
- package/dist/skating/long.d.ts +54 -0
- package/dist/skating/long.js +111 -0
- package/dist/skating/parts.d.ts +43 -0
- package/dist/skating/parts.js +40 -0
- package/dist/skating/plan.d.ts +55 -0
- package/dist/skating/plan.js +197 -0
- package/dist/skating/short.d.ts +47 -0
- package/dist/skating/short.js +89 -0
- package/dist/skating/track.d.ts +66 -0
- package/dist/skating/track.js +94 -0
- package/dist/skating/types.d.ts +57 -0
- package/dist/skating/types.js +3 -0
- package/dist/skating-entry.d.ts +17 -0
- package/dist/skating-entry.js +11 -0
- package/dist/skating.style.d.ts +4 -0
- package/dist/skating.style.js +24 -0
- package/dist/strings.d.ts +296 -0
- package/dist/strings.js +328 -11
- package/dist/style.d.ts +2 -0
- package/dist/style.js +11 -1
- package/dist/surfaces/artistic.d.ts +9 -0
- package/dist/surfaces/artistic.js +94 -0
- package/dist/surfaces/athletics.d.ts +16 -0
- package/dist/surfaces/athletics.js +132 -0
- package/dist/surfaces/baseball.js +15 -0
- package/dist/surfaces/basketball.floor.d.ts +27 -0
- package/dist/surfaces/basketball.floor.js +83 -0
- package/dist/surfaces/basketball.gear.d.ts +27 -0
- package/dist/surfaces/basketball.gear.js +90 -0
- package/dist/surfaces/basketball.js +43 -21
- package/dist/surfaces/curling.d.ts +16 -0
- package/dist/surfaces/curling.fold.d.ts +67 -0
- package/dist/surfaces/curling.fold.js +107 -0
- package/dist/surfaces/curling.js +37 -3
- package/dist/surfaces/dive.d.ts +9 -0
- package/dist/surfaces/dive.js +128 -0
- package/dist/surfaces/field.d.ts +7 -2
- package/dist/surfaces/field.js +15 -9
- package/dist/surfaces/hockey.js +1 -0
- package/dist/surfaces/net.d.ts +2 -1
- package/dist/surfaces/net.js +160 -15
- package/dist/surfaces/polo.d.ts +12 -0
- package/dist/surfaces/polo.js +159 -0
- package/dist/surfaces/pool.d.ts +27 -0
- package/dist/surfaces/pool.js +198 -0
- package/dist/surfaces/skating.d.ts +6 -0
- package/dist/surfaces/skating.js +62 -0
- package/dist/surfaces/soccer.js +2 -1
- package/dist/surfaces/surface.d.ts +44 -0
- package/dist/surfaces/venue.d.ts +15 -0
- package/dist/surfaces/venue.js +95 -0
- package/dist/tenants.d.ts +77 -0
- package/dist/tenants.data.d.ts +8 -0
- package/dist/tenants.data.js +945 -0
- package/dist/tenants.js +46 -0
- package/dist/turn-entry.d.ts +12 -0
- package/dist/turn-entry.js +7 -0
- package/dist/turn.camera.d.ts +105 -0
- package/dist/turn.camera.js +168 -0
- package/dist/turn.dom.d.ts +40 -0
- package/dist/turn.dom.js +351 -0
- package/dist/turn.kit.d.ts +86 -0
- package/dist/turn.kit.js +110 -0
- package/dist/turn.markup.d.ts +48 -0
- package/dist/turn.markup.js +108 -0
- package/dist/turn.model.d.ts +78 -0
- package/dist/turn.model.js +91 -0
- package/dist/turn.style.d.ts +5 -0
- package/dist/turn.style.js +55 -0
- package/dist/types.d.ts +183 -18
- package/dist/units.d.ts +1 -1
- package/dist/units.js +15 -1
- package/dist/uprights.board.d.ts +17 -0
- package/dist/uprights.board.js +59 -0
- package/dist/uprights.d.ts +37 -0
- package/dist/uprights.fields.d.ts +32 -0
- package/dist/uprights.fields.js +172 -0
- package/dist/uprights.goal.d.ts +49 -0
- package/dist/uprights.goal.js +69 -0
- package/dist/uprights.js +160 -0
- package/dist/uprights.more.d.ts +7 -0
- package/dist/uprights.more.js +183 -0
- package/dist/uprights.net.d.ts +12 -0
- package/dist/uprights.net.js +109 -0
- package/dist/uprights.netkit.d.ts +28 -0
- package/dist/uprights.netkit.js +67 -0
- package/dist/uprights.pools.d.ts +8 -0
- package/dist/uprights.pools.js +190 -0
- package/dist/uprights.rinks.d.ts +27 -0
- package/dist/uprights.rinks.js +106 -0
- package/dist/uprights.track.d.ts +50 -0
- package/dist/uprights.track.js +246 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +19 -2
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// The skating data: the pairs of a speed skating race on the lines of its start (an inner and an outer lane, the armbands the Rules give them), the starting positions of a short track race, the results
|
|
2
|
+
// with their medals, times and splits, a replay of the race (every skater on his own way round the track, the lane changed once a lap), and the elements of a figure skating programme as marks on the ice.
|
|
3
|
+
// Written for this package from the ISU's Special Regulations and Technical Rules for speed skating (Rules 203, 227, 253, 254 and 280) and short track speed skating (Rule 280); nothing of any other package
|
|
4
|
+
// is used. The flags are the caller's: an SVG document as text, or an address, as a team's logo is.
|
|
5
|
+
import { assemble, clean, drawingOptions, languageOf, round, wordsOf } from "./charts.js";
|
|
6
|
+
import { esc } from "./format.js";
|
|
7
|
+
import { fill } from "./strings.js";
|
|
8
|
+
import { ELEMENT_KINDS, ELEMENT_NAMES, raceById } from "./skating/events.js";
|
|
9
|
+
import { ISU_SHORT_TRACK } from "./leagues/skating.js";
|
|
10
|
+
import { disciplineOf, skatingPlan } from "./skating/plan.js";
|
|
11
|
+
import { raceName } from "./skating/describe.js";
|
|
12
|
+
import { markRace } from "./skating/long.js";
|
|
13
|
+
import { shortPose, startingPositions, startsOnOppositeStraight } from "./skating/short.js";
|
|
14
|
+
import { finishAt, laneRadius, pose, startOf } from "./skating/track.js";
|
|
15
|
+
const isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
16
|
+
const WORDS = (language) => wordsOf(language).skating;
|
|
17
|
+
/** A time in seconds as it is written: `34.32`, `1:40.91`, `6:01.03`. */
|
|
18
|
+
export function formatTime(seconds, places = 2) {
|
|
19
|
+
if (!isNum(seconds) || seconds < 0)
|
|
20
|
+
return "";
|
|
21
|
+
const r = Math.round(seconds * 10 ** places) / 10 ** places;
|
|
22
|
+
const h = Math.floor(r / 3600);
|
|
23
|
+
const m = Math.floor((r - h * 3600) / 60);
|
|
24
|
+
const s = r - h * 3600 - m * 60;
|
|
25
|
+
const sec = s.toFixed(places).padStart(places > 0 ? places + 3 : 2, "0");
|
|
26
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${sec}` : m > 0 ? `${m}:${sec}` : s.toFixed(places);
|
|
27
|
+
}
|
|
28
|
+
/** The discipline of the options' rink: a chart is for a track, the long one unless the league says the short one. */
|
|
29
|
+
function trackOf(options) {
|
|
30
|
+
const d = disciplineOf({ sport: "skating", league: options.surface?.league });
|
|
31
|
+
if (d === "figure")
|
|
32
|
+
throw new RangeError('A race chart is for a speed skating track: use league "isu-long-track" or "isu-short-track"');
|
|
33
|
+
return d;
|
|
34
|
+
}
|
|
35
|
+
function raceOf(options) {
|
|
36
|
+
const id = options.race ?? "500m";
|
|
37
|
+
if (raceById(id) === undefined)
|
|
38
|
+
throw new Error(`Unknown race "${String(id)}"`);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
/** The plan of the race's drawing, close up on its start or its finish (or whole), with the race marked. */
|
|
42
|
+
function planOf(options, race, range, title, extraStyle = {}) {
|
|
43
|
+
const surface = options.surface ?? {};
|
|
44
|
+
const style = { races: [race], start: race, ...(surface.skating ?? {}), ...extraStyle };
|
|
45
|
+
const o = drawingOptions("skating", { ...options, surface: { ...(range === undefined ? {} : { range }), ...surface, skating: style } }, title, {});
|
|
46
|
+
const plan = skatingPlan({ sport: "skating", league: surface.league, skating: style, range: o.range });
|
|
47
|
+
return { o, plan };
|
|
48
|
+
}
|
|
49
|
+
const AT = { from: "skating", units: "m" };
|
|
50
|
+
const ARMBAND = { inner: "#ffffff", outer: "#d6342a" };
|
|
51
|
+
/** The point and the heading of a skater of a long track race who has skated `d` metres. */
|
|
52
|
+
function longAt(oval, race, lane, d) {
|
|
53
|
+
const m = markRace(oval, race);
|
|
54
|
+
const nc = m.noChange;
|
|
55
|
+
const dist = raceById(race)?.distance ?? 0;
|
|
56
|
+
const start = startOf(oval, lane, dist, finishAt(oval, m.finish), nc);
|
|
57
|
+
const p = pose(oval, lane, start.v0 + d, nc);
|
|
58
|
+
return { x: p.x, y: p.y, heading: p.heading };
|
|
59
|
+
}
|
|
60
|
+
/** Where a skater of a short track race is when `d` metres have been skated from the starting position `position` (1 on the inner side). */
|
|
61
|
+
function shortAt(oval, race, position, d) {
|
|
62
|
+
const laps = ISU_SHORT_TRACK.laps[race];
|
|
63
|
+
const total = laps * oval.lap;
|
|
64
|
+
// The finishing line is the middle of the bottom straight: S/2 along it. A half-lap race starts at the middle of the top one.
|
|
65
|
+
const finish = oval.straight / 2;
|
|
66
|
+
const v0 = (((finish - total) % oval.lap) + oval.lap) % oval.lap;
|
|
67
|
+
const offset = 0.5 + 1.3 * (position - 1) - 0.5;
|
|
68
|
+
const out = offset * Math.max(0, 1 - d / 25);
|
|
69
|
+
return shortPose(oval, v0 + d, out);
|
|
70
|
+
}
|
|
71
|
+
// ----- the pairs and the starting positions
|
|
72
|
+
/**
|
|
73
|
+
* The pairs of a long track race on the start's close-up: the pair `pair` (the first by default) with each skater's name and flag behind the starting line of his lane, and an armband's colour (white
|
|
74
|
+
* for the inner lane, red for the outer: Rule 253.3.1); a table lists every pair. The starting lines are the ISU's: the 500 m on the finishing straight, the 1000 m, 1500 m, 3000 m and 5000 m on the
|
|
75
|
+
* crossing straight, where each lane's line stands a different distance from the finish.
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* import { skatingPairs } from "@johnmorrisdotca/kyogijo-stadiums/skating";
|
|
79
|
+
* const chart = skatingPairs([{ pair: 1, inner: { name: "A. Skater" }, outer: { name: "B. Skater" } }], { race: "1000m" });
|
|
80
|
+
* console.log(chart.totals, chart.title);
|
|
81
|
+
* // { skaters: 2, pairs: 1 } Pairs, 1000 m
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function skatingPairs(pairs, options = {}) {
|
|
85
|
+
if (!Array.isArray(pairs))
|
|
86
|
+
throw new TypeError("pairs is a list");
|
|
87
|
+
const track = trackOf(options);
|
|
88
|
+
if (track !== "long-track")
|
|
89
|
+
throw new RangeError("Pairs are for the long track: the short track has starting positions (skatingGrid)");
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
for (const p of pairs) {
|
|
92
|
+
if (typeof p !== "object" || p === null || !Number.isInteger(p.pair) || p.pair < 1)
|
|
93
|
+
throw new TypeError("each pair has a number, a whole number from 1");
|
|
94
|
+
if (seen.has(p.pair))
|
|
95
|
+
throw new RangeError(`pair ${p.pair} is given twice`);
|
|
96
|
+
seen.add(p.pair);
|
|
97
|
+
}
|
|
98
|
+
const race = raceOf(options);
|
|
99
|
+
const language = languageOf(options);
|
|
100
|
+
const w = WORDS(language);
|
|
101
|
+
const title = fill(w.data.draw, { race: raceName(race, language) });
|
|
102
|
+
const { o, plan } = planOf(options, race, "start", title);
|
|
103
|
+
const oval = plan.oval;
|
|
104
|
+
const which = options.pair ?? [...pairs].sort((a, b) => a.pair - b.pair)[0]?.pair;
|
|
105
|
+
const shown = pairs.find((p) => p.pair === which);
|
|
106
|
+
const names = [];
|
|
107
|
+
const flags = [];
|
|
108
|
+
const bands = [];
|
|
109
|
+
const m = markRace(oval, race);
|
|
110
|
+
for (const [laneName, entry] of [["inner", shown?.inner], ["outer", shown?.outer]]) {
|
|
111
|
+
if (entry === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
const lane = laneName === "inner" ? 0 : 1;
|
|
114
|
+
const start = m.starts.find((s) => s.lane === lane || s.lane === "both");
|
|
115
|
+
if (start === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
const fx = Math.cos(start.heading);
|
|
118
|
+
const fy = Math.sin(start.heading);
|
|
119
|
+
// The skater stands in his lane, on the line of its measurement, behind the starting line.
|
|
120
|
+
const sign = start.y >= 0 ? 1 : -1;
|
|
121
|
+
const base = { x: start.x, y: sign * laneRadius(oval, lane) };
|
|
122
|
+
const back = (d) => [base.x - fx * d, base.y - fy * d];
|
|
123
|
+
const [bx, by] = back(1.6);
|
|
124
|
+
bands.push({ x: bx, y: by, shape: "circle", size: 1.2, color: ARMBAND[laneName], title: laneName === "inner" ? w.data.lanes.inner : w.data.lanes.outer });
|
|
125
|
+
if (entry.flag !== undefined) {
|
|
126
|
+
const [px, py] = back(3.6);
|
|
127
|
+
flags.push(clean({ x: px, y: py, shape: "logo", logo: entry.flag, size: 1.6, title: entry.country }));
|
|
128
|
+
}
|
|
129
|
+
const [nx, ny] = back(entry.flag !== undefined ? 5.4 : 3);
|
|
130
|
+
names.push(clean({ x: nx, y: ny, text: entry.name, anchor: fx < 0 ? "start" : "end", size: 1.9, title: entry.country === undefined ? undefined : `${entry.name}, ${entry.country}` }));
|
|
131
|
+
}
|
|
132
|
+
const layers = [];
|
|
133
|
+
layers.push({ kind: "points", id: "armbands", name: w.data.layers.names, ...AT, clip: "none", data: bands, style: { size: 1.2 } });
|
|
134
|
+
if (flags.length > 0)
|
|
135
|
+
layers.push({ kind: "points", id: "flags", name: w.data.layers.flags, ...AT, clip: "none", data: flags, style: { shape: "logo", size: 1.6 } });
|
|
136
|
+
layers.push({ kind: "text", id: "names", name: w.data.layers.names, ...AT, clip: "none", data: names, style: { size: 1.9, anchor: "end", weight: 800 } });
|
|
137
|
+
const skaters = pairs.reduce((n, p) => n + (p.inner === undefined ? 0 : 1) + (p.outer === undefined ? 0 : 1), 0);
|
|
138
|
+
const totals = { skaters, pairs: pairs.length };
|
|
139
|
+
const last = pairs.reduce((n, p) => Math.max(n, p.pair), 0);
|
|
140
|
+
const summary = pairs.length === 0 ? "" : fill(w.data.summaryDraw, { n: skaters, race: raceName(race, language), last });
|
|
141
|
+
const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["names"] });
|
|
142
|
+
chart.table = () => pairsTable([...pairs].sort((a, b) => a.pair - b.pair), language);
|
|
143
|
+
return chart;
|
|
144
|
+
}
|
|
145
|
+
function pairsTable(pairs, language) {
|
|
146
|
+
const w = WORDS(language).data;
|
|
147
|
+
const c = w.columns;
|
|
148
|
+
const cell = (e) => (e === undefined ? "" : `${esc(e.name)}${e.country === undefined ? "" : ` (${esc(e.country)})`}`);
|
|
149
|
+
const rows = pairs.map((p) => `<tr><td>${p.pair}</td><td>${cell(p.inner)}</td><td>${cell(p.outer)}</td></tr>`).join("");
|
|
150
|
+
return `<table class="ky-table"><thead><tr><th>${c.pair}</th><th>${w.lanes.inner}</th><th>${w.lanes.outer}</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The starting positions of a short track race on the start's close-up: the five dots of the 500 m and the 1000 m (every 1.3 m from 50 cm out from the inner side, Rule 280.2c), each with its starter's
|
|
154
|
+
* name and flag. The 500 m and the 1500 m start on the straight opposite the finishing line; the others at the finishing line.
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* import { skatingGrid } from "@johnmorrisdotca/kyogijo-stadiums/skating";
|
|
158
|
+
* const chart = skatingGrid([{ position: 1, name: "A. Skater" }, { position: 2, name: "B. Skater" }], { race: "500m", surface: { league: "isu-short-track" } });
|
|
159
|
+
* console.log(chart.totals.skaters);
|
|
160
|
+
* // 2
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export function skatingGrid(starters, options = {}) {
|
|
164
|
+
if (!Array.isArray(starters))
|
|
165
|
+
throw new TypeError("starters is a list");
|
|
166
|
+
const surface = { ...(options.surface ?? {}), league: options.surface?.league ?? "isu-short-track" };
|
|
167
|
+
const opts = { ...options, surface };
|
|
168
|
+
if (trackOf(opts) !== "short-track")
|
|
169
|
+
throw new RangeError("Starting positions are for the short track: the long track has pairs (skatingPairs)");
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
for (const s of starters) {
|
|
172
|
+
if (typeof s !== "object" || s === null || !Number.isInteger(s.position) || s.position < 1 || s.position > 5)
|
|
173
|
+
throw new RangeError("each starter has a position from 1 to 5 (Rule 280.2c)");
|
|
174
|
+
if (seen.has(s.position))
|
|
175
|
+
throw new RangeError(`position ${s.position} is given twice`);
|
|
176
|
+
seen.add(s.position);
|
|
177
|
+
}
|
|
178
|
+
const race = raceOf(opts);
|
|
179
|
+
const language = languageOf(opts);
|
|
180
|
+
const w = WORDS(language);
|
|
181
|
+
const title = fill(w.data.draw, { race: raceName(race, language) });
|
|
182
|
+
const { o, plan } = planOf(opts, race, "start", title);
|
|
183
|
+
const oval = plan.short;
|
|
184
|
+
const top = startsOnOppositeStraight(race);
|
|
185
|
+
const spots = startingPositions(oval, 0, top ? -1 : 1);
|
|
186
|
+
const names = [];
|
|
187
|
+
const flags = [];
|
|
188
|
+
for (const s of [...starters].sort((a, b) => a.position - b.position)) {
|
|
189
|
+
const spot = spots[s.position - 1];
|
|
190
|
+
// The skaters go left along the top straight and right along the bottom one: the names stand behind the line.
|
|
191
|
+
const dir = top ? -1 : 1;
|
|
192
|
+
names.push(clean({ x: spot.x - dir * (s.flag === undefined ? 1.2 : 2.6), y: spot.y, text: s.name, anchor: top ? "start" : "end", size: 0.9, title: s.country === undefined ? undefined : `${s.name}, ${s.country}` }));
|
|
193
|
+
if (s.flag !== undefined)
|
|
194
|
+
flags.push(clean({ x: spot.x - dir * 1.4, y: spot.y, shape: "logo", logo: s.flag, size: 0.8, title: s.country }));
|
|
195
|
+
}
|
|
196
|
+
const layers = [];
|
|
197
|
+
if (flags.length > 0)
|
|
198
|
+
layers.push({ kind: "points", id: "flags", name: w.data.layers.flags, ...AT, clip: "none", data: flags, style: { shape: "logo", size: 0.8 } });
|
|
199
|
+
layers.push({ kind: "text", id: "names", name: w.data.layers.names, ...AT, clip: "none", data: names, style: { size: 0.9, anchor: top ? "start" : "end", weight: 800 } });
|
|
200
|
+
const totals = { skaters: starters.length, pairs: starters.length === 0 ? 0 : 1 };
|
|
201
|
+
const summary = starters.length === 0 ? "" : fill(w.data.summaryGrid, { n: starters.length, race: raceName(race, language) });
|
|
202
|
+
const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["names"] });
|
|
203
|
+
chart.table = () => `<table class="ky-table"><thead><tr><th>${w.data.columns.order}</th><th>${w.data.columns.skater}</th><th>${w.data.columns.country}</th></tr></thead><tbody>${[...starters].sort((a, b) => a.position - b.position).map((s) => `<tr><td>${s.position}</td><td>${esc(s.name)}</td><td>${esc(s.country ?? "")}</td></tr>`).join("")}</tbody></table>`;
|
|
204
|
+
return chart;
|
|
205
|
+
}
|
|
206
|
+
// ----- the results
|
|
207
|
+
const MEDAL_COLOURS = { gold: "#d4a017", silver: "#b8bec6", bronze: "#b87333" };
|
|
208
|
+
/**
|
|
209
|
+
* The results of a race as a leaderboard on the ice inside the track: for each skater with a place, a disc with the place (gold, silver and bronze for the first three), the name and the time; the splits
|
|
210
|
+
* as small marks along the skater's lane on the long track; and a table with the pair, the lane, the time, the splits and the lap times. Times are in seconds from the gun.
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* import { skatingResults } from "@johnmorrisdotca/kyogijo-stadiums/skating";
|
|
214
|
+
* const chart = skatingResults([{ name: "A", place: 1, time: 34.3 }, { name: "B", place: 2, time: 34.5 }], { race: "500m" });
|
|
215
|
+
* console.log(chart.totals.winner?.name, chart.summary);
|
|
216
|
+
* // A A won the 500 m in 34.30.
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export function skatingResults(results, options = {}) {
|
|
220
|
+
if (!Array.isArray(results))
|
|
221
|
+
throw new TypeError("results is a list");
|
|
222
|
+
const track = trackOf(options);
|
|
223
|
+
const race = raceOf(options);
|
|
224
|
+
const language = languageOf(options);
|
|
225
|
+
const w = WORDS(language);
|
|
226
|
+
const title = fill(w.data.results, { race: raceName(race, language) });
|
|
227
|
+
const { o, plan } = planOf(options, race, undefined, title);
|
|
228
|
+
const sorted = [...results].sort((a, b) => (a.place ?? 999) - (b.place ?? 999));
|
|
229
|
+
const long = track === "long-track";
|
|
230
|
+
const unit = long ? 4.4 : 1.35;
|
|
231
|
+
const rows = Math.min(options.rows ?? (long ? 8 : 5), sorted.filter((r) => r.place !== undefined).length);
|
|
232
|
+
const discs = [];
|
|
233
|
+
const places = [];
|
|
234
|
+
const words = [];
|
|
235
|
+
const x0 = long ? -28 : -9;
|
|
236
|
+
const top = -((rows - 1) / 2) * unit * 1.25;
|
|
237
|
+
sorted.filter((r) => r.place !== undefined && isNum(r.time)).slice(0, rows).forEach((r, i) => {
|
|
238
|
+
const y = top + i * unit * 1.25;
|
|
239
|
+
const medal = r.place === 1 ? "gold" : r.place === 2 ? "silver" : r.place === 3 ? "bronze" : undefined;
|
|
240
|
+
discs.push(clean({ x: x0, y, shape: "circle", size: unit * 0.9, color: medal === undefined ? "#2f3a44" : MEDAL_COLOURS[medal], title: `${r.name}, ${medal === undefined ? "" : `${w.data.medals[medal]}, `}${formatTime(r.time)}` }));
|
|
241
|
+
places.push({ x: x0, y, text: r.place, size: unit * 0.55, color: medal === undefined ? "#ffffff" : "#1c1f1d" });
|
|
242
|
+
words.push({ x: x0 + unit * 0.9, y, text: `${r.name} ${formatTime(r.time)}`, anchor: "start", size: unit * 0.55 });
|
|
243
|
+
});
|
|
244
|
+
const marks = [];
|
|
245
|
+
if (long && options.splits !== false) {
|
|
246
|
+
const oval = plan.oval;
|
|
247
|
+
for (const r of results) {
|
|
248
|
+
if (r.lane === undefined || r.splits === undefined)
|
|
249
|
+
continue;
|
|
250
|
+
const lane = r.lane === "inner" ? 0 : 1;
|
|
251
|
+
for (const sp of r.splits) {
|
|
252
|
+
if (!isNum(sp.distance) || !isNum(sp.time))
|
|
253
|
+
continue;
|
|
254
|
+
const p = longAt(oval, race, lane, sp.distance);
|
|
255
|
+
marks.push({ x: round(p.x, 3), y: round(p.y, 3), shape: "circle", size: 1.1, title: fill(w.data.split, { d: round(sp.distance, 1), t: formatTime(sp.time) }), value: sp.time });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const layers = [];
|
|
260
|
+
if (marks.length > 0)
|
|
261
|
+
layers.push({ kind: "points", id: "splits", name: w.data.layers.splits, ...AT, clip: "none", data: marks, style: { size: 1.1, fill: "#f2c230" } });
|
|
262
|
+
layers.push({ kind: "points", id: "places", name: w.data.layers.medals, ...AT, clip: "none", data: discs, style: { size: unit * 0.9 } });
|
|
263
|
+
layers.push({ kind: "text", id: "place-numbers", name: w.data.layers.medals, ...AT, clip: "none", data: places, style: { size: unit * 0.55, weight: 800, halo: false } });
|
|
264
|
+
layers.push({ kind: "text", id: "skaters", name: w.data.layers.names, ...AT, clip: "none", data: words, style: { size: unit * 0.55, anchor: "start", weight: 800 } });
|
|
265
|
+
const winner = sorted.find((r) => r.place === 1 && isNum(r.time));
|
|
266
|
+
const totals = { skaters: results.length, finished: results.filter((r) => isNum(r.time) && r.place !== undefined).length, winner: winner === undefined ? null : { name: winner.name, time: winner.time } };
|
|
267
|
+
const summary = winner === undefined ? "" : fill(w.data.summaryResults, { winner: winner.name, race: raceName(race, language), time: formatTime(totals.winner?.time ?? 0) });
|
|
268
|
+
const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["places"] });
|
|
269
|
+
chart.table = () => resultsTable(sorted, language);
|
|
270
|
+
return chart;
|
|
271
|
+
}
|
|
272
|
+
function resultsTable(results, language) {
|
|
273
|
+
const w = WORDS(language).data;
|
|
274
|
+
const c = w.columns;
|
|
275
|
+
const rows = results
|
|
276
|
+
.map((r) => {
|
|
277
|
+
const splits = (r.splits ?? []).map((s) => `${round(s.distance, 1)} ${w.metres}: ${formatTime(s.time)}`).join("; ");
|
|
278
|
+
const laps = (r.laps ?? []).map((t, i) => fill(w.lap, { n: i + 1, t: formatTime(t) })).join("; ");
|
|
279
|
+
return `<tr><td>${r.place ?? ""}</td><td>${r.pair ?? ""}</td><td>${r.lane === undefined ? "" : r.lane === "inner" ? w.lanes.inner : w.lanes.outer}</td><td>${esc(r.name)}</td><td>${esc(r.country ?? "")}</td><td>${isNum(r.time) ? formatTime(r.time) : esc(r.note ?? "")}</td><td>${esc(splits)}</td><td>${esc(laps)}</td></tr>`;
|
|
280
|
+
})
|
|
281
|
+
.join("");
|
|
282
|
+
return `<table class="ky-table"><thead><tr><th>${c.place}</th><th>${c.pair}</th><th>${c.lane}</th><th>${c.skater}</th><th>${c.country}</th><th>${c.time}</th><th>${c.splits}</th><th>${c.laps}</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
283
|
+
}
|
|
284
|
+
/** The distance a skater has skated at a time, by the splits and the finish: straight lines between them, coasting for three seconds after the finish. */
|
|
285
|
+
export function distanceSkated(runner, total, t) {
|
|
286
|
+
if (t <= 0)
|
|
287
|
+
return 0;
|
|
288
|
+
const pts = [[0, 0]];
|
|
289
|
+
for (const s of [...(runner.splits ?? [])].sort((a, b) => a.time - b.time))
|
|
290
|
+
if (s.time > 0 && s.time < runner.time && s.distance > 0 && s.distance < total)
|
|
291
|
+
pts.push([s.time, s.distance]);
|
|
292
|
+
pts.push([runner.time, total]);
|
|
293
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
294
|
+
if (t <= pts[i][0]) {
|
|
295
|
+
const [t0, d0] = pts[i - 1];
|
|
296
|
+
const [t1, d1] = pts[i];
|
|
297
|
+
return d0 + ((t - t0) / (t1 - t0)) * (d1 - d0);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const [t0, d0] = pts[pts.length - 2];
|
|
301
|
+
const [t1, d1] = pts[pts.length - 1];
|
|
302
|
+
const v = (d1 - d0) / (t1 - t0);
|
|
303
|
+
const u = Math.min(t - t1, 3);
|
|
304
|
+
return d1 + v * (u - (u * u) / 6);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* A replay of a race: every skater, from the gun, on his own way round the track (the long track's lane changed once a lap, on the crossing straight, and the first straight of the 1000 m and the 1500 m
|
|
308
|
+
* kept in the lane; the short track's oval of 111.12 m, the starters coming in from their positions), at the pace of the splits and the finish time. The positions are in the skating frame (metres
|
|
309
|
+
* from the middle of the rink). The team races, the mass start and the relays are not replayed.
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* import { skatingReplay } from "@johnmorrisdotca/kyogijo-stadiums/skating";
|
|
313
|
+
* const replay = skatingReplay([{ name: "A", lane: "inner", time: 34.4 }, { name: "B", lane: "outer", time: 34.6 }], { race: "500m" });
|
|
314
|
+
* console.log(replay.timeline.frames.length > 40, replay.duration);
|
|
315
|
+
* // true 34.6
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
export function skatingReplay(runners, options = {}) {
|
|
319
|
+
if (!Array.isArray(runners))
|
|
320
|
+
throw new TypeError("runners is a list");
|
|
321
|
+
trackOf(options);
|
|
322
|
+
const race = raceOf(options);
|
|
323
|
+
const info = raceById(race);
|
|
324
|
+
if (race === "mass-start" || race === "team-pursuit" || race === "team-sprint")
|
|
325
|
+
throw new RangeError(`The ${race} is not replayed: its skaters are not in lanes`);
|
|
326
|
+
const language = languageOf(options);
|
|
327
|
+
const w = WORDS(language);
|
|
328
|
+
const title = fill(w.data.replay, { race: raceName(race, language) });
|
|
329
|
+
const { o, plan } = planOf(options, race, undefined, title);
|
|
330
|
+
const step = isNum(options.step) && options.step > 0 ? options.step : 0.25;
|
|
331
|
+
for (const r of runners)
|
|
332
|
+
if (!isNum(r.time) || r.time <= 0)
|
|
333
|
+
throw new RangeError(`${String(r.name)}: the time is a number of seconds above zero`);
|
|
334
|
+
const total = info.distance;
|
|
335
|
+
const shortTotal = plan.short === undefined ? 0 : ISU_SHORT_TRACK.laps[race] * plan.short.lap;
|
|
336
|
+
const duration = Math.max(...runners.map((r) => r.time), 0);
|
|
337
|
+
const frames = [];
|
|
338
|
+
const at = (r, d) => {
|
|
339
|
+
if (plan.short !== undefined)
|
|
340
|
+
return shortAt(plan.short, race, r.position ?? 1, d);
|
|
341
|
+
return longAt(plan.oval, race, r.lane === "outer" ? 1 : 0, d);
|
|
342
|
+
};
|
|
343
|
+
const distance = plan.short === undefined ? total : shortTotal;
|
|
344
|
+
for (let k = 0; k * step <= duration + 3.2 + 1e-9; k += 1) {
|
|
345
|
+
const t = Math.round(k * step * 1000) / 1000;
|
|
346
|
+
const objects = [];
|
|
347
|
+
runners.forEach((r, i) => {
|
|
348
|
+
const p = at(r, distanceSkated(r, distance, t));
|
|
349
|
+
objects.push(clean({ id: `skater-${i + 1}`, team: r.lane === "outer" ? "away" : "home", x: round(p.x, 3), y: round(p.y, 3), label: r.name, number: plan.short === undefined ? (r.lane === "outer" ? 2 : 1) : (r.position ?? i + 1) }));
|
|
350
|
+
});
|
|
351
|
+
frames.push({ t, objects });
|
|
352
|
+
}
|
|
353
|
+
const events = [{ t: 0, kind: "gun", label: w.data.gun }];
|
|
354
|
+
runners.forEach((r, i) => {
|
|
355
|
+
const p = at(r, distance);
|
|
356
|
+
events.push({ t: r.time, kind: "finish", team: r.lane === "outer" ? "away" : "home", x: round(p.x, 3), y: round(p.y, 3), by: `skater-${i + 1}`, label: fill(w.data.finish, { name: r.name, time: formatTime(r.time) }) });
|
|
357
|
+
});
|
|
358
|
+
events.sort((a, b) => a.t - b.t);
|
|
359
|
+
const timeline = { frames, events, from: "skating", units: "m" };
|
|
360
|
+
const summary = fill(w.data.summaryReplay, { n: runners.length, race: raceName(race, language) });
|
|
361
|
+
return { timeline, options: { ...o, title }, look: { trail: 0, scale: plan.short === undefined ? 0.8 : 0.7, numbers: true, hold: 2 }, title, summary, duration };
|
|
362
|
+
}
|
|
363
|
+
const KIND_SHAPE = { jump: "circle", spin: "star", "step-sequence": "diamond", "choreographic-sequence": "diamond", lift: "triangle", spiral: "square", twizzle: "x", "death-spiral": "star", throw: "cross" };
|
|
364
|
+
/**
|
|
365
|
+
* The elements of a programme as marks on the rink: a mark for each element where it starts, its shape for its kind, the track of a step sequence or a spiral as a line to where it ends, and the order
|
|
366
|
+
* and the name beside it. Put them on the rink (`league: "isu-figure"`, the default for this chart).
|
|
367
|
+
* @example
|
|
368
|
+
* ```ts
|
|
369
|
+
* import { figureElements } from "@johnmorrisdotca/kyogijo-stadiums/skating";
|
|
370
|
+
* const chart = figureElements([{ kind: "jump", name: "3Lz", x: -12, y: 4, order: 1 }, { kind: "spin", name: "CCoSp4", x: 6, y: -3, order: 2 }]);
|
|
371
|
+
* console.log(chart.totals.elements, chart.totals.kinds.jump);
|
|
372
|
+
* // 2 1
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
export function figureElements(elements, options = {}) {
|
|
376
|
+
if (!Array.isArray(elements))
|
|
377
|
+
throw new TypeError("elements is a list");
|
|
378
|
+
for (const e of elements) {
|
|
379
|
+
if (typeof e !== "object" || e === null || !ELEMENT_KINDS.includes(e.kind))
|
|
380
|
+
throw new Error(`Unknown kind of element "${String(e?.kind)}": use one of ${ELEMENT_KINDS.join(", ")}`);
|
|
381
|
+
if (!isNum(e.x) || !isNum(e.y))
|
|
382
|
+
throw new TypeError("each element has a place: x and y in metres from the middle of the rink");
|
|
383
|
+
}
|
|
384
|
+
const language = languageOf(options);
|
|
385
|
+
const w = WORDS(language);
|
|
386
|
+
const surface = { ...(options.surface ?? {}), league: options.surface?.league ?? "isu-figure" };
|
|
387
|
+
if (disciplineOf({ sport: "skating", league: surface.league }) !== "figure")
|
|
388
|
+
throw new RangeError('figureElements is for the figure skating rink: use league "isu-figure"');
|
|
389
|
+
const title = w.data.elements;
|
|
390
|
+
const o = drawingOptions("skating", { ...options, surface }, title, {});
|
|
391
|
+
const plan = skatingPlan({ sport: "skating", league: surface.league, skating: surface.skating });
|
|
392
|
+
const half = (plan.rink?.length ?? 60) / 2;
|
|
393
|
+
const wide = (plan.rink?.width ?? 30) / 2;
|
|
394
|
+
const sorted = [...elements].sort((a, b) => (a.order ?? 1e9) - (b.order ?? 1e9));
|
|
395
|
+
const points = [];
|
|
396
|
+
const lines = [];
|
|
397
|
+
const labels = [];
|
|
398
|
+
const kinds = {};
|
|
399
|
+
sorted.forEach((e, i) => {
|
|
400
|
+
kinds[e.kind] = (kinds[e.kind] ?? 0) + 1;
|
|
401
|
+
const x = Math.max(-half, Math.min(half, e.x));
|
|
402
|
+
const y = Math.max(-wide, Math.min(wide, e.y));
|
|
403
|
+
const kindName = ELEMENT_NAMES[e.kind][language];
|
|
404
|
+
points.push(clean({ x, y, shape: KIND_SHAPE[e.kind], team: e.team ?? "home", outcome: e.kind, title: e.title ?? `${e.order === undefined ? i + 1 : e.order}. ${e.name ?? kindName}` }));
|
|
405
|
+
if (isNum(e.x2) && isNum(e.y2))
|
|
406
|
+
lines.push(clean({ x, y, x2: Math.max(-half, Math.min(half, e.x2)), y2: Math.max(-wide, Math.min(wide, e.y2)), team: e.team }));
|
|
407
|
+
if (options.labels !== false)
|
|
408
|
+
labels.push({ x: x + 1.1, y: y - 1.1, text: `${e.order === undefined ? i + 1 : e.order}${e.name === undefined ? "" : ` ${e.name}`}`, size: 1.15, anchor: "start" });
|
|
409
|
+
});
|
|
410
|
+
const layers = [];
|
|
411
|
+
if (lines.length > 0)
|
|
412
|
+
layers.push({ kind: "lines", id: "paths", name: w.data.layers.paths, ...AT, clip: "none", data: lines.map((l) => clean({ x: l.x, y: l.y, x2: l.x2, y2: l.y2, team: l.team ?? "home" })), style: { width: 0.35, dash: "dashed" } });
|
|
413
|
+
layers.push({ kind: "points", id: "elements", name: w.data.layers.elements, ...AT, clip: "none", data: points, style: { size: 1.5 } });
|
|
414
|
+
if (labels.length > 0)
|
|
415
|
+
layers.push({ kind: "text", id: "labels", name: w.data.layers.elements, ...AT, clip: "none", data: labels, style: { size: 1.15, weight: 800 } });
|
|
416
|
+
const totals = { elements: elements.length, kinds };
|
|
417
|
+
const who = sorted.map((e) => e.name ?? ELEMENT_NAMES[e.kind][language]).slice(0, 6).join(language === "ja" ? "、" : ", ");
|
|
418
|
+
const summary = elements.length === 0 ? "" : fill(w.data.summaryElements, { n: elements.length, who });
|
|
419
|
+
const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["elements"] });
|
|
420
|
+
chart.table = () => {
|
|
421
|
+
const c = w.data.columns;
|
|
422
|
+
const rows = sorted.map((e, i) => `<tr><td>${e.order ?? i + 1}</td><td>${esc(e.name ?? "")}</td><td>${esc(ELEMENT_NAMES[e.kind][language])}</td><td>${round(e.x, 1)}, ${round(e.y, 1)}</td></tr>`).join("");
|
|
423
|
+
return `<table class="ky-table"><thead><tr><th>${c.order}</th><th>${c.element}</th><th>${c.kind}</th><th>x, y (m)</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
424
|
+
};
|
|
425
|
+
return chart;
|
|
426
|
+
}
|
package/dist/charts.types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Frame } from "./frames.ts";
|
|
|
2
2
|
import type { HeatRamp } from "./heat.types.ts";
|
|
3
3
|
import type { Layer, LayerTeam, MarkShape, TableOptions } from "./layers.types.ts";
|
|
4
4
|
import type { SoccerProviderId } from "./surfaces/soccer-providers.ts";
|
|
5
|
-
import type { Language, SurfaceOptions } from "./types.ts";
|
|
5
|
+
import type { Language, SheetShow, SurfaceOptions } from "./types.ts";
|
|
6
6
|
import type { Unit } from "./units.ts";
|
|
7
7
|
/** What every chart gives back: the layers and the drawing options to put them on, a sentence, the totals, and the ways to draw them. */
|
|
8
8
|
export interface Chart<Totals = Record<string, never>> {
|
|
@@ -357,6 +357,58 @@ export interface CurlingOptions extends ChartOptions {
|
|
|
357
357
|
units?: Unit;
|
|
358
358
|
/** Write each stone's number on it. On by default, for the stones that have one. */
|
|
359
359
|
numbers?: boolean;
|
|
360
|
+
/** How much of the sheet to draw: `half` (the default, the free guard zone and the house at `house`), `house`, `sheet` (the whole sheet), or `folded` (the whole sheet as a strip with both houses enlarged beside it). `surface.sheet.show` says the same and wins. */
|
|
361
|
+
show?: SheetShow;
|
|
362
|
+
}
|
|
363
|
+
/** What `curlingHeat` takes. */
|
|
364
|
+
export interface CurlingHeatOptions extends ChartOptions {
|
|
365
|
+
/** The house the stones are at: the `left` one (the top house of a tall drawing, the default) or the `right`. */
|
|
366
|
+
house?: "left" | "right";
|
|
367
|
+
/** The unit of the stones' places: feet by default. */
|
|
368
|
+
units?: Unit;
|
|
369
|
+
/** Only this team's stones. Both by default. */
|
|
370
|
+
team?: "home" | "away";
|
|
371
|
+
/** Which stones: `all` of those in play (the default), `guards` (the free guards only: between the tee line and the hog line, outside the house) or `house` (those in or touching it). */
|
|
372
|
+
only?: "all" | "guards" | "house";
|
|
373
|
+
/** How the stones are drawn: `heatmap` (counts in square cells, the default), `hexbin`, `contour` (a density), `zones` (counted into the button, the rings and the free guard zone) or `discs` (each stone a translucent disc in its team's colour, so that stones that fall on each other build up colour). */
|
|
374
|
+
view?: "heatmap" | "hexbin" | "contour" | "zones" | "discs";
|
|
375
|
+
/** The width of a cell of the `heatmap`, or the radius of a hexagon of the `hexbin`, in feet. 1 foot by default. */
|
|
376
|
+
cell?: number;
|
|
377
|
+
/** How much of the sheet to draw, as for `curlingStones`: `half` by default. */
|
|
378
|
+
show?: SheetShow;
|
|
379
|
+
/** The teams' names, for the summary. */
|
|
380
|
+
names?: {
|
|
381
|
+
home?: string;
|
|
382
|
+
away?: string;
|
|
383
|
+
};
|
|
384
|
+
/** Write the number of stones in each cell, hexagon or zone. Off by default, except for the zones. */
|
|
385
|
+
labels?: boolean;
|
|
386
|
+
}
|
|
387
|
+
/** The numbers behind a heat chart of stones. */
|
|
388
|
+
export interface CurlingHeatTotals {
|
|
389
|
+
/** Stones with a place, and the stones in play among them. */
|
|
390
|
+
stones: number;
|
|
391
|
+
inPlay: number;
|
|
392
|
+
/** The stones drawn: in play, and of the team and the kind asked for. */
|
|
393
|
+
drawn: number;
|
|
394
|
+
/** Of those in play, the free guards and the stones in the house, for each team. */
|
|
395
|
+
guards: {
|
|
396
|
+
home: number;
|
|
397
|
+
away: number;
|
|
398
|
+
};
|
|
399
|
+
house: {
|
|
400
|
+
home: number;
|
|
401
|
+
away: number;
|
|
402
|
+
};
|
|
403
|
+
/** Each zone of the house and the free guard zone, with the stones drawn that came to rest in it and their share of all drawn, as a percent. */
|
|
404
|
+
zones: {
|
|
405
|
+
id: string;
|
|
406
|
+
name: string;
|
|
407
|
+
stones: number;
|
|
408
|
+
share: number | null;
|
|
409
|
+
}[];
|
|
410
|
+
/** Rows left out for want of a place. */
|
|
411
|
+
skipped: number;
|
|
360
412
|
}
|
|
361
413
|
/** One stone as the chart judged it. */
|
|
362
414
|
export interface CurlingStoneResult {
|
package/dist/court.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type League } from "./leagues.ts";
|
|
2
|
+
import { type BadmintonFigures, type BadmintonLeague, type FreeZone, type NetLeague, type NetSport, type NetballFigures, type NetballLeague, type PadelFigures, type PadelLeague, type PickleballFigures, type PickleballLeague, type RacquetballFigures, type RacquetballLeague, type RoundnetFigures, type RoundnetLeague, type SepakTakrawFigures, type SepakTakrawLeague, type SquashFigures, type SquashLeague, type TableTennisFigures, type TableTennisLeague, type TennisFigures, type TennisLeague, type TeqballFigures, type TeqballLeague, type VolleyballFigures, type VolleyballLeague } from "./leagues/courts.ts";
|
|
2
3
|
import type { SurfaceOptions } from "./types.ts";
|
|
3
4
|
/** A rectangle as `[x0, y0, x1, y1]`, in feet. */
|
|
4
5
|
export type Rect = readonly [x0: number, y0: number, x1: number, y1: number];
|
|
@@ -16,10 +17,46 @@ export interface NetLine {
|
|
|
16
17
|
y0: number;
|
|
17
18
|
y1: number;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A line drawn as a circle or an arc of one: `r` is the outer radius (the rulebooks measure to the outside of a line), `w` the line's width and `a0` to `a1` the arc,
|
|
22
|
+
* in radians from the +x axis towards +y (down the page), the whole circle when they are missing.
|
|
23
|
+
*/
|
|
24
|
+
export interface Ring {
|
|
25
|
+
cx: number;
|
|
26
|
+
cy: number;
|
|
27
|
+
r: number;
|
|
28
|
+
w: number;
|
|
29
|
+
a0?: number;
|
|
30
|
+
a1?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Something that stands round or beside the court, as a rectangle: a wall (solid), the metal fence between a padel court's walls (`mesh`), the front wall of a squash court
|
|
34
|
+
* laid out flat beside it (`face`) with its tin (`tin`).
|
|
35
|
+
*/
|
|
36
|
+
export interface Band {
|
|
37
|
+
rect: Rect;
|
|
38
|
+
kind: "wall" | "mesh" | "face" | "tin";
|
|
39
|
+
}
|
|
40
|
+
/** A solid object on the court: a teqball table (a rectangle) or a roundnet set (a disc). */
|
|
41
|
+
export type Solid = {
|
|
42
|
+
kind: "table";
|
|
43
|
+
rect: Rect;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "set";
|
|
46
|
+
cx: number;
|
|
47
|
+
cy: number;
|
|
48
|
+
r: number;
|
|
49
|
+
};
|
|
19
50
|
/** The looks each sport has, the sport's default first. */
|
|
20
51
|
export declare const LOOKS: Readonly<Record<NetSport, readonly string[]>>;
|
|
52
|
+
/** How each sport's board counts: in sets, in games, or (netball) as one total, as any other score is drawn. */
|
|
53
|
+
export declare const BOARD: Readonly<Record<NetSport, "sets" | "games" | "total">>;
|
|
21
54
|
/** True for a sport whose court is one of these. */
|
|
22
55
|
export declare const isNetSport: (sport: string) => sport is NetSport;
|
|
56
|
+
/** True for a league of one of these courts. */
|
|
57
|
+
export declare const isNetLeague: (league: League) => league is NetLeague;
|
|
58
|
+
/** True for a sport whose score is a board of sets or games (all of these courts but netball's). */
|
|
59
|
+
export declare const hasSetsBoard: (sport: string) => sport is NetSport;
|
|
23
60
|
interface Common {
|
|
24
61
|
/** The look: a name from `LOOKS`. */
|
|
25
62
|
look: string;
|
|
@@ -51,6 +88,34 @@ export type CourtPlan = (Common & {
|
|
|
51
88
|
sport: "tabletennis";
|
|
52
89
|
league: TableTennisLeague;
|
|
53
90
|
figures: TableTennisFigures;
|
|
91
|
+
}) | (Common & {
|
|
92
|
+
sport: "padel";
|
|
93
|
+
league: PadelLeague;
|
|
94
|
+
figures: PadelFigures;
|
|
95
|
+
}) | (Common & {
|
|
96
|
+
sport: "squash";
|
|
97
|
+
league: SquashLeague;
|
|
98
|
+
figures: SquashFigures;
|
|
99
|
+
}) | (Common & {
|
|
100
|
+
sport: "racquetball";
|
|
101
|
+
league: RacquetballLeague;
|
|
102
|
+
figures: RacquetballFigures;
|
|
103
|
+
}) | (Common & {
|
|
104
|
+
sport: "sepaktakraw";
|
|
105
|
+
league: SepakTakrawLeague;
|
|
106
|
+
figures: SepakTakrawFigures;
|
|
107
|
+
}) | (Common & {
|
|
108
|
+
sport: "teqball";
|
|
109
|
+
league: TeqballLeague;
|
|
110
|
+
figures: TeqballFigures;
|
|
111
|
+
}) | (Common & {
|
|
112
|
+
sport: "roundnet";
|
|
113
|
+
league: RoundnetLeague;
|
|
114
|
+
figures: RoundnetFigures;
|
|
115
|
+
}) | (Common & {
|
|
116
|
+
sport: "netball";
|
|
117
|
+
league: NetballLeague;
|
|
118
|
+
figures: NetballFigures;
|
|
54
119
|
});
|
|
55
120
|
/**
|
|
56
121
|
* The court the options ask for. A `league` picks the rulebook (the sport's own by default; `fivb-beach` for beach volleyball); `net.look` a named look, `net.free`
|
|
@@ -73,10 +138,21 @@ export interface CourtGeometry {
|
|
|
73
138
|
areas: Rect[];
|
|
74
139
|
/** Short lines outside the court (the attack line's extension, the service zone's limits), a part of `lines` that is drawn only in the full detail. */
|
|
75
140
|
small: Rect[];
|
|
76
|
-
net
|
|
141
|
+
/** The net across the middle, or `null` for a court without one (squash, racquetball, roundnet's circle, netball). */
|
|
142
|
+
net: NetLine | null;
|
|
77
143
|
posts: Fixture[];
|
|
78
144
|
antennae: Fixture[];
|
|
79
145
|
sticks: Fixture[];
|
|
146
|
+
/** Lines drawn as circles and arcs (sepak takraw, netball, roundnet). */
|
|
147
|
+
rings: Ring[];
|
|
148
|
+
/** Walls, fences and the like round or beside the court, outside its rectangle. */
|
|
149
|
+
bands: Band[];
|
|
150
|
+
/** A table or a set standing on the court. */
|
|
151
|
+
solids: Solid[];
|
|
152
|
+
/** Lines painted on a wall, laid flat beside the court (squash's front wall): they may be another colour than the floor's. */
|
|
153
|
+
wallLines: Rect[];
|
|
154
|
+
/** The width of a line that runs beneath the net from side to side (volleyball's and sepak takraw's centre line): it shows between the net's edges. */
|
|
155
|
+
underNet: number | null;
|
|
80
156
|
}
|
|
81
157
|
/**
|
|
82
158
|
* Where everything stands, from the figures of the league in feet. A line's rectangle covers the line's width: the court's dimensions are to the outside edge of the
|
|
@@ -86,4 +162,11 @@ export interface CourtGeometry {
|
|
|
86
162
|
export declare function courtGeometry(plan: CourtPlan): CourtGeometry;
|
|
87
163
|
/** How far the court's fixtures reach beyond its side lines: the net's posts and the antennae's marks. */
|
|
88
164
|
export declare function sideReach(geometry: CourtGeometry): number;
|
|
165
|
+
/** How far the walls, fences and the like stand beyond each edge of the court's rectangle: nothing for a court without any. */
|
|
166
|
+
export declare function bandReach(geometry: CourtGeometry): {
|
|
167
|
+
left: number;
|
|
168
|
+
right: number;
|
|
169
|
+
top: number;
|
|
170
|
+
bottom: number;
|
|
171
|
+
};
|
|
89
172
|
export {};
|