Files
signalk-plugin/plugin/tools/kiosk/template-loader.js
Giuseppe Raffa bb8d267cd4 Aggiunta stili CSS per Kiosk, struttura HTML per la Mappa e Riferimenti ai Sensori
• Creato un nuovo file CSS per gli stili del chiosco (kiosk) con variabili, stili per le schede (card) e animazioni.
• Aggiunto un file HTML per l'interfaccia della mappa utilizzando Mapbox, inclusi gli stili e il JavaScript per le funzionalità della mappa.
• Introdotto un file JSON per i riferimenti ai sensori, definendo percorsi ed elementi per i dati di temperatura, vento, onde, posizione, batteria, motore e sistema.

Co-authored-by: Copilot <copilot@github.com>
2026-04-23 16:19:11 +02:00

167 lines
4.8 KiB
JavaScript

/**
* Kiosk template loader + renderer (display-only).
* Espone window.kiosk con: loadTemplate, applyInline, patchBox, addBox, removeBox, updateValue.
*/
(function () {
const COLS = 24, ROWS = 18;
const canvasEl = document.getElementById('canvas');
const chip = document.getElementById('statusChip');
const state = {
template: null,
boxes: [], // array con .el attaccato
byPath: new Map(), // path → Set<box>
sensorCode: null,
sensorName: null,
apiUrl: null,
};
function setStatus(msg, err) {
chip.textContent = msg;
chip.classList.toggle('err', !!err);
}
function indexPaths() {
state.byPath.clear();
for (const b of state.boxes) {
if (!b.path) continue;
if (!state.byPath.has(b.path)) state.byPath.set(b.path, new Set());
state.byPath.get(b.path).add(b);
}
}
function renderBox(b) {
const uw = canvasEl.clientWidth / COLS;
const uh = canvasEl.clientHeight / ROWS;
b.el.style.left = (b.x * uw) + 'px';
b.el.style.top = (b.y * uh) + 'px';
b.el.style.width = (b.w * uw) + 'px';
b.el.style.height = (b.h * uh) + 'px';
b.el.style.background = b.color || '#1e293b';
b.el.style.color = b.textColor || '#fff';
const titleEl = b.el.querySelector('.title');
const valEl = b.el.querySelector('.val');
titleEl.textContent = b.title || (b.path ? b.path.split('.').pop() : '');
// adatta font-size al box
valEl.style.fontSize = Math.min(b.w * uw, b.h * uh) * 0.35 + 'px';
if (b._lastVal !== undefined) renderValue(b, b._lastVal);
else valEl.innerHTML = '<span style="opacity:.4">—</span>';
}
function renderValue(b, value) {
const valEl = b.el.querySelector('.val');
if (value == null || (typeof value === 'object' && !('longitude' in value))) {
valEl.innerHTML = '<span style="opacity:.4">—</span>';
return;
}
let v = value;
if (typeof v === 'number') {
const mul = typeof b.multiplier === 'number' ? b.multiplier : 1;
const dec = typeof b.decimals === 'number' ? b.decimals : 1;
v = (v * mul).toFixed(dec);
} else if (v && typeof v === 'object' && 'longitude' in v) {
v = v.latitude.toFixed(3) + ', ' + v.longitude.toFixed(3);
}
valEl.innerHTML = String(v) + (b.unit ? `<span class="unit">${b.unit}</span>` : '');
}
function createBoxEl() {
const el = document.createElement('div');
el.className = 'box';
el.innerHTML = '<div class="title"></div><div class="val"></div>';
canvasEl.appendChild(el);
return el;
}
function clearAll() {
for (const b of state.boxes) b.el.remove();
state.boxes = [];
state.byPath.clear();
}
function applyContent(content) {
clearAll();
if (content?.background) document.body.style.background = content.background;
for (const raw of content?.boxes || []) {
const b = { ...raw, el: createBoxEl() };
state.boxes.push(b);
renderBox(b);
}
indexPaths();
}
async function loadTemplate(templateId) {
try {
const url = templateId
? `${state.apiUrl}/kiosk/templates/${templateId}`
: `${state.apiUrl}/kiosk/template/active`;
const r = await fetch(url);
if (!r.ok) { setStatus('no template (' + r.status + ')', true); return null; }
const tpl = await r.json();
state.template = tpl;
applyContent(tpl.content);
setStatus('template ' + tpl.name);
return tpl;
} catch (e) {
setStatus('fetch error', true);
return null;
}
}
function patchBox(boxId, patch) {
const b = state.boxes.find(x => x.id === boxId);
if (!b) return false;
Object.assign(b, patch);
indexPaths();
renderBox(b);
return true;
}
function addBox(boxDef) {
const b = { ...boxDef, el: createBoxEl() };
state.boxes.push(b);
renderBox(b);
indexPaths();
return true;
}
function removeBox(boxId) {
const i = state.boxes.findIndex(b => b.id === boxId);
if (i < 0) return false;
state.boxes[i].el.remove();
state.boxes.splice(i, 1);
indexPaths();
return true;
}
function applyInline(content) {
applyContent(content);
return true;
}
function updateValue(path, value) {
const set = state.byPath.get(path);
if (!set) return;
for (const b of set) {
b._lastVal = value;
renderValue(b, value);
}
}
function init({ apiUrl, sensorCode, sensorName }) {
state.apiUrl = apiUrl;
state.sensorCode = sensorCode;
state.sensorName = sensorName;
}
function currentTemplateId() { return state.template?.id || null; }
let resizeRaf;
window.addEventListener('resize', () => {
cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(() => state.boxes.forEach(renderBox));
});
window.kiosk = { init, loadTemplate, patchBox, addBox, removeBox, applyInline, updateValue, currentTemplateId, setStatus };
})();