• 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>
167 lines
4.8 KiB
JavaScript
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 };
|
|
})();
|