Files
signalk-plugin/plugin/tools/kiosk/canvas.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

336 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const COLS = 24, ROWS = 18;
const SNAP = 0.5;
const SNAP_MAG = 0.3;
const MIN_GW = 2, MIN_GH = 1.5;
const MAX_GW = 20, MAX_GH = 16;
const DEF_GW = 6, DEF_GH = 5;
const canvasEl = document.getElementById('canvas');
const tooltipEl = document.getElementById('tooltip');
const emptyState = document.getElementById('emptyState');
const cardCountEl = document.getElementById('cardCount');
const unitBadge = document.getElementById('unitBadge');
const modalOvl = document.getElementById('modalOverlay');
const modalTitle = document.getElementById('modalTitle');
const importArea = document.getElementById('importArea');
const modalApply = document.getElementById('modalApply');
const toastEl = document.getElementById('toast');
let cards = [], cardIdCounter = 0, selectedCard = null, zCounter = 1;
let snapGuidesH = [], snapGuidesV = [];
let editMode = false;
const uw = () => canvasEl.clientWidth / COLS;
const uh = () => canvasEl.clientHeight / ROWS;
const gSnap = v => Math.round(v / SNAP) * SNAP;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
function screenToGrid(cx, cy) {
const r = canvasEl.getBoundingClientRect();
return { gx: (cx - r.left) / uw(), gy: (cy - r.top) / uh() };
}
function renderCard(c) {
const u = uw(), h = uh();
c.el.style.left = (c.gx * u) + 'px';
c.el.style.top = (c.gy * h) + 'px';
c.el.style.width = (c.gw * u) + 'px';
c.el.style.height = (c.gh * h) + 'px';
if (editMode) c.el.classList.add('editable');
else c.el.classList.remove('editable', 'selected');
}
function renderAll() {
cards.forEach(renderCard);
unitBadge.textContent = `1u = ${Math.round(uw())}px`;
}
function updateCount() {
const n = cards.length;
cardCountEl.textContent = `${n} card${n !== 1 ? 's' : ''}`;
emptyState.classList.toggle('hidden', n > 0);
}
// Responsive re-render
let rafId = null;
window.addEventListener('resize', () => {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(renderAll);
});
// Toast
let toastT = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastT);
toastT = setTimeout(() => toastEl.classList.remove('show'), 2200);
}
// Guides
function ensureGuides() {
if (snapGuidesH.length) return;
for (let i = 0; i < 2; i++) {
let g = document.createElement('div'); g.className = 'guide snap-guide horizontal'; canvasEl.appendChild(g); snapGuidesH.push(g);
g = document.createElement('div'); g.className = 'guide snap-guide vertical'; canvasEl.appendChild(g); snapGuidesV.push(g);
}
}
function hideGuides() {
snapGuidesH.forEach(g => g.classList.remove('visible')); snapGuidesV.forEach(g => g.classList.remove('visible'));
}
function showGuide(type, gp, idx = 0) {
if (type === 'h' && idx < snapGuidesH.length) { snapGuidesH[idx].style.top = (gp * uh()) + 'px'; snapGuidesH[idx].classList.add('visible'); }
else if (type === 'v' && idx < snapGuidesV.length) { snapGuidesV[idx].style.left = (gp * uw()) + 'px'; snapGuidesV[idx].classList.add('visible'); }
}
// Magnetic snap
function magSnap(el, gx, gy, gw, gh) {
let sx = gx, sy = gy, gH = [], gV = [];
const others = cards.filter(c => c.el !== el);
let bH = SNAP_MAG + 1;
for (const o of others)
for (const [f, t] of [[gy, o.gy], [gy, o.gy + o.gh], [gy + gh, o.gy], [gy + gh, o.gy + o.gh]]) {
const d = Math.abs(f - t);
if (d < bH) { bH = d; sy = gy + (t - f); gH = [t]; }
}
let bV = SNAP_MAG + 1;
for (const o of others)
for (const [f, t] of [[gx, o.gx], [gx, o.gx + o.gw], [gx + gw, o.gx], [gx + gw, o.gx + o.gw]]) {
const d = Math.abs(f - t);
if (d < bV) { bV = d; sx = gx + (t - f); gV = [t]; }
}
if (bH > SNAP_MAG) sy = gSnap(gy);
if (bV > SNAP_MAG) sx = gSnap(gx);
return { gx: sx, gy: sy, guidesH: gH, guidesV: gV };
}
// Signal K data handling
function updateData(path, value) {
cards.filter(c => c.path === path).forEach(c => {
const body = c.el.querySelector('.card-body');
if (body) {
let displayVal = value;
if (path.includes('speed')) displayVal = (value * 1.94384).toFixed(1) + ' kn';
else if (path.includes('depth')) displayVal = value.toFixed(1) + ' m';
body.textContent = displayVal;
}
});
}
window.updateKioskData = updateData;
// Create card
function createCard(gx, gy, gw = DEF_GW, gh = DEF_GH, forceId = null, gz = null, type = 'widget', path = null) {
const id = forceId || (++cardIdCounter);
if (forceId && forceId >= cardIdCounter) cardIdCounter = forceId;
if (!forceId && id > cardIdCounter) cardIdCounter = id;
const skPaths = window.skPaths || [];
const finalPath = path || (type === 'widget' && skPaths.length ? skPaths[(id - 1) % skPaths.length] : null);
const el = document.createElement('div');
el.className = 'card spawning' + (editMode ? ' editable' : '');
el.dataset.id = id;
el.dataset.type = type;
const z = gz || (++zCounter);
el.style.zIndex = z;
if (gz && gz >= zCounter) zCounter = gz;
let headerHtml = `<span class="card-label">${type === 'map' ? 'Mappa' : (finalPath ? finalPath.split('.').pop() : 'Widget')}</span>`;
// Suggerimento menu path se widget
if (type === 'widget') {
let menuHtml = `<div class="path-menu">`;
skPaths.forEach(p => {
menuHtml += `<div class="path-option" data-path="${p}">${p.split('.').pop()}</div>`;
});
menuHtml += `</div>`;
headerHtml = `<div class="label-wrapper">${headerHtml}${menuHtml}</div>`;
}
el.innerHTML = `
<div class="card-header">
${headerHtml}
<button class="card-close" title="Rimuovi">ELIMINA</button>
</div>
<div class="card-body"></div>
<div class="rh corner nw"></div><div class="rh corner ne"></div>
<div class="rh corner se"></div><div class="rh corner sw"></div>
<div class="rh edge n"></div><div class="rh edge s"></div>
<div class="rh edge e"></div><div class="rh edge w"></div>`;
canvasEl.appendChild(el);
const c = { id, el, gx, gy, gw, gh, type, path: finalPath };
cards.push(c);
renderCard(c);
if (type === 'map') {
const mapDiv = document.createElement('div');
mapDiv.id = `map-container-${id}`;
mapDiv.className = 'card-map-canvas';
el.querySelector('.card-body').appendChild(mapDiv);
if (window.initMapInstance) window.initMapInstance(mapDiv.id);
} else {
updateBody(c);
// Listener per il cambio path
el.querySelectorAll('.path-option').forEach(opt => {
opt.addEventListener('click', (e) => {
e.stopPropagation();
c.path = opt.dataset.path;
el.querySelector('.card-label').textContent = c.path.split('.').pop();
toast(`Path aggiornato: ${c.path}`);
});
});
}
el.addEventListener('animationend', () => el.classList.remove('spawning'), { once: true });
el.querySelector('.card-close').addEventListener('click', ev => { ev.stopPropagation(); removeCard(c); });
el.addEventListener('mousedown', () => { if (editMode) selectCard(c); });
setupDrag(c);
setupResize(c);
updateCount();
return c;
}
function removeCard(c) {
c.el.classList.add('removing');
c.el.addEventListener('animationend', () => {
c.el.remove();
cards = cards.filter(x => x.id !== c.id);
if (selectedCard === c) selectedCard = null;
updateCount();
}, { once: true });
}
function selectCard(c) {
if (selectedCard?.el) selectedCard.el.classList.remove('selected');
selectedCard = c; c.el.classList.add('selected'); c.el.style.zIndex = ++zCounter;
}
function updateBody(c) {
if (c.type === 'map') {
if (window.resizeMapInstance) window.resizeMapInstance();
} else {
const b = c.el.querySelector('.card-body');
if (b && !b.textContent.trim()) {
b.textContent = `${c.gw.toFixed(1)} × ${c.gh.toFixed(1)} u`;
}
}
}
// Drag
function setupDrag(c) {
c.el.addEventListener('mousedown', e => {
if (!editMode) return;
if (e.target.classList.contains('rh') || e.target.classList.contains('card-close')) return;
e.preventDefault(); ensureGuides(); c.el.classList.add('dragging');
const start = screenToGrid(e.clientX, e.clientY);
const oGx = c.gx, oGy = c.gy;
const onMove = ev => {
const now = screenToGrid(ev.clientX, ev.clientY);
let nx = oGx + (now.gx - start.gx), ny = oGy + (now.gy - start.gy);
nx = clamp(nx, 0, COLS - c.gw); ny = clamp(ny, 0, ROWS - c.gh);
const s = magSnap(c.el, nx, ny, c.gw, c.gh);
c.gx = clamp(s.gx, 0, COLS - c.gw); c.gy = clamp(s.gy, 0, ROWS - c.gh);
hideGuides();
s.guidesH.forEach((p, i) => showGuide('h', p, i));
s.guidesV.forEach((p, i) => showGuide('v', p, i));
renderCard(c);
tooltipEl.textContent = `${c.gx.toFixed(1)}, ${c.gy.toFixed(1)}`;
tooltipEl.style.left = (ev.clientX + 14) + 'px'; tooltipEl.style.top = (ev.clientY + 14) + 'px';
tooltipEl.classList.add('visible');
};
const onUp = () => {
c.el.classList.remove('dragging'); hideGuides(); tooltipEl.classList.remove('visible');
document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
if (c.type === 'map' && window.resizeMapInstance) window.resizeMapInstance();
};
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
});
}
// Resize
function setupResize(c) {
c.el.querySelectorAll('.rh').forEach(h => {
h.addEventListener('mousedown', e => {
if (!editMode) return;
e.preventDefault(); e.stopPropagation(); ensureGuides();
c.el.classList.add('resizing'); selectCard(c);
const start = screenToGrid(e.clientX, e.clientY);
const oGx = c.gx, oGy = c.gy, oGw = c.gw, oGh = c.gh;
const isN = h.classList.contains('nw') || h.classList.contains('ne') || h.classList.contains('n');
const isS = h.classList.contains('sw') || h.classList.contains('se') || h.classList.contains('s');
const isW = h.classList.contains('nw') || h.classList.contains('sw') || h.classList.contains('w');
const isE = h.classList.contains('ne') || h.classList.contains('se') || h.classList.contains('e');
const onMove = ev => {
const now = screenToGrid(ev.clientX, ev.clientY);
const dx = now.gx - start.gx, dy = now.gy - start.gy;
let nw = oGw, nh = oGh, nx = oGx, ny = oGy;
if (isE) nw = oGw + dx; if (isS) nh = oGh + dy;
if (isW) { nw = oGw - dx; nx = oGx + dx; }
if (isN) { nh = oGh - dy; ny = oGy + dy; }
nw = gSnap(clamp(nw, MIN_GW, MAX_GW)); nh = gSnap(clamp(nh, MIN_GH, MAX_GH));
if (isW) nx = oGx + oGw - nw; if (isN) ny = oGy + oGh - nh;
nx = gSnap(clamp(nx, 0, COLS - nw)); ny = gSnap(clamp(ny, 0, ROWS - nh));
c.gx = nx; c.gy = ny; c.gw = nw; c.gh = nh;
renderCard(c); updateBody(c);
tooltipEl.textContent = `${nw.toFixed(1)} × ${nh.toFixed(1)} u`;
tooltipEl.style.left = (ev.clientX + 14) + 'px'; tooltipEl.style.top = (ev.clientY + 14) + 'px';
tooltipEl.classList.add('visible');
};
const onUp = () => {
c.el.classList.remove('resizing'); tooltipEl.classList.remove('visible'); hideGuides();
document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
});
});
}
// ═══════════════════════════════════════════════════════
// EXPORT / IMPORT
// ═══════════════════════════════════════════════════════
function exportConfig() {
return JSON.stringify({
canvas: { cols: COLS, rows: ROWS },
cards: cards.map(c => ({
id: c.id, type: c.type,
dimensions: {
x: Math.round(c.gx * 100) / 100, y: Math.round(c.gy * 100) / 100,
z: parseInt(c.el.style.zIndex) || 1,
width: Math.round(c.gw * 100) / 100, height: Math.round(c.gh * 100) / 100
}
}))
}, null, 2);
}
function importConfig(json) {
let data;
try { data = JSON.parse(json); } catch { toast('JSON non valido'); return false; }
if (!data.cards || !Array.isArray(data.cards)) { toast('Formato non valido: serve "cards" array'); return false; }
cards.forEach(c => c.el.remove());
cards = []; selectedCard = null; cardIdCounter = 0; zCounter = 1;
for (const entry of data.cards) {
const d = entry.dimensions || {};
createCard(
clamp(d.x ?? 0, 0, COLS - MIN_GW), clamp(d.y ?? 0, 0, ROWS - MIN_GH),
clamp(d.width ?? DEF_GW, MIN_GW, MAX_GW), clamp(d.height ?? DEF_GH, MIN_GH, MAX_GH),
entry.id ?? null, d.z ?? 1, entry.type ?? 'widget'
);
}
updateCount(); renderAll();
toast(`Importate ${data.cards.length} card`);
return true;
}
updateCount(); renderAll();