Skip to content

Commit 85527eb

Browse files
committed
Fixes and version bump
1 parent d8245f1 commit 85527eb

11 files changed

Lines changed: 447 additions & 59 deletions

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,10 @@ GET /aquanote/files/{compet_id}/{run_id}/{filename}
145145

146146
### Sportsdata CSV formats
147147

148-
The Configuration tab has two Sportsdata CSV options:
148+
The Configuration tab has Sportsdata format selectors with a `Strict` toggle next to each one:
149149

150-
* **Sportsdata load schema** filters the CSV files shown for the selected run. Aquanote validates CSV headers in the current run folder against the selected Sportsdata swimming schema before adding them to the data dropdown. Static mode can only discover files listed in metadata or `flat.json`.
150+
* **Sportsdata JSON format** selects the preferred metadata model for sportsdata JSON files. In non-strict mode, Aquanote will still try to load partial Swimflow-like metadata and fill missing race fields with defaults.
151+
* **Sportsdata load schema** filters the CSV files shown for the selected run. Aquanote validates CSV headers in the current run folder against the selected Sportsdata swimming schema before adding them to the data dropdown. In non-strict mode, a detected sportsdata CSV can still load even when validation reports schema issues; those issues are kept as diagnostics instead of blocking the run. Static mode can only discover files listed in metadata or `flat.json`.
151152
* **Sportsdata save format** controls the columns used by the download button. The default is **Swimming tracking CSV**; **Swimming basic tracking CSV** writes `frameId,swimmerId,eventId,time,distance`.
152153

153154

assets/css/global-refactor.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,31 @@ input:checked + .slider:before {
14871487
margin: 0;
14881488
}
14891489

1490+
.configuration-field-group {
1491+
display: flex;
1492+
align-items: center;
1493+
gap: 12px;
1494+
min-width: 0;
1495+
}
1496+
1497+
.configuration-field-group select,
1498+
.configuration-field-group input[type="url"] {
1499+
flex: 1 1 auto;
1500+
}
1501+
1502+
.configuration-inline-toggle {
1503+
display: inline-flex;
1504+
align-items: center;
1505+
gap: 6px;
1506+
font-weight: 500;
1507+
white-space: nowrap;
1508+
}
1509+
1510+
.configuration-inline-toggle input {
1511+
width: auto;
1512+
margin: 0;
1513+
}
1514+
14901515
.configuration-actions {
14911516
display: flex;
14921517
gap: 12px;

assets/js/aquanote-providers.js

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,51 @@ let staticProviderData = {
2121
aliases: {},
2222
};
2323

24+
async function fetchJsonCandidates(candidates, fetcher, description) {
25+
const failures = [];
26+
for (const candidate of candidates) {
27+
try {
28+
return await fetcher(candidate);
29+
} catch (error) {
30+
failures.push(`${candidate}: ${error?.message || String(error)}`);
31+
}
32+
}
33+
throw new Error(`No matching metadata found for ${description}. Tried: ${failures.join("; ")}`);
34+
}
35+
36+
function runDirectoryName(run) {
37+
return String(run || "").trim().replace(/\.json$/i, "");
38+
}
39+
40+
function metadataCandidates(run, entries = []) {
41+
const names = new Set();
42+
const rawRun = String(run || "").trim();
43+
const normalizedRun = runDirectoryName(rawRun);
44+
if (rawRun.toLowerCase().endsWith(".json")) {
45+
names.add(rawRun);
46+
}
47+
if (normalizedRun) {
48+
names.add(`${normalizedRun}.json`);
49+
}
50+
names.add("meta.json");
51+
for (const entry of entries) {
52+
const name = String(entry?.name || "").trim();
53+
if (!name.toLowerCase().endsWith(".json")) {
54+
continue;
55+
}
56+
if (name === `${normalizedRun}.json` || name === "meta.json" || name.includes(normalizedRun)) {
57+
names.add(name);
58+
}
59+
}
60+
for (const entry of entries) {
61+
const name = String(entry?.name || "").trim();
62+
if (name.toLowerCase().endsWith(".json")) {
63+
names.add(name);
64+
}
65+
}
66+
return [...names];
67+
}
68+
2469
export function setStaticProviderData(data) {
2570
staticProviderData = {
2671
competitions: Array.isArray(data?.competitions) ? data.competitions : [],
@@ -37,21 +82,25 @@ function makeStaticProvider(basePath) {
3782
return {
3883
getCompets: async () => staticProviderData.competitions,
3984
getRuns: async (comp) => staticProviderData.runs[comp] ?? [],
40-
getDatas: async (comp, run) => staticProviderData.csvFiles[run] ?? [],
85+
getDatas: async (comp, run) => staticProviderData.csvFiles[runDirectoryName(run)] ?? [],
4186
getQuality: async () => [],
42-
loadRunJson: async (comp, run) => {
43-
const url = `${basePath}${comp}/${run}/${run}.json`;
44-
const res = await fetch(url);
45-
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
46-
return res.json();
47-
},
87+
loadRunJson: async (comp, run) => fetchJsonCandidates(
88+
metadataCandidates(run, staticProviderData.csvFiles[runDirectoryName(run)] ?? []),
89+
async (filename) => {
90+
const url = `${basePath}${comp}/${runDirectoryName(run)}/${filename}`;
91+
const res = await fetch(url);
92+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
93+
return res.json();
94+
},
95+
`${run}.json`
96+
),
4897
fetchCsv: async (comp, run, filename) => {
49-
const url = `${basePath}${comp}/${run}/${filename}`;
98+
const url = `${basePath}${comp}/${runDirectoryName(run)}/${filename}`;
5099
const res = await fetch(url);
51100
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
52101
return parseCsvText(await res.text());
53102
},
54-
getVideoUrl: (comp, run, filename) => `${basePath}${comp}/${run}/${filename}`,
103+
getVideoUrl: (comp, run, filename) => `${basePath}${comp}/${runDirectoryName(run)}/${filename}`,
55104
};
56105
}
57106

@@ -71,26 +120,34 @@ function makeHttpProvider(baseUrl) {
71120
return Array.isArray(data) ? data.filter(d => d.type === "directory") : [];
72121
},
73122
getDatas: async (comp, run) => {
74-
const data = await apiFetch(`/getDatas/${comp}/${run}`);
123+
const data = await apiFetch(`/getDatas/${comp}/${runDirectoryName(run)}`);
75124
return Array.isArray(data) ? data.filter(d => d.type === "file") : [];
76125
},
77126
getQuality: async (comp, run, side) => {
78-
const data = await apiFetch(`/getQuality/${comp}/${run}`);
127+
const data = await apiFetch(`/getQuality/${comp}/${runDirectoryName(run)}`);
79128
if (!Array.isArray(data)) return [];
80129
const key = side === "droite" ? "fixeDroite" : "fixeGauche";
81130
return data.filter(d => d.type === "file" && d.name.includes(key));
82131
},
83132
loadRunJson: async (comp, run) => {
84-
const res = await fetch(`${baseUrl}/files/${comp}/${run}/${run}.json`);
85-
if (!res.ok) throw new Error(`API JSON ${res.status}: ${comp}/${run}`);
86-
return res.json();
133+
const runDirectory = runDirectoryName(run);
134+
const entries = await apiFetch(`/getDatas/${comp}/${runDirectory}`).catch(() => []);
135+
return fetchJsonCandidates(
136+
metadataCandidates(run, entries),
137+
async (filename) => {
138+
const res = await fetch(`${baseUrl}/files/${comp}/${runDirectory}/${filename}`);
139+
if (!res.ok) throw new Error(`API JSON ${res.status}: ${comp}/${runDirectory}/${filename}`);
140+
return res.json();
141+
},
142+
`${run}.json`
143+
);
87144
},
88145
fetchCsv: async (comp, run, filename) => {
89-
const res = await fetch(`${baseUrl}/files/${comp}/${run}/${filename}`);
146+
const res = await fetch(`${baseUrl}/files/${comp}/${runDirectoryName(run)}/${filename}`);
90147
if (!res.ok) throw new Error(`API CSV ${res.status}: ${filename}`);
91148
return parseCsvText(await res.text());
92149
},
93-
getVideoUrl: (comp, run, filename) => `${baseUrl}/files/${comp}/${run}/${filename}`,
150+
getVideoUrl: (comp, run, filename) => `${baseUrl}/files/${comp}/${runDirectoryName(run)}/${filename}`,
94151
};
95152
}
96153

@@ -100,17 +157,25 @@ function makeElectronProvider() {
100157
getCompets: async () => window.myAPI.getLocalCompetitions(BASE),
101158
getRuns: async (comp) => window.myAPI.getLocalRuns(BASE, comp),
102159
getDatas: async (comp, run) => {
103-
const files = await window.myAPI.getLocalFiles(BASE, comp, run);
160+
const files = await window.myAPI.getLocalFiles(BASE, comp, runDirectoryName(run));
104161
return files.filter(f => f.name);
105162
},
106163
getQuality: async (comp, run, side) => {
107-
const files = await window.myAPI.getLocalFiles(BASE, comp, run);
164+
const files = await window.myAPI.getLocalFiles(BASE, comp, runDirectoryName(run));
108165
const key = side === "droite" ? "fixeDroite" : "fixeGauche";
109166
return files.filter(f => f.name && f.name.includes(key));
110167
},
111-
loadRunJson: async (comp, run) => window.myAPI.readJsonFile(BASE, comp, run, `${run}.json`),
112-
fetchCsv: async (comp, run, filename) => window.myAPI.readCsvFile(BASE, comp, run, filename),
113-
getVideoUrl: (comp, run, filename) => `${BASE}/${comp}/${run}/${filename}`,
168+
loadRunJson: async (comp, run) => {
169+
const runDirectory = runDirectoryName(run);
170+
const files = await window.myAPI.getLocalFiles(BASE, comp, runDirectory);
171+
return fetchJsonCandidates(
172+
metadataCandidates(run, files),
173+
async (filename) => window.myAPI.readJsonFile(BASE, comp, runDirectory, filename),
174+
`${run}.json`
175+
);
176+
},
177+
fetchCsv: async (comp, run, filename) => window.myAPI.readCsvFile(BASE, comp, runDirectoryName(run), filename),
178+
getVideoUrl: (comp, run, filename) => `${BASE}/${comp}/${runDirectoryName(run)}/${filename}`,
114179
};
115180
}
116181

assets/js/configuration_panel.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import {
55
getDataSourceMode,
66
getLocalServerUrl,
77
getSportsdataJsonFormatId,
8+
getSportsdataJsonStrictMode,
89
getSportsdataLoadFormatId,
9-
getSportsdataSaveFormatId
10+
getSportsdataLoadStrictMode,
11+
getSportsdataSaveFormatId,
12+
getSportsdataSaveStrictMode
1013
} from "./local_api.js";
1114
import { SPORTS_DATA_CSV_FORMATS, SPORTS_DATA_JSON_FORMATS } from "./sportsdata.js";
1215

@@ -44,6 +47,9 @@ function syncConfigurationForm() {
4447
const sportsdataJsonFormatSelect = getElement("config_sportsdata_json_format");
4548
const sportsdataLoadFormatSelect = getElement("config_sportsdata_load_format");
4649
const sportsdataSaveFormatSelect = getElement("config_sportsdata_save_format");
50+
const sportsdataJsonStrictInput = getElement("config_sportsdata_json_strict");
51+
const sportsdataLoadStrictInput = getElement("config_sportsdata_load_strict");
52+
const sportsdataSaveStrictInput = getElement("config_sportsdata_save_strict");
4753

4854
if (sourceSelect) {
4955
sourceSelect.value = getDataSourceMode();
@@ -57,6 +63,15 @@ function syncConfigurationForm() {
5763
syncFormatSelect(sportsdataJsonFormatSelect, getSportsdataJsonFormatId(), SPORTS_DATA_JSON_FORMATS);
5864
syncFormatSelect(sportsdataLoadFormatSelect, getSportsdataLoadFormatId(), SPORTS_DATA_CSV_FORMATS);
5965
syncFormatSelect(sportsdataSaveFormatSelect, getSportsdataSaveFormatId(), SPORTS_DATA_CSV_FORMATS);
66+
if (sportsdataJsonStrictInput) {
67+
sportsdataJsonStrictInput.checked = getSportsdataJsonStrictMode();
68+
}
69+
if (sportsdataLoadStrictInput) {
70+
sportsdataLoadStrictInput.checked = getSportsdataLoadStrictMode();
71+
}
72+
if (sportsdataSaveStrictInput) {
73+
sportsdataSaveStrictInput.checked = getSportsdataSaveStrictMode();
74+
}
6075
}
6176

6277
function applyConfiguration() {
@@ -66,6 +81,9 @@ function applyConfiguration() {
6681
const sportsdataJsonFormat = getElement("config_sportsdata_json_format")?.value || DEFAULTS.sportsdataJsonFormat;
6782
const sportsdataLoadFormat = getElement("config_sportsdata_load_format")?.value || DEFAULTS.sportsdataLoadFormat;
6883
const sportsdataSaveFormat = getElement("config_sportsdata_save_format")?.value || DEFAULTS.sportsdataSaveFormat;
84+
const sportsdataJsonStrict = getElement("config_sportsdata_json_strict")?.checked ?? DEFAULTS.sportsdataJsonStrict;
85+
const sportsdataLoadStrict = getElement("config_sportsdata_load_strict")?.checked ?? DEFAULTS.sportsdataLoadStrict;
86+
const sportsdataSaveStrict = getElement("config_sportsdata_save_strict")?.checked ?? DEFAULTS.sportsdataSaveStrict;
6987

7088
setStatus("Applying configuration...", "ready");
7189
window.location.assign(buildDataSourceUrl({
@@ -74,7 +92,10 @@ function applyConfiguration() {
7492
apiBaseUrl,
7593
sportsdataJsonFormat,
7694
sportsdataLoadFormat,
77-
sportsdataSaveFormat
95+
sportsdataSaveFormat,
96+
sportsdataJsonStrict,
97+
sportsdataLoadStrict,
98+
sportsdataSaveStrict
7899
}));
79100
}
80101

@@ -86,7 +107,10 @@ function resetConfiguration() {
86107
apiBaseUrl: DEFAULTS.apiBaseUrl,
87108
sportsdataJsonFormat: DEFAULTS.sportsdataJsonFormat,
88109
sportsdataLoadFormat: DEFAULTS.sportsdataLoadFormat,
89-
sportsdataSaveFormat: DEFAULTS.sportsdataSaveFormat
110+
sportsdataSaveFormat: DEFAULTS.sportsdataSaveFormat,
111+
sportsdataJsonStrict: DEFAULTS.sportsdataJsonStrict,
112+
sportsdataLoadStrict: DEFAULTS.sportsdataLoadStrict,
113+
sportsdataSaveStrict: DEFAULTS.sportsdataSaveStrict
90114
}));
91115
}
92116

assets/js/cycles_handler.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { displayMode, selected_swim, temp_start, selected_cycle, last_checkpoint, vue_du_dessus } from './refactor-script.js';
7-
import { findVideoByType, megaData, curr_swims, frame_rate, pool_size, n_camera, turn_distances, turn_times, getDisplayLaneIndex } from './loader.js';
7+
import { findVideoByType, megaData, curr_swims, frame_rate, pool_size, n_camera, turn_distances, turn_times } from './loader.js';
88
import { draw_stats } from './side_views.js';
99
import { updateTable } from './main.js';
1010
import { construct_modify_selected_annotation_table } from './data_handler.js';
@@ -72,7 +72,7 @@ export function makeBar(data, idx, idswim, scale, elemSize, vidSize, meta) {
7272
can.setAttribute("class", "crop_can")
7373
can.setAttribute("swim", idswim)
7474
can.setAttribute("num", idx)
75-
let pts = getBar([data.x, data.y], meta, getDisplayLaneIndex(idswim, meta))
75+
let pts = getBar([data.x, data.y], meta, idswim)
7676
const displayStart = videoPointToDisplay(pts[0], meta);
7777
const displayScale = displayStart?.k ?? (elemSize[0] / vidSize[0]);
7878

@@ -92,6 +92,7 @@ export function makeBar(data, idx, idswim, scale, elemSize, vidSize, meta) {
9292
can.style["top"] = displayStart ? `${displayStart.y}px` : (scale[1](pts[0][1])) + "%";
9393
can.style["left"] = displayStart ? `${displayStart.x}px` : (scale[0](pts[0][0])) + "%";
9494

95+
can.style["transform-origin"] = "1px 0px";
9596
can.style["transform"] = "rotate(" + get_orr(pts[0], pts[1]) + "deg)"
9697

9798
let div = document.createElement("div");
@@ -106,6 +107,7 @@ export function makeBar(data, idx, idswim, scale, elemSize, vidSize, meta) {
106107
div.style["left"] = displayStart ? `${displayStart.x - displayScale}px` : (scale[0](pts[0][0] - 1) + 0.4) + "%";
107108
div.style["top"] = displayStart ? `${displayStart.y + 3 * displayScale}px` : (scale[1](pts[0][1]) + 3) + "%";
108109
div.style["height"] = can.height + "px"
110+
div.style["transform-origin"] = "1px 0px";
109111
div.style["transform"] = "rotate(" + get_orr(pts[0], pts[1]) + "deg)"
110112
}
111113
return [can, div]

0 commit comments

Comments
 (0)