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>
This commit is contained in:
336
plugin/tools/kiosk/canvas.js
Normal file
336
plugin/tools/kiosk/canvas.js
Normal file
@@ -0,0 +1,336 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user