'use strict'; /* Main app logic: Qt bridge, GPS readout, waypoints, routes, navigation. */ // ── Modo de iluminación ──────────────────────────────────────────────────── function setMode(mode) { const modes = ['night','dusk','day','dayplus']; if (!modes.includes(mode)) return; // Aplica al html element (igual que ECDIS) if (mode === 'day') { document.documentElement.removeAttribute('data-mode'); } else { document.documentElement.setAttribute('data-mode', mode); } // Marca botón activo modes.forEach(m => { const btn = document.getElementById('mode-' + m); if (btn) btn.classList.toggle('active', m === mode); }); try { localStorage.setItem('gps-mode', mode); } catch(e) {} // Redibujar skyplot con la paleta del nuevo modo if (typeof SkyPlot !== 'undefined') SkyPlot.redraw(); // ── Capas OSM + fondo canvas ───────────────────────────────────────────── // ECDIS correcto: las ayudas IALA NO se filtran — solo el fondo/OSM se oscurece. // El filtro CSS en #map fue eliminado (bug Qt5 WebEngine). En cambio: // · osmLayer.opacity → controla visibilidad de tiles OSM // · #map background → color de océano base en modo oscuro var _osmOp = { night: 0.12, dusk: 0.38, day: 0.82, dayplus: 0.90 }; var _mapBg = { night: '#0a1018', dusk: '#101c30', day: '#a8c8e8', dayplus: '#b8d8f0' }; if (typeof GPSMap !== 'undefined' && GPSMap.setOsmOpacity) { GPSMap.setOsmOpacity(_osmOp[mode] != null ? _osmOp[mode] : 0.82); GPSMap.setMapBackground(_mapBg[mode]); } // ── Paleta S-52 de capas ENC ───────────────────────────────────────────── // Recolorea DEPARE/LNDARE/DEPCNT según modo — ayudas IALA nunca se tocan. if (typeof ChartLayer !== 'undefined' && ChartLayer.setChartMode) { var encMode = mode === 'dayplus' ? 'day' : mode === 'day' ? 'day-std' : mode; ChartLayer.setChartMode(encMode); } } // Restaura modo al cargar (localStorage puede fallar en file://) (function(){ try { const saved = localStorage.getItem('gps-mode') || 'day'; setMode(saved); } catch(e) { setMode('day'); } })(); // ── State ────────────────────────────────────────────────────────────────── let _fix = {}; let _waypoints = []; // [{id,name,lat,lon,notes,mark_type}] let _routes = []; // [{id,name,wpt_ids}] let _marks = []; // [{id,name,lat,lon,mark_type}] — marcas POI (no son WPTs de ruta) let _navWpt = null; // active go-to waypoint let _autoCenter = false; let _chartLoadPending = false; // trigger chart load on first GPS fix let _pendingMarcaType = null; // tipo de marca seleccionado en el modal, pendiente de click en mapa // ── Helpers ──────────────────────────────────────────────────────────────── function _fmtDM(deg, hPos, hNeg) { if (deg == null) return '--°--\'-.--'; const h = deg >= 0 ? hPos : hNeg; const a = Math.abs(deg); const d = Math.floor(a); const m = (a - d) * 60; return `${d}°${m.toFixed(3)}'${h}`; } function _fmtNum(v, dec, unit = '') { return v != null && !isNaN(v) ? Number(v).toFixed(dec) + unit : '--'; } function _bearingTo(lat1, lon1, lat2, lon2) { const φ1 = lat1 * Math.PI/180, φ2 = lat2 * Math.PI/180; const Δλ = (lon2 - lon1) * Math.PI/180; const y = Math.sin(Δλ) * Math.cos(φ2); const x = Math.cos(φ1)*Math.sin(φ2) - Math.sin(φ1)*Math.cos(φ2)*Math.cos(Δλ); return (Math.atan2(y, x) * 180/Math.PI + 360) % 360; } function _distNM(lat1, lon1, lat2, lon2) { const R = 3440.065; // NM const φ1 = lat1 * Math.PI/180, φ2 = lat2 * Math.PI/180; const Δφ = (lat2 - lat1) * Math.PI/180; const Δλ = (lon2 - lon1) * Math.PI/180; const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); } const FIX_NAMES = ['NO FIX','GPS','DGPS','PPS','RTK','Float RTK','DR','Manual','Simul.','WAAS']; const FIX_MODE = ['','','2D','3D']; // ── GPS message handler (called from bridge.js via gpsMessage signal) ────── window.handleGPSMsg = function(msg) { if (msg.type === 'connected') { document.getElementById('lbl-port').textContent = msg.port; document.getElementById('dot-gps').className = 'status-dot dot-ok'; } else if (msg.type === 'no_port') { document.getElementById('lbl-port').textContent = 'NO GPS'; document.getElementById('dot-gps').className = 'status-dot'; } else if (msg.type === 'disconnected' || msg.type === 'error') { document.getElementById('lbl-port').textContent = msg.type === 'error' ? (msg.msg || 'ERROR') : 'DISCONNECTED'; document.getElementById('dot-gps').className = 'status-dot dot-err'; } else if (msg.type === 'position' || msg.type === 'rmc') { _updateFix(msg); } else if (msg.type === 'satellites') { SkyPlot.update(msg.sats); } else if (msg.type === 'dop') { _updateDOP(msg); } else if (msg.type === 'raw') { _appendNMEA(msg.sentence); } else if (msg.type === 'sensor') { if (typeof window.handleSensorMsg === 'function') window.handleSensorMsg(msg); } }; // ── GPS readout ──────────────────────────────────────────────────────────── function _updateFix(msg) { Object.assign(_fix, msg); const lat = _fix.lat, lon = _fix.lon; const fq = _fix.fix_quality ?? 0; document.getElementById('r-lat').textContent = _fmtDM(lat, 'N', 'S'); document.getElementById('r-lon').textContent = _fmtDM(lon, 'E', 'W'); document.getElementById('r-sog').textContent = _fmtNum(_fix.sog, 1, ' kn'); document.getElementById('r-cog').textContent = _fmtNum(_fix.cog, 1, '°'); document.getElementById('r-cogm').textContent= _fmtNum(_fix.cog_m, 1, '°M'); document.getElementById('r-magvar').textContent = _fix.magvar != null ? (_fix.magvar >= 0 ? '+' : '') + _fix.magvar.toFixed(1) + '°' : '--'; document.getElementById('r-alt').textContent = _fmtNum(_fix.altitude, 1, ' m'); document.getElementById('r-hdop').textContent= _fmtNum(_fix.hdop, 1); document.getElementById('r-sats').textContent= _fix.satellites ?? '--'; // Fix badge — colores: rojo=sin fix, verde=GPS, esmeralda=DGPS, cian=RTK const badge = document.getElementById('fix-badge'); const fixName = FIX_NAMES[fq] || `FIX ${fq}`; badge.textContent = fixName; var fixCls = 'fix-none'; if (fq === 0) fixCls = 'fix-none'; else if (fq === 2 || fq === 9) fixCls = 'fix-dgps'; // DGPS / WAAS else if (fq >= 4) fixCls = 'fix-great'; // RTK / Float RTK else fixCls = 'fix-ok'; // GPS normal (fq 1,3) badge.className = 'fix-badge ' + fixCls; // Map update if (lat != null && lon != null && fq > 0) { GPSMap.update(lat, lon, _fix.cog || 0, _fix.sog || 0); if (_autoCenter) GPSMap.centerOnGPS(); _updateNavDisplay(); // Primer fix GPS: cargar cartas para la posición actual. // Backup por si moveend llega antes de que _attachLayers registre el handler. if (_chartLoadPending && typeof ChartLayer !== 'undefined') { _chartLoadPending = false; setTimeout(function() { ChartLayer.loadAll(); }, 500); } } // Sky plot if (msg.sats) SkyPlot.update(msg.sats); } function _updateDOP(msg) { document.getElementById('r-hdop').textContent = _fmtNum(msg.hdop, 1); document.getElementById('r-vdop').textContent = _fmtNum(msg.vdop, 1); document.getElementById('r-pdop').textContent = _fmtNum(msg.pdop, 1); document.getElementById('r-fix').textContent = FIX_MODE[msg.fix_mode] || '--'; } // ── UTC clock ────────────────────────────────────────────────────────────── setInterval(() => { const now = new Date(); const utc = now.toISOString().replace('T',' ').substring(0,19) + ' UTC'; document.getElementById('utc-clock').textContent = utc; }, 1000); // ── Navigation (go-to waypoint) ──────────────────────────────────────────── function startNav(wpt) { _navWpt = wpt; GPSMap.setActiveNav(wpt); _renderWaypoints(); document.getElementById('nav-section').style.display = ''; document.getElementById('nav-wpt-name').textContent = wpt.name; _updateNavDisplay(); } function stopNav() { _navWpt = null; GPSMap.setActiveNav(null); _renderWaypoints(); document.getElementById('nav-section').style.display = 'none'; } function _updateNavDisplay() { if (!_navWpt || _fix.lat == null) return; const brg = _bearingTo(_fix.lat, _fix.lon, _navWpt.lat, _navWpt.lon); const dist = _distNM(_fix.lat, _fix.lon, _navWpt.lat, _navWpt.lon); const sog = _fix.sog || 0; const eta = sog > 0.1 ? (dist / sog * 60).toFixed(0) + ' min' : '--'; document.getElementById('nav-brg').textContent = brg.toFixed(1) + '°'; document.getElementById('nav-dist').textContent = dist.toFixed(2) + ' NM'; document.getElementById('nav-eta').textContent = eta; document.getElementById('nav-xte').textContent = '--'; } // ── Waypoints ────────────────────────────────────────────────────────────── async function _loadWaypoints() { if (!window.py) return; var all = JSON.parse(await _py('get_waypoints')); // Separar WPTs de navegación (sin mark_type) de marcas POI (con mark_type) _waypoints = all.filter(w => !w.mark_type); _marks = all.filter(w => w.mark_type); _renderWaypoints(); _renderMapWaypoints(); if (GPSMap && GPSMap.renderMarks) GPSMap.renderMarks(_marks); _renderMarksList(); } function _renderWaypoints() { const el = document.getElementById('wpt-list'); if (!el) return; if (!_waypoints.length) { el.innerHTML = '
No waypoints saved
'; return; } el.innerHTML = _waypoints.map(w => { const dist = (_fix.lat != null) ? _distNM(_fix.lat, _fix.lon, w.lat, w.lon).toFixed(1) + ' NM' : ''; const brg = (_fix.lat != null) ? _bearingTo(_fix.lat, _fix.lon, w.lat, w.lon).toFixed(0) + '°' : ''; const isActive = _navWpt && _navWpt.id === w.id; return `
${w.name}
${_fmtDM(w.lat,'N','S')} ${_fmtDM(w.lon,'E','W')}
${dist ? `
${brg} ${dist}
` : ''}
`; }).join(''); } function _renderMarksList() { var el = document.getElementById('mark-list'); if (!el) return; if (!_marks.length) { el.innerHTML = '
No hay marcas guardadas
'; return; } var MARK_DEFS = { fishing:'🎣', marina:'⚓', fuel:'⛽', restaurant:'🍴', dive:'🤿', anchorage:'🚢', beach:'🏖️', ramp:'🚤', repair:'🔧', hospital:'🏥', customs:'🛂', danger:'⚠️', hotel:'🏨', poi:'📍' }; el.innerHTML = _marks.map(function(m) { var emoji = MARK_DEFS[m.mark_type] || '📍'; return '
' + '' + emoji + '' + '
' + '
' + m.name + '
' + '
' + _fmtDM(m.lat,'N','S') + ' ' + _fmtDM(m.lon,'E','W') + '
' + '
' + '
' + '' + '' + '' + '
' + '
'; }).join(''); } function _renderMapWaypoints() { GPSMap.renderWaypoints(_waypoints); const wptMap = Object.fromEntries(_waypoints.map(w => [w.id, w])); GPSMap.renderRoutes(_routes, wptMap); } window.onWptMapClick = function(wpt) { openWptModal(wpt); }; // ── Map click handlers (draw modes) ─────────────────────────────────────── let _routeDraftPoints = []; let _routeDraftName = ''; window.onMapClickWpt = async function(lat, lon) { const n = _waypoints.length + 1; openWptModal({ lat, lon, name: `WPT ${String(n).padStart(3,'0')}` }); }; window.onMapClickRoute = async function(lat, lon, idx) { if (!window.py) return; const name = `RP${String(idx).padStart(2,'0')}`; const data = { name, lat, lon, notes: 'Route point' }; const saved = JSON.parse(await _py('save_waypoint', JSON.stringify(data))); _routeDraftPoints.push(saved); await _loadWaypoints(); const btn = document.getElementById('btn-draw-route'); if (btn) btn.textContent = `✔ RTE (${idx})`; }; window.onMapDblClickRoute = async function(coords) { if (_routeDraftPoints.length < 2) { alert('Need at least 2 points for a route'); _cancelDrawMode(); return; } const name = prompt('Route name:', _routeDraftName || `Route ${_routes.length + 1}`); if (!name) { _cancelDrawMode(); return; } const wpt_ids = _routeDraftPoints.map(p => p.id); await _py('save_route', JSON.stringify({ name, wpt_ids })); _routeDraftPoints = []; _routeDraftName = ''; _cancelDrawMode(); await _loadRoutes(); lpTab('rte'); // muestra el tab de rutas }; // ── Draw mode toolbar ────────────────────────────────────────────────────── function _cancelDrawMode() { GPSMap.cancelDraw(); _routeDraftPoints = []; const btnWpt = document.getElementById('btn-draw-wpt'); const btnRte = document.getElementById('btn-draw-route'); if (btnWpt) { btnWpt.classList.remove('active'); btnWpt.textContent = '✚ WPT'; } if (btnRte) { btnRte.classList.remove('active'); btnRte.textContent = '✚ RTE'; } } function toggleDrawWpt() { if (GPSMap.getDrawMode() === 'wpt') { _cancelDrawMode(); return; } _cancelDrawMode(); GPSMap.setDrawMode('wpt'); const btn = document.getElementById('btn-draw-wpt'); if (btn) { btn.classList.add('active'); btn.textContent = '✕ WPT'; } } function toggleDrawRoute() { if (GPSMap.getDrawMode() === 'route') { if (_routeDraftPoints.length >= 2) { window.onMapDblClickRoute([]); } else { _cancelDrawMode(); } return; } _cancelDrawMode(); _routeDraftPoints = []; GPSMap.setDrawMode('route'); const btn = document.getElementById('btn-draw-route'); if (btn) { btn.classList.add('active'); btn.textContent = '✔ RTE (0)'; } } document.addEventListener('keydown', e => { if (e.key === 'Escape' && GPSMap.getDrawMode() !== 'none') _cancelDrawMode(); }); // ── Waypoint modal ───────────────────────────────────────────────────────── window.openWptModal = function(wpt = {}) { document.getElementById('wpt-id').value = wpt.id || ''; document.getElementById('wpt-name').value = wpt.name || ''; document.getElementById('wpt-lat').value = wpt.lat != null ? wpt.lat.toFixed(6) : ''; document.getElementById('wpt-lon').value = wpt.lon != null ? wpt.lon.toFixed(6) : ''; document.getElementById('wpt-notes').value = wpt.notes || ''; showModal('modal-wpt'); }; function addWptFromGPS() { if (_fix.lat == null) { alert('No GPS fix'); return; } const n = _waypoints.length + 1; openWptModal({ lat: _fix.lat, lon: _fix.lon, name: `WPT ${String(n).padStart(3,'0')}` }); } function addWptManual() { openWptModal(); } async function saveWpt() { const name = document.getElementById('wpt-name').value.trim(); const lat = parseFloat(document.getElementById('wpt-lat').value); const lon = parseFloat(document.getElementById('wpt-lon').value); if (!name || isNaN(lat) || isNaN(lon)) { alert('Name, lat and lon are required'); return; } const data = { id: document.getElementById('wpt-id').value || undefined, name, lat, lon, notes: document.getElementById('wpt-notes').value.trim(), }; await _py('save_waypoint', JSON.stringify(data)); closeModal(); await _loadWaypoints(); } async function deleteWpt(id) { if (!confirm('Delete waypoint?')) return; window.py.delete_waypoint(id); // void — fire and forget if (_navWpt && _navWpt.id === id) stopNav(); await _loadWaypoints(); } // ── Routes ───────────────────────────────────────────────────────────────── async function _loadRoutes() { if (!window.py) return; _routes = JSON.parse(await _py('get_routes')); _renderRoutes(); _renderMapWaypoints(); } function _renderRoutes() { const el = document.getElementById('route-list'); if (!el) return; if (!_routes.length) { el.innerHTML = '
No routes saved
'; return; } const wptMap = Object.fromEntries(_waypoints.map(w => [w.id, w])); el.innerHTML = _routes.map(r => { const wpts = (r.wpt_ids || []).map(id => wptMap[id]?.name || id).join(' → '); return `
${r.name}
${wpts}
`; }).join(''); } function newRoute() { document.getElementById('rte-name').value = ''; document.getElementById('rte-id').value = ''; const sel = document.getElementById('rte-wpt-selector'); sel.innerHTML = _waypoints.map(w => ` `).join(''); showModal('modal-route'); } async function saveRoute() { const name = document.getElementById('rte-name').value.trim(); if (!name) { alert('Name required'); return; } const checks = document.querySelectorAll('#rte-wpt-selector input:checked'); const wpt_ids = [...checks].map(c => c.value); if (wpt_ids.length < 2) { alert('Select at least 2 waypoints'); return; } const data = { id: document.getElementById('rte-id').value || undefined, name, wpt_ids, }; await _py('save_route', JSON.stringify(data)); closeModal(); await _loadRoutes(); } async function deleteRoute(id) { if (!confirm('Delete route?')) return; window.py.delete_route(id); // void await _loadRoutes(); } // ── NMEA log ─────────────────────────────────────────────────────────────── const _nmeaBuf = []; function _escapeHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function _appendNMEA(line) { const color = line.startsWith('$GP') || line.startsWith('$GN') ? '#4ade80' : line.startsWith('$GL') ? '#f87171' : line.startsWith('$GA') ? '#34d399' : '#94a3b8'; // Escape the raw NMEA sentence before inserting into innerHTML to prevent XSS _nmeaBuf.push(`${_escapeHtml(line)}`); if (_nmeaBuf.length > 200) _nmeaBuf.shift(); // Actualiza en tiempo real solo si el tab NMEA está activo const nmeaContent = document.getElementById('lp-nmea'); if (nmeaContent && !nmeaContent.classList.contains('hidden')) { const el = document.getElementById('nmea-log'); if (el) { el.innerHTML = _nmeaBuf.join('\n'); el.scrollTop = el.scrollHeight; } } } // ── Port connect modal ───────────────────────────────────────────────────── async function showConnectModal() { if (!window.py) { alert('Bridge not ready'); return; } const ports = JSON.parse(await _py('list_ports')); const sel = document.getElementById('sel-port'); sel.innerHTML = ports.length ? ports.map(p => ``).join('') : ''; showModal('modal-connect'); } function doConnect() { const port = document.getElementById('sel-port').value; const baud = parseInt(document.getElementById('sel-baud').value); if (!port) return; window.py.connect_gps(port, baud); // void — GPS connected msg arrives via signal document.getElementById('lbl-port').textContent = port; closeModal(); } function doDisconnect() { window.py.disconnect_gps(); // void document.getElementById('lbl-port').textContent = 'DISCONNECTED'; document.getElementById('dot-gps').className = 'status-dot dot-err'; closeModal(); } // ── Panel izquierdo — sistema de tabs ───────────────────────────────────── const _LP_TABS = ['gps', 'wpt', 'rte', 'mrk', 'nmea']; function lpTab(tab) { try { _LP_TABS.forEach(t => { const content = document.getElementById('lp-' + t); const btn = document.getElementById('lptab-' + t); if (content) content.classList.toggle('hidden', t !== tab); if (btn) btn.classList.toggle('active', t === tab); }); // Acciones al abrir cada tab if (tab === 'wpt') _renderWaypoints(); if (tab === 'rte') _renderRoutes(); if (tab === 'mrk') _renderMarksList(); if (tab === 'nmea') { const el = document.getElementById('nmea-log'); if (el && _nmeaBuf.length) { el.innerHTML = _nmeaBuf.join('\n'); el.scrollTop = el.scrollHeight; } } } catch(e) { console.error('[lpTab]', e); } } // Compatibilidad: si algo llama togglePanel apunta al tab equivalente function togglePanel(panelId) { if (panelId === 'panel-waypoints') lpTab('wpt'); else if (panelId === 'panel-routes') lpTab('rte'); else if (panelId === 'panel-nmea') lpTab('nmea'); } // ── Modal helpers ────────────────────────────────────────────────────────── function showModal(id) { document.querySelectorAll('.modal').forEach(m => m.classList.add('hidden')); document.getElementById(id).classList.remove('hidden'); document.getElementById('modal-overlay').classList.remove('hidden'); } function closeModal() { document.getElementById('modal-overlay').classList.add('hidden'); } document.getElementById('modal-overlay').addEventListener('click', e => { if (e.target === document.getElementById('modal-overlay')) closeModal(); }); // ── Track ────────────────────────────────────────────────────────────────── async function _loadTrack() { if (!window.py) return; const pts = JSON.parse(await _py('get_track', 2000)); GPSMap.loadTrack(pts); } // ── Charts ───────────────────────────────────────────────────────────────── let _chartCells = []; async function showChartsModal() { showModal('modal-charts'); // abre el modal siempre, aunque falle el refresh await _refreshChartCells(); } async function _refreshChartCells() { const el = document.getElementById('chart-cell-list'); if (!window.py) { if (el) el.innerHTML = '
Bridge not ready — restart app
'; return; } try { _chartCells = JSON.parse(await _py('get_chart_cells')); _renderChartCells(); } catch(e) { console.error('[charts] refresh failed:', e); if (el) el.innerHTML = '
Error: ' + e + '
'; } } function _renderChartCells() { const el = document.getElementById('chart-cell-list'); if (!el) return; if (!_chartCells.length) { el.innerHTML = '
No charts installed
'; return; } el.innerHTML = _chartCells.map(c => { const bbox = c.bbox ? c.bbox.map(v => v.toFixed(2)).join(', ') : '--'; return `
${c.id} ${c.features} features
bbox: ${bbox}
`; }).join(''); } async function uploadChart() { if (!window.py) return; const btn = document.getElementById('btn-upload-chart'); btn.textContent = 'OPENING…'; btn.disabled = true; try { const res = JSON.parse(await _py('open_chart_file_dialog')); let msg = ''; if (res.installed && res.installed.length) msg += `Installed: ${res.installed.join(', ')}\n`; if (res.skipped && res.skipped.length) msg += `Already installed (skipped): ${res.skipped.join(', ')}\n`; if (res.errors && res.errors.length) msg += `Errors:\n` + res.errors.map(e => ` ${e.file}: ${e.error}`).join('\n'); if (!msg) msg = 'No charts installed (dialog cancelled or no valid files).'; if (res.installed && res.installed.length) { alert(msg.trim()); await ChartLayer.reloadAll(); await _refreshChartCells(); } } catch(e) { alert('Upload error: ' + e); } finally { btn.textContent = 'UPLOAD'; btn.disabled = false; } } async function scanChartsPath() { if (!window.py) return; const inp = document.getElementById('chart-path-inp'); const path = inp.value.trim(); if (!path) { alert('Enter a folder path (e.g. E:\\ENC_Charts)'); return; } const btn = document.getElementById('btn-scan-chart'); btn.textContent = 'SCANNING…'; btn.disabled = true; try { const res = JSON.parse(await _py('scan_charts_path', path)); let msg = ''; if (res.installed && res.installed.length) msg += `Installed: ${res.installed.join(', ')}\n`; if (res.skipped && res.skipped.length) msg += `Already installed (skipped): ${res.skipped.join(', ')}\n`; if (res.errors && res.errors.length) msg += `Errors:\n` + res.errors.map(e => ` ${e.file}: ${e.error}`).join('\n'); if (!msg) msg = 'No .000 or .zip chart files found in that folder.'; alert(msg.trim()); if (res.installed && res.installed.length) { await ChartLayer.reloadAll(); await _refreshChartCells(); } } catch(e) { alert('Scan error: ' + e); } finally { btn.textContent = 'SCAN'; btn.disabled = false; } } async function deleteChart(cellId) { if (!confirm(`Delete chart cell "${cellId}"?`)) return; window.py.delete_chart(cellId); // void await ChartLayer.reloadAll(); await _refreshChartCells(); } async function setChartRegion(cellId, region) { await _py('set_chart_region', cellId, region); await ChartLayer.reloadAll(); } // ── ENC layers modal (AVANZADO) ──────────────────────────────────────────── function openEncLayersModal() { ChartLayer.setDetailLevel('advanced'); var adv = ChartLayer.getAdvLayers(); // Profundidades document.getElementById('el-depare').checked = adv.depare; document.getElementById('el-depcnt').checked = adv.depcnt; document.getElementById('el-soundg').checked = adv.soundg; // Peligros y zonas document.getElementById('el-hazards').checked = adv.hazards; document.getElementById('el-zones').checked = adv.zones; // Tierra y costa document.getElementById('el-coalne').checked = adv.coalne; document.getElementById('el-landmask').checked = adv.landmask; document.getElementById('el-lndare').checked = adv.lndare; document.getElementById('el-buaare').checked = adv.buaare; // Mapa base document.getElementById('el-osm').checked = adv.osm; showModal('modal-enc-layers'); } function applyEncLayers() { ChartLayer.setLayerVisibility({ // Profundidades depare: document.getElementById('el-depare').checked, depcnt: document.getElementById('el-depcnt').checked, soundg: document.getElementById('el-soundg').checked, // Peligros y zonas hazards: document.getElementById('el-hazards').checked, zones: document.getElementById('el-zones').checked, // Tierra y costa coalne: document.getElementById('el-coalne').checked, landmask: document.getElementById('el-landmask').checked, lndare: document.getElementById('el-lndare').checked, buaare: document.getElementById('el-buaare').checked, // Mapa base osm: document.getElementById('el-osm').checked, }); closeModal(); } // ── Chart-under-cursor indicator ────────────────────────────────────────── (function _initChartCursor() { /* Espera a que el mapa esté listo */ function _setup() { if (!window.GPSMap || !GPSMap.getOLMap) return setTimeout(_setup, 500); const olMap = GPSMap.getOLMap(); const info = document.getElementById('map-chart-info'); if (!info) return; let _hideTimer = null; olMap.on('pointermove', function (evt) { if (!_chartCells.length) return; const [lon, lat] = ol.proj.toLonLat(evt.coordinate); const hits = _chartCells.filter(function (c) { if (!c.bbox || c.bbox.length < 4) return false; return lon >= c.bbox[0] && lat >= c.bbox[1] && lon <= c.bbox[2] && lat <= c.bbox[3]; }); if (hits.length) { info.textContent = '⛵ ' + hits.map(function(c){ return c.id; }).join(' · '); info.classList.add('visible'); clearTimeout(_hideTimer); _hideTimer = setTimeout(function(){ info.classList.remove('visible'); }, 3000); } else { clearTimeout(_hideTimer); info.classList.remove('visible'); } }); } _setup(); })(); // ── Sensor data (compass HDG, ecosonda depth/temp) ──────────────────────── window.handleSensorMsg = function (msg) { /* msg viene de bridge.py via gpsMessage con type:'sensor' Formatos esperados: HDT: {type:'sensor', src:'HDT', hdg_t: 123.4} HDM: {type:'sensor', src:'HDM', hdg_m: 123.4} DBT/DPT: {type:'sensor', src:'DBT', depth: 12.3} MTW: {type:'sensor', src:'MTW', water_temp: 28.5} */ if (msg.hdg_t != null) { const e = document.getElementById('r-hdg-t'); if (e) e.textContent = msg.hdg_t.toFixed(1) + '°T'; } if (msg.hdg_m != null) { const e = document.getElementById('r-hdg-m'); if (e) e.textContent = msg.hdg_m.toFixed(1) + '°M'; } if (msg.depth != null) { const e = document.getElementById('r-depth'); if (e) e.textContent = msg.depth.toFixed(1) + ' m'; } if (msg.water_temp != null) { const e = document.getElementById('r-water-temp'); if (e) e.textContent = msg.water_temp.toFixed(1) + '°C'; } }; // ── MARCAS POI ───────────────────────────────────────────────────────────── var _selectedMarcaType = null; window.openMarcaModal = function() { _selectedMarcaType = null; document.querySelectorAll('.marca-item').forEach(function(el) { el.classList.remove('selected'); }); var hint = document.getElementById('marca-type-hint'); if (hint) hint.textContent = '— Ningún tipo seleccionado —'; var btn = document.getElementById('btn-marca-ok'); if (btn) btn.disabled = true; showModal('modal-marca'); }; window.selectMarcaType = function(el) { document.querySelectorAll('.marca-item').forEach(function(e) { e.classList.remove('selected'); }); el.classList.add('selected'); _selectedMarcaType = el.getAttribute('data-type'); var hint = document.getElementById('marca-type-hint'); if (hint) hint.textContent = '✔ ' + el.querySelector('.marca-label').textContent + ' seleccionado'; var btn = document.getElementById('btn-marca-ok'); if (btn) btn.disabled = false; }; window.startMarcaDraw = function() { if (!_selectedMarcaType) return; _pendingMarcaType = _selectedMarcaType; closeModal(); // Activar modo de dibujo MARCA (reutiliza el draw-mode del mapa) GPSMap.setDrawMode('mark'); var btn = document.getElementById('btn-draw-mark'); if (btn) { btn.classList.add('active'); btn.textContent = '📍...'; } }; // Callback cuando el usuario hace click en mapa en modo mark window.onMapClickMark = async function(lat, lon) { if (!window.py || !_pendingMarcaType) return; var n = _marks.length + 1; var typeLabels = { fishing:'PESCA', marina:'MARINA', fuel:'COMBUST', restaurant:'REST', dive:'BUCEO', anchorage:'FONDEO', beach:'PLAYA', ramp:'RAMPA', repair:'TALLER', hospital:'EMERG', customs:'ADUANA', danger:'PELIGRO', hotel:'HOTEL', poi:'POI' }; var prefix = typeLabels[_pendingMarcaType] || 'MARCA'; var data = { name: prefix + String(n).padStart(2,'0'), lat, lon, mark_type: _pendingMarcaType, notes: '' }; await _py('save_waypoint', JSON.stringify(data)); await _loadWaypoints(); // Salir del modo marca GPSMap.setDrawMode('none'); var btn = document.getElementById('btn-draw-mark'); if (btn) { btn.classList.remove('active'); btn.textContent = '📍MARCA'; } _pendingMarcaType = null; }; // Drag de WPT — guarda nueva posición en backend window.onWptDrag = async function(wpt) { if (!window.py) return; await _py('save_waypoint', JSON.stringify(wpt)); // Re-renderizar rutas con las nuevas coordenadas var wptMap = Object.fromEntries(_waypoints.map(w => [w.id, w])); GPSMap.renderRoutes(_routes, wptMap); }; // Drag de MARCA — guarda nueva posición en backend window.onMarkDrag = async function(mark) { if (!window.py) return; await _py('save_waypoint', JSON.stringify(mark)); }; window.openMarkModal = function(m) { document.getElementById('mark-id').value = m.id || ''; document.getElementById('mark-name').value = m.name || ''; document.getElementById('mark-type-val').value = m.mark_type || 'poi'; document.getElementById('mark-lat').value = m.lat != null ? m.lat.toFixed(6) : ''; document.getElementById('mark-lon').value = m.lon != null ? m.lon.toFixed(6) : ''; document.getElementById('mark-notes').value = m.notes || ''; var MARK_DEFS = { fishing:'🎣 Pesca', marina:'⚓ Marina', fuel:'⛽ Combustible', restaurant:'🍴 Restaurante', dive:'🤿 Buceo', anchorage:'🚢 Fondeo', beach:'🏖️ Playa', ramp:'🚤 Rampa', repair:'🔧 Taller', hospital:'🏥 Emergencia', customs:'🛂 Aduana', danger:'⚠️ Peligro', hotel:'🏨 Hotel', poi:'📍 POI' }; document.getElementById('mark-type-display').textContent = MARK_DEFS[m.mark_type] || '📍 POI'; showModal('modal-mark'); }; window.saveMark = async function() { if (!window.py) return; var data = { id: document.getElementById('mark-id').value || undefined, name: document.getElementById('mark-name').value.trim(), lat: parseFloat(document.getElementById('mark-lat').value), lon: parseFloat(document.getElementById('mark-lon').value), notes: document.getElementById('mark-notes').value.trim(), mark_type: document.getElementById('mark-type-val').value || 'poi', }; if (!data.name || isNaN(data.lat) || isNaN(data.lon)) { alert('Nombre, lat y lon son requeridos'); return; } await _py('save_waypoint', JSON.stringify(data)); closeModal(); await _loadWaypoints(); }; window.deleteMark = async function(id) { if (!confirm('¿Eliminar esta marca?')) return; window.py.delete_waypoint(id); await _loadWaypoints(); }; window.toggleWptLock = async function(id) { if (!window.py) return; var wpt = _waypoints.find(function(w) { return w.id === id; }); if (!wpt) return; wpt.locked = wpt.locked ? 0 : 1; await _py('save_waypoint', JSON.stringify(wpt)); await _loadWaypoints(); }; window.toggleMarkLock = async function(id) { if (!window.py) return; var mark = _marks.find(function(m) { return m.id === id; }); if (!mark) return; mark.locked = mark.locked ? 0 : 1; await _py('save_waypoint', JSON.stringify(mark)); await _loadWaypoints(); }; window.onMarkMapClick = function(mark) { openMarkModal(mark); }; // ── Boot (called by bridge.js once QWebChannel is ready) ────────────────── window.bootApp = async function () { await _loadWaypoints(); await _loadRoutes(); await _loadTrack(); // Carga inicial de cartas (mapa arranca en Miami por defecto). // moveend handler se activa dentro de loadAll() → carga automática al navegar. _chartLoadPending = true; // backup: re-disparar al primer fix GPS si no hay celdas await ChartLayer.loadAll(); };