Files
AR-GPS/frontend/js/map.js
T

612 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* Chart plotter — OpenLayers based.
Exposes window.GPSMap with methods used by app.js */
const GPSMap = (function () {
// ── Map setup ──────────────────────────────────────────────────────────────
// Fondo oceánico sólido — visible siempre aunque no haya internet.
// Las cartas ENC (land/depth) renderizan encima con zIndex 2+.
const bgLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
// Tile vacío 1×1px azul — no hace peticiones de red
tileUrlFunction: function() { return null; },
}),
zIndex: 0,
});
// OSM — se carga cuando hay internet, falla silenciosamente si no hay.
const osmLayer = new ol.layer.Tile({
source: new ol.source.OSM({ crossOrigin: 'anonymous' }),
zIndex: 1,
opacity: 0.82, // leve transparencia: las cartas ENC resaltan encima
});
const trackSource = new ol.source.Vector();
const trackLayer = new ol.layer.Vector({
source: trackSource,
style: new ol.style.Style({
stroke: new ol.style.Stroke({ color: '#00c8e8cc', width: 2.5 }),
}),
zIndex: 20,
});
const wptSource = new ol.source.Vector();
const wptLayer = new ol.layer.Vector({ source: wptSource, zIndex: 22 });
const routeSource = new ol.source.Vector();
const routeLayer = new ol.layer.Vector({ source: routeSource, zIndex: 21 });
// Capa de marcas (POI: pesca, marina, buceo, etc.) — zIndex 23, encima de WPTs
const marksSource = new ol.source.Vector();
const marksLayer = new ol.layer.Vector({ source: marksSource, zIndex: 23 });
const ownSource = new ol.source.Vector();
const ownLayer = new ol.layer.Vector({ source: ownSource, zIndex: 25 });
// ── Draw mode — must be declared BEFORE ol.Map (used in layers array) ──────
// 'none' | 'wpt' | 'route'
let _drawMode = 'none';
let _routeDraftCoords = []; // [[lon,lat], ...] accumulating route
let _routeDraftFeature = null;
const routeDraftSource = new ol.source.Vector();
const routeDraftLayer = new ol.layer.Vector({
source: routeDraftSource, zIndex: 30,
style: new ol.style.Style({
stroke: new ol.style.Stroke({ color: '#fbbf24', width: 2, lineDash: [8,5] }),
image: new ol.style.Circle({
radius: 5,
fill: new ol.style.Fill({ color: '#fbbf24' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 1.5 }),
}),
}),
});
const map = new ol.Map({
target: 'map',
layers: [bgLayer, osmLayer, trackLayer, routeLayer, ownLayer, wptLayer, marksLayer, routeDraftLayer],
// Default: Miami (IALA-B, área de prueba GPS). GPS auto-centra en cuanto llega fix.
view: new ol.View({ center: ol.proj.fromLonLat([-80.19, 25.77]), zoom: 12 }),
controls: [], // OL 9.x: ol.control.defaults eliminado; controles custom en toolbar
// NOTA: NO añadir pixelRatio:1 — en Qt5 WebEngine la causa del desfase de coordenadas
// era document.documentElement.style.zoom (eliminado de index.html), NO el devicePixelRatio.
});
// ── State ──────────────────────────────────────────────────────────────────
let _lat = null, _lon = null, _cog = 0, _sog = 0;
let _trackVis = true;
let _orientation = 'N'; // 'N' or 'C'
let _trackCoords = []; // [lon,lat] ordered
let _ownFeature = null;
// ── Own ship arrow ─────────────────────────────────────────────────────────
function _arrowCanvas(col = '#00d8f0', sz = 28) {
const c = document.createElement('canvas');
c.width = c.height = sz;
const ctx = c.getContext('2d');
const cx = sz / 2, cy = sz / 2, r = sz * 0.45;
ctx.save();
ctx.translate(cx, cy);
// Sombra exterior para visibilidad sobre cualquier tile OSM
ctx.shadowBlur = 5; ctx.shadowColor = 'rgba(0,0,0,0.60)';
// Arrow pointing up (north = 0°)
ctx.beginPath();
ctx.moveTo(0, -r);
ctx.lineTo(r * 0.52, r * 0.70);
ctx.lineTo(0, r * 0.28);
ctx.lineTo(-r * 0.52, r * 0.70);
ctx.closePath();
ctx.fillStyle = col;
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.restore();
return c;
}
// Cache del dataURL de la flecha — se regenera solo si cambia el COG (rotación vía style)
// Color: magenta S-52 OWNSHIP — inconfundible sobre el agua, estándar ECDIS
var _arrowDataUrl = null;
function _updateOwnShip() {
if (_lat == null || _lon == null) return;
const coord = ol.proj.fromLonLat([_lon, _lat]);
if (!_ownFeature) {
_ownFeature = new ol.Feature({ geometry: new ol.geom.Point(coord) });
ownSource.addFeature(_ownFeature);
} else {
_ownFeature.getGeometry().setCoordinates(coord);
}
// Qt5 WebEngine: img:canvas NO funciona en ol.style.Icon — usar src:dataURL.
if (!_arrowDataUrl) {
var cv = _arrowCanvas('#cc00ff', 32); // magenta S-52 OWNSHIP
try { _arrowDataUrl = cv.toDataURL('image/png'); } catch(e) { _arrowDataUrl = null; }
}
var ownStyle;
if (_arrowDataUrl) {
ownStyle = new ol.style.Style({
image: new ol.style.Icon({
src: _arrowDataUrl,
anchor: [0.5, 0.5],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
scale: 1,
rotation: ol.math.toRadians(_cog),
rotateWithView: _orientation === 'C',
}),
});
} else {
// Fallback si toDataURL falla: triángulo con RegularShape
ownStyle = new ol.style.Style({
image: new ol.style.RegularShape({
points: 3,
radius: 10,
fill: new ol.style.Fill({ color: '#00d8f0' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 }),
rotation: ol.math.toRadians(_cog),
}),
});
}
_ownFeature.setStyle(ownStyle);
if (_orientation === 'C') {
map.getView().setRotation(-ol.math.toRadians(_cog));
}
}
// ── Track ──────────────────────────────────────────────────────────────────
function _appendTrack(lon, lat) {
_trackCoords.push(ol.proj.fromLonLat([lon, lat]));
if (_trackCoords.length < 2) return;
trackSource.clear();
trackSource.addFeature(new ol.Feature({
geometry: new ol.geom.LineString(_trackCoords),
}));
}
function loadTrack(points) {
_trackCoords = points.map(p => ol.proj.fromLonLat([p.lon, p.lat]));
trackSource.clear();
if (_trackCoords.length >= 2) {
trackSource.addFeature(new ol.Feature({
geometry: new ol.geom.LineString(_trackCoords),
}));
}
}
// ── ECDIS helpers ─────────────────────────────────────────────────────────
function _brg(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 _nm(lat1, lon1, lat2, lon2) {
const R = 3440.065;
const φ1=lat1*Math.PI/180, φ2=lat2*Math.PI/180;
const Δφ=(lat2-lat1)*Math.PI/180, Δλ=(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));
}
// ── Iconos de MARCAS POI ──────────────────────────────────────────────────
// Pin shape: círculo con punta abajo. Anchor = [0.5, 1.0] → la punta toca la posición.
// size=34x40: viewBox "0 0 34 40", círculo cx=17 cy=17 r=15, punta en (17,40).
var _MARK_DEFS = {
fishing: { emoji: '🎣', label: 'Pesca', col: '#2eaaff' },
marina: { emoji: '⚓', label: 'Marina', col: '#00d8f0' },
fuel: { emoji: '⛽', label: 'Combustible', col: '#f8cc38' },
restaurant: { emoji: '🍴', label: 'Restaurante', col: '#ff8844' },
dive: { emoji: '🤿', label: 'Buceo', col: '#00ccaa' },
anchorage: { emoji: '⚓', label: 'Fondeo', col: '#a8d8ea' },
beach: { emoji: '🏖️', label: 'Playa', col: '#f8e87a' },
ramp: { emoji: '🚤', label: 'Rampa', col: '#88ccff' },
repair: { emoji: '🔧', label: 'Taller', col: '#b0b0b0' },
hospital: { emoji: '🏥', label: 'Emergencia', col: '#ff4444' },
hotel: { emoji: '🏨', label: 'Hotel', col: '#cc88ff' },
customs: { emoji: '🛂', label: 'Aduana', col: '#ffaa44' },
danger: { emoji: '⚠️', label: 'Peligro', col: '#ff4444' },
poi: { emoji: '📍', label: 'POI', col: '#ff6688' },
waypoint: { emoji: null, label: 'WPT Nav', col: '#00d8f0' }, // símbolo ECDIS
};
function _markSvg(markType, active) {
var d = _MARK_DEFS[markType] || _MARK_DEFS['poi'];
var col = active ? '#ffcc00' : d.col;
var pinPath = 'M17,0 C9.3,0 3,6.3 3,14 C3,22 17,40 17,40 C17,40 31,22 31,14 C31,6.3 24.7,0 17,0 Z';
var emojiSize = 15;
var svgInner = d.emoji
? ('<text x="17" y="20" text-anchor="middle" dominant-baseline="central" font-size="' + emojiSize + '" font-family="Segoe UI Emoji,Apple Color Emoji,Noto Color Emoji,sans-serif">' + d.emoji + '</text>')
: ('<line x1="17" y1="7" x2="17" y2="21" stroke="white" stroke-width="1.4"/>' +
'<line x1="10" y1="14" x2="24" y2="14" stroke="white" stroke-width="1.4"/>' +
'<circle cx="17" cy="14" r="2.5" fill="white"/>');
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="34" height="40" viewBox="0 0 34 40">' +
'<filter id="shd" x="-30%" y="-10%" width="160%" height="160%">' +
'<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" flood-color="rgba(0,0,0,0.55)"/>' +
'</filter>' +
'<path d="' + pinPath + '" fill="' + col + '" stroke="white" stroke-width="1.8" filter="url(#shd)"/>' +
'<circle cx="17" cy="14" r="11" fill="rgba(0,0,0,0.18)"/>' +
svgInner +
'</svg>'
);
}
// Icono SVG ECDIS: círculo con cruz y punto central (IEC 61174 — planned position)
function _wptSvg(active) {
const col = active ? '#ffcc00' : '#00d8f0';
const fill = active ? '0.22' : '0.10';
const sw = active ? '2.2' : '1.8';
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">' +
'<circle cx="11" cy="11" r="8" fill="' + col + '" fill-opacity="' + fill + '" stroke="' + col + '" stroke-width="' + sw + '"/>' +
'<line x1="11" y1="3.5" x2="11" y2="18.5" stroke="' + col + '" stroke-width="1.3"/>' +
'<line x1="3.5" y1="11" x2="18.5" y2="11" stroke="' + col + '" stroke-width="1.3"/>' +
'<circle cx="11" cy="11" r="2" fill="' + col + '"/>' +
'</svg>'
);
}
// ── Waypoints ──────────────────────────────────────────────────────────────
let _activeNavWpt = null;
function renderWaypoints(wpts) {
wptSource.clear();
(wpts || []).forEach(w => {
const f = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([w.lon, w.lat])),
wpt: w,
});
f.setId('wpt_' + w.id);
const isActive = _activeNavWpt && _activeNavWpt.id === w.id;
var isLocked = !!w.locked;
var col = isActive ? '#ffcc00' : '#00d8f0';
var styles = [new ol.style.Style({
image: new ol.style.Icon({
src: _wptSvg(isActive),
anchor: [0.5, 0.5], anchorXUnits: 'fraction', anchorYUnits: 'fraction',
scale: 1,
}),
text: new ol.style.Text({
text: w.name,
offsetX: 14, offsetY: -8,
font: 'bold 10px "JetBrains Mono", monospace',
textAlign: 'left',
fill: new ol.style.Fill({ color: col }),
stroke: new ol.style.Stroke({ color: 'rgba(3,8,16,0.92)', width: 3 }),
}),
})];
if (isLocked) {
styles.push(new ol.style.Style({
text: new ol.style.Text({
text: '🔒', offsetX: 10, offsetY: 10,
font: '10px sans-serif', textAlign: 'left',
}),
}));
}
f.setStyle(styles);
wptSource.addFeature(f);
});
}
function renderRoutes(routes, wptMap) {
routeSource.clear();
(routes || []).forEach(r => {
const wpts = (r.wpt_ids || []).map(id => wptMap[id]).filter(Boolean);
if (wpts.length < 2) return;
const coords = wpts.map(w => ol.proj.fromLonLat([w.lon, w.lat]));
// Línea de ruta — cyan sólida ECDIS (IEC 61174 planned route)
const lineFeat = new ol.Feature({ geometry: new ol.geom.LineString(coords), route: r });
lineFeat.setStyle(new ol.style.Style({
stroke: new ol.style.Stroke({ color: 'rgba(0,210,240,0.88)', width: 1.8 }),
}));
routeSource.addFeature(lineFeat);
// Etiqueta de cada tramo: rumbo verdadero y distancia en NM
for (let i = 0; i < wpts.length - 1; i++) {
const w1 = wpts[i], w2 = wpts[i+1];
const c1 = ol.proj.fromLonLat([w1.lon, w1.lat]);
const c2 = ol.proj.fromLonLat([w2.lon, w2.lat]);
const mid = [(c1[0]+c2[0])/2, (c1[1]+c2[1])/2];
const brg = _brg(w1.lat, w1.lon, w2.lat, w2.lon);
const dst = _nm(w1.lat, w1.lon, w2.lat, w2.lon);
const lbl = String(Math.round(brg)).padStart(3,'0') + '°T ' + dst.toFixed(2) + ' NM';
const midFeat = new ol.Feature({ geometry: new ol.geom.Point(mid) });
midFeat.setStyle(new ol.style.Style({
text: new ol.style.Text({
text: lbl,
font: '9px "JetBrains Mono", monospace',
offsetY: -10,
fill: new ol.style.Fill({ color: '#00e8ff' }),
stroke: new ol.style.Stroke({ color: 'rgba(3,8,16,0.90)', width: 3 }),
backgroundFill: new ol.style.Fill({ color: 'rgba(6,14,28,0.72)' }),
padding: [2, 5, 2, 5],
}),
image: new ol.style.RegularShape({ // pequeño rombo marcador de tramo
points: 4, radius: 3, angle: Math.PI/4,
fill: new ol.style.Fill({ color: 'rgba(0,210,240,0.80)' }),
}),
}));
routeSource.addFeature(midFeat);
}
});
}
// ── Marcas POI ────────────────────────────────────────────────────────────
function renderMarks(marks) {
marksSource.clear();
(marks || []).forEach(function(m) {
var f = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([m.lon, m.lat])),
mark: m,
});
f.setId('mark_' + m.id);
var isLocked = !!m.locked;
var markStyles = [new ol.style.Style({
image: new ol.style.Icon({
src: _markSvg(m.mark_type, false),
anchor: [0.5, 1.0],
anchorXUnits: 'fraction', anchorYUnits: 'fraction',
scale: 1,
}),
text: new ol.style.Text({
text: m.name,
offsetY: -44, offsetX: 0,
font: 'bold 10px "Inter", sans-serif',
textAlign: 'center',
fill: new ol.style.Fill({ color: (_MARK_DEFS[m.mark_type] || _MARK_DEFS.poi).col }),
stroke: new ol.style.Stroke({ color: 'rgba(3,8,16,0.92)', width: 3 }),
}),
})];
if (isLocked) {
markStyles.push(new ol.style.Style({
text: new ol.style.Text({
text: '🔒', offsetX: 12, offsetY: -26,
font: '11px sans-serif', textAlign: 'left',
}),
}));
}
f.setStyle(markStyles);
marksSource.addFeature(f);
});
}
// ── Translate interaction — WPTs y marcas arrastrables ───────────────────
// Se inicializa tarde (en _initTranslate) porque necesita el mapa ya creado.
function _initTranslate() {
var tr = new ol.interaction.Translate({
layers: [wptLayer, marksLayer],
hitTolerance: 8,
filter: function(feature) {
var w = feature.get('wpt');
var m = feature.get('mark');
if (w && w.locked) return false;
if (m && m.locked) return false;
return true;
},
});
tr.on('translateend', function(evt) {
evt.features.forEach(function(f) {
var newLonLat = ol.proj.toLonLat(f.getGeometry().getCoordinates());
var wpt = f.get('wpt');
var mark = f.get('mark');
if (wpt) {
wpt.lon = newLonLat[0]; wpt.lat = newLonLat[1];
window.onWptDrag && window.onWptDrag(wpt);
}
if (mark) {
mark.lon = newLonLat[0]; mark.lat = newLonLat[1];
window.onMarkDrag && window.onMarkDrag(mark);
}
});
});
map.addInteraction(tr);
}
_initTranslate();
// ── COG vector ────────────────────────────────────────────────────────────
let _cogFeature = null;
function _updateCogVector() {
if (_lat == null || _sog < 0.3) {
if (_cogFeature) { ownSource.removeFeature(_cogFeature); _cogFeature = null; }
return;
}
const distM = _sog * 0.514444 * 360; // 6 min ahead
const h = _cog * Math.PI / 180;
const mLat = 111320, mLon = 111320 * Math.cos(_lat * Math.PI / 180);
const tipLon = _lon + (distM * Math.sin(h)) / mLon;
const tipLat = _lat + (distM * Math.cos(h)) / mLat;
const from = ol.proj.fromLonLat([_lon, _lat]);
const to = ol.proj.fromLonLat([tipLon, tipLat]);
if (!_cogFeature) {
_cogFeature = new ol.Feature({ geometry: new ol.geom.LineString([from, to]) });
_cogFeature.setStyle(new ol.style.Style({
stroke: new ol.style.Stroke({ color: 'rgba(204,0,255,0.80)', width: 2.0, lineDash: [7,4] }),
}));
ownSource.addFeature(_cogFeature);
} else {
_cogFeature.getGeometry().setCoordinates([from, to]);
}
}
// ── Map click ─────────────────────────────────────────────────────────────
map.on('click', evt => {
const [lon, lat] = ol.proj.toLonLat(evt.coordinate);
if (_drawMode === 'wpt') {
window.onMapClickWpt && window.onMapClickWpt(lat, lon);
return;
}
if (_drawMode === 'mark') {
window.onMapClickMark && window.onMapClickMark(lat, lon);
return;
}
if (_drawMode === 'route') {
_routeDraftCoords.push([lon, lat]);
_updateRouteDraft();
window.onMapClickRoute && window.onMapClickRoute(lat, lon, _routeDraftCoords.length);
return;
}
// Normal mode: click en WPT o MARCA para info
const f = map.forEachFeatureAtPixel(evt.pixel, f => f, { hitTolerance: 10 });
if (f && f.get('wpt')) { window.onWptMapClick && window.onWptMapClick(f.get('wpt')); }
if (f && f.get('mark')) { window.onMarkMapClick && window.onMarkMapClick(f.get('mark')); }
});
map.on('dblclick', evt => {
if (_drawMode === 'route' && _routeDraftCoords.length >= 2) {
window.onMapDblClickRoute && window.onMapDblClickRoute(_routeDraftCoords.slice());
_clearRouteDraft();
evt.preventDefault();
}
});
// ── Draft route rendering ─────────────────────────────────────────────────
function _updateRouteDraft() {
routeDraftSource.clear();
if (_routeDraftCoords.length < 1) return;
// Line
if (_routeDraftCoords.length >= 2) {
routeDraftSource.addFeature(new ol.Feature({
geometry: new ol.geom.LineString(_routeDraftCoords.map(c => ol.proj.fromLonLat(c))),
}));
}
// Dots at each waypoint
_routeDraftCoords.forEach(c => {
routeDraftSource.addFeature(new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat(c)),
}));
});
}
function _clearRouteDraft() {
_routeDraftCoords = [];
routeDraftSource.clear();
}
// ── Pointer coordinates + cursor feedback ────────────────────────────────
map.on('pointermove', evt => {
const [lon, lat] = ol.proj.toLonLat(evt.coordinate);
const el = document.getElementById('map-coords');
if (el) el.textContent = `LAT ${lat.toFixed(5)}° LON ${lon.toFixed(5)}°`;
// Crosshair cursor in draw modes
map.getTargetElement().style.cursor = _drawMode !== 'none' ? 'crosshair' : '';
});
// ── Public API ────────────────────────────────────────────────────────────
function update(lat, lon, cog, sog) {
const moved = (_lat !== lat || _lon !== lon);
_lat = lat; _lon = lon; _cog = cog || 0; _sog = sog || 0;
_updateOwnShip();
_updateCogVector();
if (moved) _appendTrack(lon, lat);
}
function centerOnGPS() {
// Push-button: centra una sola vez, sin auto-follow
if (_lat == null) return;
map.getView().animate({ center: ol.proj.fromLonLat([_lon, _lat]), duration: 300 });
}
function setOrientation(mode) {
_orientation = mode;
document.getElementById('btn-north').classList.toggle('active', mode === 'N');
document.getElementById('btn-course').classList.toggle('active', mode === 'C');
if (mode === 'N') map.getView().setRotation(0);
_updateOwnShip();
}
function toggleTrack(vis) {
_trackVis = vis !== undefined ? vis : !_trackVis;
trackLayer.setVisible(_trackVis);
const btn = document.getElementById('btn-track');
if (btn) btn.classList.toggle('active', _trackVis);
}
function clearTrackMap() {
_trackCoords = [];
trackSource.clear();
}
function setActiveNav(wpt) {
_activeNavWpt = wpt;
}
function getCenter() {
return ol.proj.toLonLat(map.getView().getCenter());
}
function getOLMap() { return map; }
function zoomIn() {
var v = map.getView();
v.animate({ zoom: Math.min((v.getZoom() || 12) + 1, 20), duration: 220 });
}
function zoomOut() {
var v = map.getView();
v.animate({ zoom: Math.max((v.getZoom() || 12) - 1, 2), duration: 220 });
}
function setDrawMode(mode) {
_drawMode = mode; // 'none' | 'wpt' | 'route'
if (mode === 'none') _clearRouteDraft();
map.getTargetElement().style.cursor = mode !== 'none' ? 'crosshair' : '';
}
function cancelDraw() {
_drawMode = 'none';
_clearRouteDraft();
map.getTargetElement().style.cursor = '';
}
function getDrawMode() { return _drawMode; }
// Control de opacidad del layer OSM — permite modo night/dusk sin filtrar las ayudas IALA.
// Las ayudas (encLayer) están en otra capa OL y no se ven afectadas por este ajuste.
function setOsmOpacity(v) {
osmLayer.setOpacity(v == null ? 0.82 : Math.max(0, Math.min(1, v)));
}
// Fondo del canvas #map — cambia el color de océano base según modo
function setMapBackground(color) {
var el = document.getElementById('map');
if (el) el.style.background = color || '#a8c8e8';
}
return {
update, centerOnGPS, setOrientation, toggleTrack,
clearTrackMap, renderWaypoints, renderRoutes, renderMarks,
setActiveNav, getCenter, loadTrack, getOLMap,
setDrawMode, cancelDraw, getDrawMode, zoomIn, zoomOut,
setOsmOpacity, setMapBackground,
marksSource,
};
})();
// Bind toolbar buttons
function centerOnGPS() { GPSMap.centerOnGPS(); }
function setOrientation(m){ GPSMap.setOrientation(m); }
function toggleTrack() { GPSMap.toggleTrack(); }
function mapZoomIn() { GPSMap.zoomIn(); }
function mapZoomOut() { GPSMap.zoomOut(); }
function clearTrack() {
if (!window.py) return;
if (!confirm('Clear the GPS track log?')) return;
window.py.clear_track();
GPSMap.clearTrackMap();
}
function addWptAtCenter() {
const [lon, lat] = GPSMap.getCenter();
window.openWptModal && window.openWptModal({ lat, lon });
}