diff --git a/app/diario-bordo.html b/app/diario-bordo.html
index 24da53d..5f2a762 100644
--- a/app/diario-bordo.html
+++ b/app/diario-bordo.html
@@ -1951,6 +1951,33 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
+
+
+
⚓ Cartas Náuticas
+
Provedor de cartas usado nos mapas (rastreio, fundeio, zonas). OpenSeaMap é grátis e cobre o essencial (sondas, faróis, bóias). Navionics requer chave aprovada pela Garmin.
+
Provedor
+
+OpenSeaMap (grátis · padrão)
+Navionics (requer chave)
+Apenas OSM (sem cartas náuticas)
+
+
+
Chave Navionics (navKey)
+
+
+
+
Testar e salvar
+
+
+
+
+
+
🗺️ Exportar para OpenCPN
+
OpenCPN é o app de navegação náutica open-source mais usado em PCs/notebooks. Exporte um GPX consolidado com todas suas viagens, fundeios e zonas pra abrir no OpenCPN ou em qualquer plotter compatível.
+
↓ Baixar GPX completo
+
Inclui: tracks de cada travessia · waypoints de fundeios históricos · routes das zonas demarcadas. Compatível também com Garmin, Raymarine, B&G via importação GPX.
+
+
🌬 Meteorologia · Windy Point Forecast
Cole sua chave da
Windy API para usar dados premium (vento u/v, ondas, modelos GFS/ECMWF). Sem chave, usa Open-Meteo grátis.
@@ -2732,6 +2759,87 @@ function anchorAdvice(windKn,boatType){
return `⚓ Calmo · scope mínimo 5:1`;
}
const STORAGE_KEY='diario_bordo_v3';
+
+// ============ NAUTICAL CHART LAYERS (OpenSeaMap + Navionics) ============
+// addMapLayers(map): adiciona base OSM + overlay de cartas náuticas configurável
+// Chart provider: salvo em state.chartCfg.provider ('osm'|'opensea'|'navionics')
+// Navionics requer state.chartCfg.navKey (obtido via formulário Garmin)
+
+function getChartProvider(){
+ const cfg=state.chartCfg||{};
+ return cfg.provider||(cfg.navKey?'navionics':'opensea');
+}
+
+function addMapLayers(map){
+ // Base layer: OSM padrão (sempre)
+ const osm=L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{
+ maxZoom:19,attribution:'© OpenStreetMap',className:'map-tiles-osm'
+ });
+ // Base layer: Esri Satélite (alternativo)
+ const sat=L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{
+ maxZoom:19,attribution:'Tiles © Esri'
+ });
+ // Overlay: OpenSeaMap (cartas náuticas grátis — sondas, faróis, bóias)
+ const seamap=L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',{
+ maxZoom:18,attribution:'© OpenSeaMap',opacity:.95,className:'map-tiles-seamark'
+ });
+
+ // Adiciona base padrão
+ osm.addTo(map);
+
+ // Aplica camada náutica conforme config
+ const provider=getChartProvider();
+ let nauticalLayer=null;
+ if(provider==='opensea'){
+ seamap.addTo(map);
+ nauticalLayer=seamap;
+ }else if(provider==='navionics'&&state.chartCfg?.navKey){
+ nauticalLayer=tryAddNavionicsLayer(map);
+ if(!nauticalLayer){seamap.addTo(map);nauticalLayer=seamap}
+ }
+
+ // Layer switcher (canto superior direito)
+ const baseLayers={'OSM Padrão':osm,'Satélite':sat};
+ const overlayLayers={'OpenSeaMap (cartas grátis)':seamap};
+ if(state.chartCfg?.navKey){
+ overlayLayers['Navionics ⚓']={addTo:m=>tryAddNavionicsLayer(m),removeFrom:m=>{}};
+ }
+ L.control.layers(baseLayers,overlayLayers,{position:'topright',collapsed:true}).addTo(map);
+
+ return map;
+}
+
+let _navionicsScriptLoaded=null;
+function loadNavionicsScript(){
+ if(_navionicsScriptLoaded)return _navionicsScriptLoaded;
+ _navionicsScriptLoaded=new Promise((resolve,reject)=>{
+ const s=document.createElement('script');
+ s.src='https://webapiv2.navionics.com/dist/webapi/webapi.min.js';
+ s.async=true;
+ s.onload=()=>resolve(window.JNC);
+ s.onerror=()=>reject(new Error('Navionics script failed'));
+ document.head.appendChild(s);
+ });
+ return _navionicsScriptLoaded;
+}
+
+function tryAddNavionicsLayer(map){
+ const key=state.chartCfg?.navKey;
+ if(!key)return null;
+ loadNavionicsScript().then(JNC=>{
+ if(!JNC?.Leaflet?.NavionicsOverlay)return;
+ try{
+ const overlay=new JNC.Leaflet.NavionicsOverlay({
+ navKey:key,
+ chartType:JNC.NAVIONICS_CHARTS.NAUTICAL,
+ isTransparent:true,logoPayoff:true,zIndex:5,
+ });
+ overlay.addTo(map);
+ console.log('[navionics] overlay added');
+ }catch(e){console.warn('[navionics] add failed',e.message)}
+ }).catch(e=>console.warn('[navionics] load failed',e.message));
+ return {addTo:()=>{},removeFrom:()=>{}}; // placeholder pra layer control
+}
const TRACKING_KEY='diario_tracking_v3';
const ANCHOR_KEY='diario_anchor_v3';
let pendingFilter='active';
@@ -2836,7 +2944,7 @@ function reopenTracking(){if(tracking.active)openTrackingModal()}
function openTrackingModal(){
openModal('tracking-modal');
setTimeout(()=>{
- if(!trackingMap){trackingMap=L.map('tracking-map').setView([0,0],2);L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(trackingMap)}
+ if(!trackingMap){trackingMap=L.map('tracking-map').setView([0,0],2);addMapLayers(trackingMap)}
setTimeout(()=>trackingMap.invalidateSize(),100);
if(tracking.points.length){const ll=tracking.points.map(p=>[p.lat,p.lng]);if(trackingPolyline)trackingPolyline.remove();trackingPolyline=L.polyline(ll,{color:'#a07832',weight:4,opacity:.85}).addTo(trackingMap);const last=tracking.points[tracking.points.length-1];if(trackingMarker)trackingMarker.remove();trackingMarker=L.marker([last.lat,last.lng]).addTo(trackingMap);trackingMap.fitBounds(trackingPolyline.getBounds(),{padding:[20,20]})}
updateTrackingUI();
@@ -3516,7 +3624,7 @@ function openMapModal(tid){
setTimeout(()=>{
if(mapInstance){mapInstance.remove();mapInstance=null}
mapInstance=L.map('map-modal-content');
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(mapInstance);
+ addMapLayers(mapInstance);
const ll=t.track.points.map(p=>[p.lat,p.lng]);
L.polyline(ll,{color:'#a07832',weight:4,opacity:.85}).addTo(mapInstance);
if(ll.length){L.marker(ll[0]).addTo(mapInstance).bindPopup('Saída');L.marker(ll[ll.length-1]).addTo(mapInstance).bindPopup('Chegada');mapInstance.fitBounds(ll,{padding:[20,20]})}
@@ -3913,7 +4021,7 @@ function updateAnchorUI(){
function openAnchorWatchModal(){
openModal('anchor-watch-modal');
setTimeout(()=>{
- if(!anchorMap){anchorMap=L.map('anchor-map').setView([anchorWatch.anchorPos.lat,anchorWatch.anchorPos.lng],17);L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(anchorMap)}
+ if(!anchorMap){anchorMap=L.map('anchor-map').setView([anchorWatch.anchorPos.lat,anchorWatch.anchorPos.lng],17);addMapLayers(anchorMap)}
setTimeout(()=>anchorMap.invalidateSize(),100);
drawAnchorOnMap();
document.getElementById('aw-radius-slider').value=anchorWatch.radius;
@@ -5320,7 +5428,7 @@ function openAnchorHistoryMap(id){
setTimeout(()=>{
if(historyMap){historyMap.remove();historyMap=null}
historyMap=L.map('anchor-history-map').setView([h.anchorPos.lat,h.anchorPos.lng],17);
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(historyMap);
+ addMapLayers(historyMap);
const a=[h.anchorPos.lat,h.anchorPos.lng];
const sp=h.swingPos||h.anchorPos;
const s=[sp.lat,sp.lng];
@@ -5575,6 +5683,119 @@ function bindWeatherInputs(){
const st=document.getElementById('windy-status');
if(state.weatherCfg?.windyKey)st.innerHTML=`
Chave configurada · usando Windy ${state.weatherCfg.model} `;
else st.innerHTML=`
Sem chave · usando Open-Meteo (grátis) `;
+ // bind charts cfg também (mesmo fluxo abrir aba)
+ bindChartInputs();
+}
+
+function bindChartInputs(){
+ const p=document.getElementById('chart-provider');
+ const k=document.getElementById('chart-nav-key');
+ const st=document.getElementById('chart-status');
+ if(!p||!k||!st)return;
+ p.value=state.chartCfg?.provider||'opensea';
+ k.value=state.chartCfg?.navKey||'';
+ if(state.chartCfg?.provider==='navionics'&&state.chartCfg?.navKey){
+ st.innerHTML='
Navionics ativo · cartas oficiais ';
+ }else if(state.chartCfg?.provider==='opensea'||!state.chartCfg?.provider){
+ st.innerHTML='
OpenSeaMap ativo · cartas náuticas grátis ';
+ }else{
+ st.innerHTML='
Apenas OSM · sem overlay náutico ';
+ }
+}
+
+function saveChartCfg(){
+ state.chartCfg={
+ provider:document.getElementById('chart-provider').value,
+ navKey:document.getElementById('chart-nav-key').value.trim(),
+ };
+ saveState();
+ bindChartInputs();
+}
+
+function testChartCfg(){
+ saveChartCfg();
+ toast('Cartas salvas — abra um mapa pra ver');
+}
+
+// ============ EXPORT GPX para OpenCPN ============
+function gpxEscape(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
+
+function exportOpenCPN(){
+ const now=new Date().toISOString();
+ let gpx=`
+
+
+ Shivao — ${gpxEscape(activeBoat()?.name||'Diário de Bordo')}
+ Export consolidado de viagens, fundeios e zonas
+ ${now}
+
+`;
+ // Waypoints: anchorages históricos
+ const anchors=state.anchorHistory||[];
+ for(const a of anchors){
+ if(!a.anchorPos)continue;
+ gpx+=`
+ ⚓ ${gpxEscape(a.label||'Fundeio '+new Date(a.startedAt||Date.now()).toLocaleDateString('pt-BR'))}
+ Raio ${a.radius||'?'}m · ${a.duration?Math.round(a.duration/60000)+'min':'desconhecido'}
+ Anchor
+ anchorage
+
+`;
+ }
+ // Tracks: cada viagem com pontos GPS
+ const trips=(state.trips||[]).filter(t=>t.track&&t.track.length>0);
+ for(const t of trips){
+ gpx+=`
+ ${gpxEscape(t.destination||'Travessia '+new Date(t.dateStart||Date.now()).toLocaleDateString('pt-BR'))}
+ ${gpxEscape((t.notes||'').slice(0,200))}
+
+`;
+ for(const pt of t.track){
+ const lat=pt.lat||pt[0];
+ const lng=pt.lng||pt[1];
+ const ts=pt.t||pt.ts;
+ if(!lat||!lng)continue;
+ gpx+=` ${ts?`${new Date(ts).toISOString()} `:''}
+`;
+ }
+ gpx+=`
+
+`;
+ }
+ // Routes: zonas (forbidden/attention) como rotas fechadas
+ const zones=state.zones||[];
+ for(const z of zones){
+ if(!z.center||!z.radius)continue;
+ // Aproxima círculo como polígono de 16 pontos
+ const points=[];
+ for(let i=0;i<=16;i++){
+ const angle=(i/16)*2*Math.PI;
+ const dLat=(z.radius/111000)*Math.cos(angle);
+ const dLng=(z.radius/(111000*Math.cos(z.center.lat*Math.PI/180)))*Math.sin(angle);
+ points.push([z.center.lat+dLat,z.center.lng+dLng]);
+ }
+ gpx+=`
+ ${z.kind==='forbidden'?'⛔ ':'⚠ '}${gpxEscape(z.name||'Zona')}
+ ${z.kind} · raio ${Math.round(z.radius)}m
+`;
+ for(const [lat,lng] of points){
+ gpx+=`
+`;
+ }
+ gpx+=`
+`;
+ }
+ gpx+=` `;
+
+ // Download
+ const blob=new Blob([gpx],{type:'application/gpx+xml'});
+ const url=URL.createObjectURL(blob);
+ const a=document.createElement('a');
+ a.href=url;
+ a.download=`shivao-${new Date().toISOString().slice(0,10)}.gpx`;
+ document.body.appendChild(a);a.click();a.remove();
+ setTimeout(()=>URL.revokeObjectURL(url),1000);
+ toast(`GPX gerado · ${trips.length} tracks · ${anchors.length} fundeios · ${zones.length} zonas`);
}
function saveWeatherKey(){
@@ -6066,7 +6287,7 @@ function openZoneEditor(id){
else if(anchorWatch.lastPos){initLat=anchorWatch.lastPos.lat;initLng=anchorWatch.lastPos.lng;initZoom=15}
else if(tracking.points.length){const last=tracking.points[tracking.points.length-1];initLat=last.lat;initLng=last.lng;initZoom=15}
zoneMap=L.map('zone-map').setView([initLat,initLng],initZoom);
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(zoneMap);
+ addMapLayers(zoneMap);
// mostrar zonas existentes (exceto a editada)
state.zones.forEach(z=>{
if(editingZone&&z.id===editingZone.id)return;
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index 4c0fecf..ca0d956 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "br.com.pontualtech.shivao"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 11
- versionName "1.7.1"
+ versionCode 12
+ versionName "1.8.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/mobile/package.json b/mobile/package.json
index 1357e3a..1ce77e0 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "shivao-mobile",
- "version": "1.7.1",
+ "version": "1.8.0",
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
"main": "index.js",
"type": "module",
diff --git a/server/public/index.html b/server/public/index.html
index 24da53d..5f2a762 100644
--- a/server/public/index.html
+++ b/server/public/index.html
@@ -1951,6 +1951,33 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
+
+
+
⚓ Cartas Náuticas
+
Provedor de cartas usado nos mapas (rastreio, fundeio, zonas). OpenSeaMap é grátis e cobre o essencial (sondas, faróis, bóias). Navionics requer chave aprovada pela Garmin.
+
Provedor
+
+OpenSeaMap (grátis · padrão)
+Navionics (requer chave)
+Apenas OSM (sem cartas náuticas)
+
+
+
Chave Navionics (navKey)
+
+
+
+
Testar e salvar
+
+
+
+
+
+
🗺️ Exportar para OpenCPN
+
OpenCPN é o app de navegação náutica open-source mais usado em PCs/notebooks. Exporte um GPX consolidado com todas suas viagens, fundeios e zonas pra abrir no OpenCPN ou em qualquer plotter compatível.
+
↓ Baixar GPX completo
+
Inclui: tracks de cada travessia · waypoints de fundeios históricos · routes das zonas demarcadas. Compatível também com Garmin, Raymarine, B&G via importação GPX.
+
+
🌬 Meteorologia · Windy Point Forecast
Cole sua chave da
Windy API para usar dados premium (vento u/v, ondas, modelos GFS/ECMWF). Sem chave, usa Open-Meteo grátis.
@@ -2732,6 +2759,87 @@ function anchorAdvice(windKn,boatType){
return `⚓ Calmo · scope mínimo 5:1`;
}
const STORAGE_KEY='diario_bordo_v3';
+
+// ============ NAUTICAL CHART LAYERS (OpenSeaMap + Navionics) ============
+// addMapLayers(map): adiciona base OSM + overlay de cartas náuticas configurável
+// Chart provider: salvo em state.chartCfg.provider ('osm'|'opensea'|'navionics')
+// Navionics requer state.chartCfg.navKey (obtido via formulário Garmin)
+
+function getChartProvider(){
+ const cfg=state.chartCfg||{};
+ return cfg.provider||(cfg.navKey?'navionics':'opensea');
+}
+
+function addMapLayers(map){
+ // Base layer: OSM padrão (sempre)
+ const osm=L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{
+ maxZoom:19,attribution:'© OpenStreetMap',className:'map-tiles-osm'
+ });
+ // Base layer: Esri Satélite (alternativo)
+ const sat=L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{
+ maxZoom:19,attribution:'Tiles © Esri'
+ });
+ // Overlay: OpenSeaMap (cartas náuticas grátis — sondas, faróis, bóias)
+ const seamap=L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',{
+ maxZoom:18,attribution:'© OpenSeaMap',opacity:.95,className:'map-tiles-seamark'
+ });
+
+ // Adiciona base padrão
+ osm.addTo(map);
+
+ // Aplica camada náutica conforme config
+ const provider=getChartProvider();
+ let nauticalLayer=null;
+ if(provider==='opensea'){
+ seamap.addTo(map);
+ nauticalLayer=seamap;
+ }else if(provider==='navionics'&&state.chartCfg?.navKey){
+ nauticalLayer=tryAddNavionicsLayer(map);
+ if(!nauticalLayer){seamap.addTo(map);nauticalLayer=seamap}
+ }
+
+ // Layer switcher (canto superior direito)
+ const baseLayers={'OSM Padrão':osm,'Satélite':sat};
+ const overlayLayers={'OpenSeaMap (cartas grátis)':seamap};
+ if(state.chartCfg?.navKey){
+ overlayLayers['Navionics ⚓']={addTo:m=>tryAddNavionicsLayer(m),removeFrom:m=>{}};
+ }
+ L.control.layers(baseLayers,overlayLayers,{position:'topright',collapsed:true}).addTo(map);
+
+ return map;
+}
+
+let _navionicsScriptLoaded=null;
+function loadNavionicsScript(){
+ if(_navionicsScriptLoaded)return _navionicsScriptLoaded;
+ _navionicsScriptLoaded=new Promise((resolve,reject)=>{
+ const s=document.createElement('script');
+ s.src='https://webapiv2.navionics.com/dist/webapi/webapi.min.js';
+ s.async=true;
+ s.onload=()=>resolve(window.JNC);
+ s.onerror=()=>reject(new Error('Navionics script failed'));
+ document.head.appendChild(s);
+ });
+ return _navionicsScriptLoaded;
+}
+
+function tryAddNavionicsLayer(map){
+ const key=state.chartCfg?.navKey;
+ if(!key)return null;
+ loadNavionicsScript().then(JNC=>{
+ if(!JNC?.Leaflet?.NavionicsOverlay)return;
+ try{
+ const overlay=new JNC.Leaflet.NavionicsOverlay({
+ navKey:key,
+ chartType:JNC.NAVIONICS_CHARTS.NAUTICAL,
+ isTransparent:true,logoPayoff:true,zIndex:5,
+ });
+ overlay.addTo(map);
+ console.log('[navionics] overlay added');
+ }catch(e){console.warn('[navionics] add failed',e.message)}
+ }).catch(e=>console.warn('[navionics] load failed',e.message));
+ return {addTo:()=>{},removeFrom:()=>{}}; // placeholder pra layer control
+}
const TRACKING_KEY='diario_tracking_v3';
const ANCHOR_KEY='diario_anchor_v3';
let pendingFilter='active';
@@ -2836,7 +2944,7 @@ function reopenTracking(){if(tracking.active)openTrackingModal()}
function openTrackingModal(){
openModal('tracking-modal');
setTimeout(()=>{
- if(!trackingMap){trackingMap=L.map('tracking-map').setView([0,0],2);L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(trackingMap)}
+ if(!trackingMap){trackingMap=L.map('tracking-map').setView([0,0],2);addMapLayers(trackingMap)}
setTimeout(()=>trackingMap.invalidateSize(),100);
if(tracking.points.length){const ll=tracking.points.map(p=>[p.lat,p.lng]);if(trackingPolyline)trackingPolyline.remove();trackingPolyline=L.polyline(ll,{color:'#a07832',weight:4,opacity:.85}).addTo(trackingMap);const last=tracking.points[tracking.points.length-1];if(trackingMarker)trackingMarker.remove();trackingMarker=L.marker([last.lat,last.lng]).addTo(trackingMap);trackingMap.fitBounds(trackingPolyline.getBounds(),{padding:[20,20]})}
updateTrackingUI();
@@ -3516,7 +3624,7 @@ function openMapModal(tid){
setTimeout(()=>{
if(mapInstance){mapInstance.remove();mapInstance=null}
mapInstance=L.map('map-modal-content');
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(mapInstance);
+ addMapLayers(mapInstance);
const ll=t.track.points.map(p=>[p.lat,p.lng]);
L.polyline(ll,{color:'#a07832',weight:4,opacity:.85}).addTo(mapInstance);
if(ll.length){L.marker(ll[0]).addTo(mapInstance).bindPopup('Saída');L.marker(ll[ll.length-1]).addTo(mapInstance).bindPopup('Chegada');mapInstance.fitBounds(ll,{padding:[20,20]})}
@@ -3913,7 +4021,7 @@ function updateAnchorUI(){
function openAnchorWatchModal(){
openModal('anchor-watch-modal');
setTimeout(()=>{
- if(!anchorMap){anchorMap=L.map('anchor-map').setView([anchorWatch.anchorPos.lat,anchorWatch.anchorPos.lng],17);L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(anchorMap)}
+ if(!anchorMap){anchorMap=L.map('anchor-map').setView([anchorWatch.anchorPos.lat,anchorWatch.anchorPos.lng],17);addMapLayers(anchorMap)}
setTimeout(()=>anchorMap.invalidateSize(),100);
drawAnchorOnMap();
document.getElementById('aw-radius-slider').value=anchorWatch.radius;
@@ -5320,7 +5428,7 @@ function openAnchorHistoryMap(id){
setTimeout(()=>{
if(historyMap){historyMap.remove();historyMap=null}
historyMap=L.map('anchor-history-map').setView([h.anchorPos.lat,h.anchorPos.lng],17);
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(historyMap);
+ addMapLayers(historyMap);
const a=[h.anchorPos.lat,h.anchorPos.lng];
const sp=h.swingPos||h.anchorPos;
const s=[sp.lat,sp.lng];
@@ -5575,6 +5683,119 @@ function bindWeatherInputs(){
const st=document.getElementById('windy-status');
if(state.weatherCfg?.windyKey)st.innerHTML=`
Chave configurada · usando Windy ${state.weatherCfg.model} `;
else st.innerHTML=`
Sem chave · usando Open-Meteo (grátis) `;
+ // bind charts cfg também (mesmo fluxo abrir aba)
+ bindChartInputs();
+}
+
+function bindChartInputs(){
+ const p=document.getElementById('chart-provider');
+ const k=document.getElementById('chart-nav-key');
+ const st=document.getElementById('chart-status');
+ if(!p||!k||!st)return;
+ p.value=state.chartCfg?.provider||'opensea';
+ k.value=state.chartCfg?.navKey||'';
+ if(state.chartCfg?.provider==='navionics'&&state.chartCfg?.navKey){
+ st.innerHTML='
Navionics ativo · cartas oficiais ';
+ }else if(state.chartCfg?.provider==='opensea'||!state.chartCfg?.provider){
+ st.innerHTML='
OpenSeaMap ativo · cartas náuticas grátis ';
+ }else{
+ st.innerHTML='
Apenas OSM · sem overlay náutico ';
+ }
+}
+
+function saveChartCfg(){
+ state.chartCfg={
+ provider:document.getElementById('chart-provider').value,
+ navKey:document.getElementById('chart-nav-key').value.trim(),
+ };
+ saveState();
+ bindChartInputs();
+}
+
+function testChartCfg(){
+ saveChartCfg();
+ toast('Cartas salvas — abra um mapa pra ver');
+}
+
+// ============ EXPORT GPX para OpenCPN ============
+function gpxEscape(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
+
+function exportOpenCPN(){
+ const now=new Date().toISOString();
+ let gpx=`
+
+
+ Shivao — ${gpxEscape(activeBoat()?.name||'Diário de Bordo')}
+ Export consolidado de viagens, fundeios e zonas
+ ${now}
+
+`;
+ // Waypoints: anchorages históricos
+ const anchors=state.anchorHistory||[];
+ for(const a of anchors){
+ if(!a.anchorPos)continue;
+ gpx+=`
+ ⚓ ${gpxEscape(a.label||'Fundeio '+new Date(a.startedAt||Date.now()).toLocaleDateString('pt-BR'))}
+ Raio ${a.radius||'?'}m · ${a.duration?Math.round(a.duration/60000)+'min':'desconhecido'}
+ Anchor
+ anchorage
+
+`;
+ }
+ // Tracks: cada viagem com pontos GPS
+ const trips=(state.trips||[]).filter(t=>t.track&&t.track.length>0);
+ for(const t of trips){
+ gpx+=`
+ ${gpxEscape(t.destination||'Travessia '+new Date(t.dateStart||Date.now()).toLocaleDateString('pt-BR'))}
+ ${gpxEscape((t.notes||'').slice(0,200))}
+
+`;
+ for(const pt of t.track){
+ const lat=pt.lat||pt[0];
+ const lng=pt.lng||pt[1];
+ const ts=pt.t||pt.ts;
+ if(!lat||!lng)continue;
+ gpx+=` ${ts?`${new Date(ts).toISOString()} `:''}
+`;
+ }
+ gpx+=`
+
+`;
+ }
+ // Routes: zonas (forbidden/attention) como rotas fechadas
+ const zones=state.zones||[];
+ for(const z of zones){
+ if(!z.center||!z.radius)continue;
+ // Aproxima círculo como polígono de 16 pontos
+ const points=[];
+ for(let i=0;i<=16;i++){
+ const angle=(i/16)*2*Math.PI;
+ const dLat=(z.radius/111000)*Math.cos(angle);
+ const dLng=(z.radius/(111000*Math.cos(z.center.lat*Math.PI/180)))*Math.sin(angle);
+ points.push([z.center.lat+dLat,z.center.lng+dLng]);
+ }
+ gpx+=`
+ ${z.kind==='forbidden'?'⛔ ':'⚠ '}${gpxEscape(z.name||'Zona')}
+ ${z.kind} · raio ${Math.round(z.radius)}m
+`;
+ for(const [lat,lng] of points){
+ gpx+=`
+`;
+ }
+ gpx+=`
+`;
+ }
+ gpx+=` `;
+
+ // Download
+ const blob=new Blob([gpx],{type:'application/gpx+xml'});
+ const url=URL.createObjectURL(blob);
+ const a=document.createElement('a');
+ a.href=url;
+ a.download=`shivao-${new Date().toISOString().slice(0,10)}.gpx`;
+ document.body.appendChild(a);a.click();a.remove();
+ setTimeout(()=>URL.revokeObjectURL(url),1000);
+ toast(`GPX gerado · ${trips.length} tracks · ${anchors.length} fundeios · ${zones.length} zonas`);
}
function saveWeatherKey(){
@@ -6066,7 +6287,7 @@ function openZoneEditor(id){
else if(anchorWatch.lastPos){initLat=anchorWatch.lastPos.lat;initLng=anchorWatch.lastPos.lng;initZoom=15}
else if(tracking.points.length){const last=tracking.points[tracking.points.length-1];initLat=last.lat;initLng=last.lng;initZoom=15}
zoneMap=L.map('zone-map').setView([initLat,initLng],initZoom);
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(zoneMap);
+ addMapLayers(zoneMap);
// mostrar zonas existentes (exceto a editada)
state.zones.forEach(z=>{
if(editingZone&&z.id===editingZone.id)return;
diff --git a/server/src/index.js b/server/src/index.js
index e88e4fa..2c9a4d3 100644
--- a/server/src/index.js
+++ b/server/src/index.js
@@ -347,7 +347,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
});
// Atalho: /apk redireciona pra última APK release no Forgejo
-const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.7.1/Shivao-v1.7.1.apk';
+const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.8.0/Shivao-v1.8.0.apk';
app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL));
// Página A4 imprimível com QR Code + instruções (cola no barco/marina)