/** * tile-renderer.js — registra renderer per ogni tipo di tile. * * Ogni renderer espone: * mount(el, tile) → setup DOM iniziale * bind(el, tile, binder) → sottoscrizione ai path → ritorna unbind() * * Tipi supportati: value | gauge | map */ (function () { const renderers = {}; function register(type, r) { renderers[type] = r; } function get(type) { return renderers[type]; } function format(value, { decimals = 1 } = {}) { if (value == null || Number.isNaN(value)) return '—'; if (typeof value === 'number') { const factor = Math.pow(10, decimals); return (Math.round(value * factor) / factor).toString(); } return String(value); } // ============================== VALUE ================================== register('value', { mount(el, tile) { const d = tile.display || {}; el.innerHTML = `
${escapeHtml(d.title || tile.source?.path || '')}
${d.unit ? `${escapeHtml(d.unit)}` : ''}
`; const val = el.querySelector('[data-val]'); if (d.font) val.style.fontFamily = d.font; if (d.fontSize) val.style.fontSize = d.fontSize; if (d.color) val.style.color = d.color; if (d.bg) el.style.background = d.bg; }, bind(el, tile, binder) { const num = el.querySelector('[data-num]'); const off = binder.subscribe(tile.source.path, (v) => { num.textContent = format(v, { decimals: tile.display?.decimals ?? 1 }); el.classList.remove('stale'); }); const staleTimer = setInterval(() => { const l = binder.getLatest(tile.source.path); if (!l && !el.classList.contains('stale')) el.classList.add('stale'); }, 5000); return () => { off(); clearInterval(staleTimer); }; } }); // ============================== GAUGE ================================== // SVG gauge semplice (arco 270°, lancetta). Range = tile.range = [min, max]. register('gauge', { mount(el, tile) { const d = tile.display || {}; el.innerHTML = `
${escapeHtml(d.title || tile.source?.path || '')}
${escapeHtml(d.unit || '')} `; if (d.bg) el.style.background = d.bg; }, bind(el, tile, binder) { const [min, max] = tile.range || [0, 100]; const arc = el.querySelector('[data-arc]'); const needle = el.querySelector('[data-needle]'); const num = el.querySelector('[data-num]'); // pre-compute arc length (perimetro semicerchio R=80) const totalLen = 376.99; // ~ 80 * Math.PI * 1.5 (270deg) arc.setAttribute('stroke-dasharray', `${totalLen} ${totalLen}`); const off = binder.subscribe(tile.source.path, (v) => { const num_v = (v == null || Number.isNaN(+v)) ? null : Number(v); num.textContent = format(num_v, { decimals: tile.display?.decimals ?? 1 }); if (num_v == null) return; const ratio = Math.max(0, Math.min(1, (num_v - min) / (max - min))); arc.setAttribute('stroke-dashoffset', String(totalLen * (1 - ratio))); // needle: da -135° a +135° const deg = -135 + 270 * ratio; needle.setAttribute('transform', `rotate(${deg} 100 120)`); el.classList.remove('stale'); }); return off; } }); // ============================== MAP ==================================== let mapboxLoadingPromise = null; function loadMapbox() { if (window.mapboxgl) return Promise.resolve(); if (mapboxLoadingPromise) return mapboxLoadingPromise; mapboxLoadingPromise = new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://api.mapbox.com/mapbox-gl-js/v3.6.0/mapbox-gl.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); return mapboxLoadingPromise; } register('map', { mount(el, tile) { const d = tile.display || {}; el.innerHTML = `
${escapeHtml(d.title || 'Mappa')}
`; if (d.bg) el.style.background = d.bg; }, bind(el, tile, binder) { const mapHost = el.querySelector('[data-map]'); const overlays = el.querySelector('[data-overlays]'); const extras = Array.isArray(tile.extras) ? tile.extras : []; // pre-popola overlay const extraEls = extras.map((ex, i) => { const span = document.createElement('span'); span.dataset.idx = i; span.innerHTML = `${escapeHtml(ex.label || ex.path)}: ${escapeHtml(ex.unit || '')}`; overlays.appendChild(span); return span.querySelector('[data-ex]'); }); let map = null, marker = null; const mapboxKey = document.querySelector('meta[name="mapbox-key"]')?.content || ''; const offs = []; loadMapbox().then(() => { if (!window.mapboxgl) return; window.mapboxgl.accessToken = mapboxKey; map = new window.mapboxgl.Map({ container: mapHost, style: { version: 8, sources: { osm: { type:'raster', tiles:['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'], tileSize:256 }, openseamap: { type:'raster', tiles:['https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png'], tileSize:256 }, }, layers: [ { id:'osm-layer', type:'raster', source:'osm', minzoom:0, maxzoom:22 }, { id:'sea-layer', type:'raster', source:'openseamap', minzoom:0, maxzoom:18 }, ], }, center: [0, 0], zoom: 2, }); marker = new window.mapboxgl.Marker({ color: tile.display?.color || '#ef4444' }) .setLngLat([0, 0]).addTo(map); }).catch(err => console.warn('[KIOSK|MAP] mapbox load failed:', err)); let curLat = null, curLon = null; const update = () => { if (curLat == null || curLon == null || !map || !marker) return; marker.setLngLat([curLon, curLat]); map.flyTo({ center: [curLon, curLat], zoom: Math.max(13, map.getZoom()) }); }; offs.push(binder.subscribe(tile.source.latPath, v => { curLat = Number(v); update(); })); offs.push(binder.subscribe(tile.source.lonPath, v => { curLon = Number(v); update(); })); // extras extras.forEach((ex, i) => { offs.push(binder.subscribe(ex.path, v => { extraEls[i].textContent = format(v, { decimals: ex.decimals ?? 1 }); })); }); return () => { offs.forEach(o => o && o()); if (map) { try { map.remove(); } catch (_) {} map = null; } }; } }); function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } window.tileRenderer = { register, get }; })();