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.
+
+ +
+
+ +
Solicite em garmin.com/forms/navionics-web-api. Após aprovação, seu domínio (shivao.pontualtech.work) precisa ser autorizado pela Navionics.
+
+ +
+
+ + +
+
🗺️ 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.
+ +
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 + + +`; + // 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?``:''} +`; + } + 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.
+
+ +
+
+ +
Solicite em garmin.com/forms/navionics-web-api. Após aprovação, seu domínio (shivao.pontualtech.work) precisa ser autorizado pela Navionics.
+
+ +
+
+ + +
+
🗺️ 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.
+ +
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 + + +`; + // 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?``:''} +`; + } + 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)