|
@@ -616,22 +616,206 @@ async function toggleAway() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// --- Vahelehed (tabid): Andurid | Transport ---
|
|
|
|
|
|
|
+// --- Värvisensor (AS7341, uus67.varvus) ---
|
|
|
|
|
+let varvActive = false;
|
|
|
|
|
+let chartVarvSpectrum = null;
|
|
|
|
|
+let chartVarvChroma = null;
|
|
|
|
|
+let varvLatest = [];
|
|
|
|
|
+let varvSelected = 0;
|
|
|
|
|
+
|
|
|
|
|
+const VARV_BANDS = [
|
|
|
|
|
+ ["f1", "415 nm", "#7b2ff7"],
|
|
|
|
|
+ ["f2", "445 nm", "#3b5bdb"],
|
|
|
|
|
+ ["f3", "480 nm", "#1c7ed6"],
|
|
|
|
|
+ ["f4", "515 nm", "#2fb344"],
|
|
|
|
|
+ ["f5", "555 nm", "#9acd32"],
|
|
|
|
|
+ ["f6", "590 nm", "#f0b000"],
|
|
|
|
|
+ ["f7", "630 nm", "#e8590c"],
|
|
|
|
|
+ ["f8", "680 nm", "#c92a2a"],
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+async function loadVarv() {
|
|
|
|
|
+ const hint = document.getElementById("varv-hint");
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [r1, r2] = await Promise.all([
|
|
|
|
|
+ fetch("/api/varv/latest?limit=60"),
|
|
|
|
|
+ fetch("/api/varv/history?hours=24"),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ if (!r1.ok || !r2.ok) throw new Error("HTTP " + r1.status + "/" + r2.status);
|
|
|
|
|
+ varvLatest = (await r1.json()).rows || [];
|
|
|
|
|
+ const history = (await r2.json()).rows || [];
|
|
|
|
|
+ if (varvSelected >= varvLatest.length) varvSelected = 0;
|
|
|
|
|
+ renderVarvCurrent();
|
|
|
|
|
+ renderVarvGrid();
|
|
|
|
|
+ renderVarvTimeline(history);
|
|
|
|
|
+ renderVarvSpectrum();
|
|
|
|
|
+ renderVarvChroma(varvLatest);
|
|
|
|
|
+ const upd = document.getElementById("varv-updated");
|
|
|
|
|
+ if (upd) upd.textContent = varvLatest.length ? "Viimane mõõtmine " + varvLatest[0].ts : "Andmeid pole";
|
|
|
|
|
+ if (hint) hint.hidden = true;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e);
|
|
|
|
|
+ if (hint) { hint.hidden = false; hint.textContent = "Värviandmete laadimine ebaõnnestus."; }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const varvFmt = (v) => (v == null ? "–" : Number(v).toFixed(3));
|
|
|
|
|
+
|
|
|
|
|
+function renderVarvCurrent() {
|
|
|
|
|
+ const sw = document.getElementById("varv-current-swatch");
|
|
|
|
|
+ const hex = document.getElementById("varv-hex");
|
|
|
|
|
+ const meta = document.getElementById("varv-current-meta");
|
|
|
|
|
+ if (!sw) return;
|
|
|
|
|
+ if (!varvLatest.length) {
|
|
|
|
|
+ sw.style.background = "transparent";
|
|
|
|
|
+ if (hex) hex.textContent = "#------";
|
|
|
|
|
+ if (meta) meta.textContent = "Andmeid pole.";
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const c = varvLatest[0];
|
|
|
|
|
+ sw.style.background = c.rgb_hex;
|
|
|
|
|
+ if (hex) hex.textContent = c.rgb_hex.toUpperCase();
|
|
|
|
|
+ if (meta) meta.innerHTML =
|
|
|
|
|
+ `${c.ts}<br>RGB ${c.r} · ${c.g} · ${c.b}<br>` +
|
|
|
|
|
+ `x = ${varvFmt(c.x_chrom)} · y = ${varvFmt(c.y_chrom)} · gain ${c.gain}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderVarvGrid() {
|
|
|
|
|
+ const grid = document.getElementById("varv-grid");
|
|
|
|
|
+ if (!grid) return;
|
|
|
|
|
+ grid.innerHTML = varvLatest.map((c, i) =>
|
|
|
|
|
+ `<button type="button" class="swatch${i === varvSelected ? " sel" : ""}" ` +
|
|
|
|
|
+ `style="background:${c.rgb_hex}" title="${c.ts} ${c.rgb_hex}" data-i="${i}"></button>`
|
|
|
|
|
+ ).join("");
|
|
|
|
|
+ grid.querySelectorAll(".swatch").forEach((el) => {
|
|
|
|
|
+ el.addEventListener("click", () => {
|
|
|
|
|
+ varvSelected = Number(el.dataset.i) || 0;
|
|
|
|
|
+ renderVarvGrid();
|
|
|
|
|
+ renderVarvSpectrum();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderVarvTimeline(rows) {
|
|
|
|
|
+ const el = document.getElementById("varv-timeline");
|
|
|
|
|
+ if (!el) return;
|
|
|
|
|
+ if (!rows.length) {
|
|
|
|
|
+ el.innerHTML = '<span class="varv-sub">Viimase 24 h andmeid pole.</span>';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ el.innerHTML = rows.map((r) => {
|
|
|
|
|
+ const d = new Date(r.t);
|
|
|
|
|
+ const hh = pad(d.getHours()) + ":00";
|
|
|
|
|
+ return `<div class="swatch tl" style="background:${r.rgb_hex}" ` +
|
|
|
|
|
+ `title="${r.ts.slice(0, 10)} ${hh} ${r.rgb_hex} · ${r.n} mõõtmist"></div>`;
|
|
|
|
|
+ }).join("");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderVarvSpectrum() {
|
|
|
|
|
+ const el = document.getElementById("chart-varv-spectrum");
|
|
|
|
|
+ if (!el || !varvLatest.length) { if (chartVarvSpectrum) chartVarvSpectrum.clear(); return; }
|
|
|
|
|
+ const c = varvLatest[Math.min(varvSelected, varvLatest.length - 1)];
|
|
|
|
|
+ const label = document.getElementById("varv-spectrum-label");
|
|
|
|
|
+ if (label) label.textContent = `— ${c.ts} · ${c.rgb_hex.toUpperCase()}`;
|
|
|
|
|
+ if (!chartVarvSpectrum) chartVarvSpectrum = echarts.init(el, null, { renderer: "canvas" });
|
|
|
|
|
+ chartVarvSpectrum.setOption({
|
|
|
|
|
+ grid: { left: 56, right: 16, top: 16, bottom: 32 },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: "category",
|
|
|
|
|
+ data: VARV_BANDS.map((b) => b[1]),
|
|
|
|
|
+ axisLabel: { color: cssVar("--muted") },
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: "value",
|
|
|
|
|
+ axisLabel: { color: cssVar("--muted") },
|
|
|
|
|
+ splitLine: { lineStyle: { color: cssVar("--border") } },
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: { trigger: "axis" },
|
|
|
|
|
+ series: [{
|
|
|
|
|
+ type: "bar",
|
|
|
|
|
+ barWidth: "62%",
|
|
|
|
|
+ data: VARV_BANDS.map((b) => ({
|
|
|
|
|
+ value: c.bands[b[0]] || 0,
|
|
|
|
|
+ itemStyle: { color: b[2] },
|
|
|
|
|
+ })),
|
|
|
|
|
+ }],
|
|
|
|
|
+ }, true);
|
|
|
|
|
+ chartVarvSpectrum.resize();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderVarvChroma(rows) {
|
|
|
|
|
+ const el = document.getElementById("chart-varv-chroma");
|
|
|
|
|
+ if (!el) return;
|
|
|
|
|
+ if (!chartVarvChroma) chartVarvChroma = echarts.init(el, null, { renderer: "canvas" });
|
|
|
|
|
+ const pts = rows
|
|
|
|
|
+ .filter((r) => r.x_chrom != null && r.y_chrom != null)
|
|
|
|
|
+ .map((r) => [Number(r.x_chrom), Number(r.y_chrom), r.ts, r.rgb_hex]);
|
|
|
|
|
+ chartVarvChroma.setOption({
|
|
|
|
|
+ grid: { left: 52, right: 24, top: 20, bottom: 40 },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: "value", min: 0, max: 0.75, name: "x",
|
|
|
|
|
+ nameTextStyle: { color: cssVar("--muted") },
|
|
|
|
|
+ axisLabel: { color: cssVar("--muted") },
|
|
|
|
|
+ splitLine: { lineStyle: { color: cssVar("--border") } },
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: "value", min: 0, max: 0.85, name: "y",
|
|
|
|
|
+ nameTextStyle: { color: cssVar("--muted") },
|
|
|
|
|
+ axisLabel: { color: cssVar("--muted") },
|
|
|
|
|
+ splitLine: { lineStyle: { color: cssVar("--border") } },
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ formatter: (p) => {
|
|
|
|
|
+ if (p.seriesName === "D65") return "valge punkt D65";
|
|
|
|
|
+ return `${p.value[3]}<br>x=${p.value[0].toFixed(3)} y=${p.value[1].toFixed(3)}<br>${p.value[2]}`;
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: "mõõtmised",
|
|
|
|
|
+ type: "scatter",
|
|
|
|
|
+ symbolSize: 11,
|
|
|
|
|
+ data: pts,
|
|
|
|
|
+ itemStyle: { color: "rgba(31,111,235,0.7)" },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: "D65",
|
|
|
|
|
+ type: "scatter",
|
|
|
|
|
+ symbol: "pin",
|
|
|
|
|
+ symbolSize: 26,
|
|
|
|
|
+ data: [[0.3127, 0.329]],
|
|
|
|
|
+ itemStyle: { color: "#8b949e" },
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ }, true);
|
|
|
|
|
+ chartVarvChroma.resize();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+window.addEventListener("resize", () => {
|
|
|
|
|
+ if (chartVarvSpectrum) chartVarvSpectrum.resize();
|
|
|
|
|
+ if (chartVarvChroma) chartVarvChroma.resize();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// --- Vahelehed (tabid): Andurid | Transport | Värv ---
|
|
|
const transport = { active: false, days: 1 };
|
|
const transport = { active: false, days: 1 };
|
|
|
|
|
+const VIEWS = ["andurid", "transport", "varv"];
|
|
|
|
|
|
|
|
function switchView(view) {
|
|
function switchView(view) {
|
|
|
- const isTransport = view === "transport";
|
|
|
|
|
|
|
+ if (!VIEWS.includes(view)) view = "andurid";
|
|
|
document.querySelectorAll("#tabs .tab").forEach((b) => {
|
|
document.querySelectorAll("#tabs .tab").forEach((b) => {
|
|
|
b.classList.toggle("active", b.dataset.view === view);
|
|
b.classList.toggle("active", b.dataset.view === view);
|
|
|
});
|
|
});
|
|
|
- const vAnd = document.getElementById("view-andurid");
|
|
|
|
|
- const vTr = document.getElementById("view-transport");
|
|
|
|
|
- if (vAnd) vAnd.hidden = isTransport;
|
|
|
|
|
- if (vTr) vTr.hidden = !isTransport;
|
|
|
|
|
- transport.active = isTransport;
|
|
|
|
|
|
|
+ for (const v of VIEWS) {
|
|
|
|
|
+ const el = document.getElementById("view-" + v);
|
|
|
|
|
+ if (el) el.hidden = v !== view;
|
|
|
|
|
+ }
|
|
|
|
|
+ transport.active = view === "transport";
|
|
|
|
|
+ varvActive = view === "varv";
|
|
|
if (location.hash !== "#" + view) history.replaceState(null, "", "#" + view);
|
|
if (location.hash !== "#" + view) history.replaceState(null, "", "#" + view);
|
|
|
- if (isTransport) {
|
|
|
|
|
|
|
+ if (view === "transport") {
|
|
|
loadTransport();
|
|
loadTransport();
|
|
|
|
|
+ } else if (view === "varv") {
|
|
|
|
|
+ loadVarv();
|
|
|
} else {
|
|
} else {
|
|
|
if (chart) chart.resize();
|
|
if (chart) chart.resize();
|
|
|
if (chartEnergy) chartEnergy.resize();
|
|
if (chartEnergy) chartEnergy.resize();
|
|
@@ -657,7 +841,10 @@ function initTabs() {
|
|
|
setInterval(() => {
|
|
setInterval(() => {
|
|
|
if (transport.active) loadTransport();
|
|
if (transport.active) loadTransport();
|
|
|
}, 60000);
|
|
}, 60000);
|
|
|
- switchView(location.hash === "#transport" ? "transport" : "andurid");
|
|
|
|
|
|
|
+ setInterval(() => {
|
|
|
|
|
+ if (varvActive) loadVarv();
|
|
|
|
|
+ }, 20000);
|
|
|
|
|
+ switchView(VIEWS.includes(location.hash.slice(1)) ? location.hash.slice(1) : "andurid");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function loadTransport() {
|
|
function loadTransport() {
|