• 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>
336 lines
13 KiB
JavaScript
336 lines
13 KiB
JavaScript
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(); |