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 = `
`; skPaths.forEach(p => { menuHtml += `
${p.split('.').pop()}
`; }); menuHtml += `
`; headerHtml = `
${headerHtml}${menuHtml}
`; } el.innerHTML = `
${headerHtml}
`; 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();