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 = `${type === 'map' ? 'Mappa' : (finalPath ? finalPath.split('.').pop() : 'Widget')}`;
// Suggerimento menu path se widget
if (type === 'widget') {
let menuHtml = `
`;
headerHtml = `${headerHtml}${menuHtml}
`;
}
el.innerHTML = `
`;
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();