feat(charts): cartas náuticas OpenSeaMap (grátis) + slot Navionics + export OpenCPN v1.8.0
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run
Cartas náuticas nos mapas (rastreio, fundeio, zonas, viagens): - OpenSeaMap como overlay padrão grátis (sondas, faróis, bóias, marcas) - Slot Navionics ativável via chave (após aprovação Garmin) - Dynamic load do JNC.Leaflet.NavionicsOverlay quando chave preenchida - Layer switcher no canto direito do mapa: OSM Padrão / Satélite Esri, overlay OpenSeaMap / Navionics - Helper addMapLayers() centraliza configuração — substituiu 5 usages manuais de L.tileLayer espalhados (tracking, trip view, anchor, anchor history, zone editor) Settings (Mais → Cartas Náuticas): - Dropdown provedor: OpenSeaMap/Navionics/só OSM - Campo chave Navionics (password) com link pro form Garmin - Status visual do provedor ativo Integração OpenCPN (Mais → Exportar para OpenCPN): - Botão gera GPX consolidado de todo o diário: - Tracks: cada viagem com pontos GPS sequenciais - Waypoints: cada fundeio histórico com símbolo Anchor - Routes: cada zona (forbidden/attention) como polígono fechado - Aproximação círculo→polígono 16 pontos pra zonas circulares - Compatível com OpenCPN, Garmin, Raymarine, B&G, qualquer plotter GPX-compliant - Download direto via Blob URL, sem servidor Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f8e92f3c58
commit
0921d98ef3
5 changed files with 456 additions and 14 deletions
|
|
@ -1951,6 +1951,33 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
|
|||
</div>
|
||||
<div id="share-result" style="font-family:var(--f-mono);font-size:11px;color:var(--ink-deep);margin-top:6px;letter-spacing:.04em;line-height:1.55"></div>
|
||||
</div>
|
||||
<!-- Cartas Náuticas -->
|
||||
<div class="export-card" id="charts-card">
|
||||
<div class="export-card-title">⚓ Cartas Náuticas</div>
|
||||
<div class="export-card-text" style="margin-bottom:10px">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.</div>
|
||||
<div class="field"><label class="field-label">Provedor</label>
|
||||
<select id="chart-provider" onchange="saveChartCfg()">
|
||||
<option value="opensea">OpenSeaMap (grátis · padrão)</option>
|
||||
<option value="navionics">Navionics (requer chave)</option>
|
||||
<option value="osm">Apenas OSM (sem cartas náuticas)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Chave Navionics (navKey)</label>
|
||||
<input type="password" id="chart-nav-key" placeholder="cole a chave após aprovação Garmin" style="font-family:var(--f-mono);font-size:11px" onchange="saveChartCfg()">
|
||||
<div class="field-hint" style="margin-top:6px">Solicite em <a href="https://www.garmin.com/en-US/forms/navionics-web-api/" target="_blank" rel="noopener" style="color:var(--m-accent,#06b6d4);text-decoration:underline">garmin.com/forms/navionics-web-api</a>. Após aprovação, seu domínio (shivao.pontualtech.work) precisa ser autorizado pela Navionics.</div>
|
||||
</div>
|
||||
<button class="btn btn-block" onclick="testChartCfg()">Testar e salvar</button>
|
||||
<div id="chart-status" style="font-family:var(--f-mono);font-size:10.5px;color:var(--sepia);margin-top:8px;letter-spacing:.04em;line-height:1.55"></div>
|
||||
</div>
|
||||
|
||||
<!-- Export OpenCPN -->
|
||||
<div class="export-card" id="opencpn-card">
|
||||
<div class="export-card-title">🗺️ Exportar para OpenCPN</div>
|
||||
<div class="export-card-text" style="margin-bottom:10px"><a href="https://opencpn.org/" target="_blank" rel="noopener" style="color:var(--m-accent,#06b6d4);text-decoration:underline">OpenCPN</a> é 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.</div>
|
||||
<button class="btn btn-block btn-primary" onclick="exportOpenCPN()">↓ Baixar GPX completo</button>
|
||||
<div class="field-hint" style="margin-top:8px">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.</div>
|
||||
</div>
|
||||
|
||||
<div class="export-card" style="border-color:var(--ocean);background:linear-gradient(180deg,var(--bg-paper),#dde6ec)">
|
||||
<div class="export-card-title">🌬 Meteorologia · Windy Point Forecast</div>
|
||||
<div class="export-card-text" style="margin-bottom:10px">Cole sua chave da <a href="https://api.windy.com/" target="_blank" style="color:var(--ocean);font-style:normal;text-decoration:underline">Windy API</a> para usar dados premium (vento u/v, ondas, modelos GFS/ECMWF). Sem chave, usa Open-Meteo grátis.</div>
|
||||
|
|
@ -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=`<span style="color:var(--algae)">Chave configurada · usando Windy ${state.weatherCfg.model}</span>`;
|
||||
else st.innerHTML=`<span style="color:var(--sepia)">Sem chave · usando Open-Meteo (grátis)</span>`;
|
||||
// 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='<span style="color:var(--m-ok,#10b981)">Navionics ativo · cartas oficiais</span>';
|
||||
}else if(state.chartCfg?.provider==='opensea'||!state.chartCfg?.provider){
|
||||
st.innerHTML='<span style="color:var(--m-text-mid,#b3c5d6)">OpenSeaMap ativo · cartas náuticas grátis</span>';
|
||||
}else{
|
||||
st.innerHTML='<span style="color:var(--m-text-soft,#7d97ad)">Apenas OSM · sem overlay náutico</span>';
|
||||
}
|
||||
}
|
||||
|
||||
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=`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="Shivao Diario de Bordo" xmlns="http://www.topografix.com/GPX/1/1">
|
||||
<metadata>
|
||||
<name>Shivao — ${gpxEscape(activeBoat()?.name||'Diário de Bordo')}</name>
|
||||
<desc>Export consolidado de viagens, fundeios e zonas</desc>
|
||||
<time>${now}</time>
|
||||
</metadata>
|
||||
`;
|
||||
// Waypoints: anchorages históricos
|
||||
const anchors=state.anchorHistory||[];
|
||||
for(const a of anchors){
|
||||
if(!a.anchorPos)continue;
|
||||
gpx+=` <wpt lat="${a.anchorPos.lat.toFixed(6)}" lon="${a.anchorPos.lng.toFixed(6)}">
|
||||
<name>⚓ ${gpxEscape(a.label||'Fundeio '+new Date(a.startedAt||Date.now()).toLocaleDateString('pt-BR'))}</name>
|
||||
<desc>Raio ${a.radius||'?'}m · ${a.duration?Math.round(a.duration/60000)+'min':'desconhecido'}</desc>
|
||||
<sym>Anchor</sym>
|
||||
<type>anchorage</type>
|
||||
</wpt>
|
||||
`;
|
||||
}
|
||||
// Tracks: cada viagem com pontos GPS
|
||||
const trips=(state.trips||[]).filter(t=>t.track&&t.track.length>0);
|
||||
for(const t of trips){
|
||||
gpx+=` <trk>
|
||||
<name>${gpxEscape(t.destination||'Travessia '+new Date(t.dateStart||Date.now()).toLocaleDateString('pt-BR'))}</name>
|
||||
<desc>${gpxEscape((t.notes||'').slice(0,200))}</desc>
|
||||
<trkseg>
|
||||
`;
|
||||
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+=` <trkpt lat="${lat.toFixed(6)}" lon="${lng.toFixed(6)}">${ts?`<time>${new Date(ts).toISOString()}</time>`:''}</trkpt>
|
||||
`;
|
||||
}
|
||||
gpx+=` </trkseg>
|
||||
</trk>
|
||||
`;
|
||||
}
|
||||
// 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+=` <rte>
|
||||
<name>${z.kind==='forbidden'?'⛔ ':'⚠ '}${gpxEscape(z.name||'Zona')}</name>
|
||||
<desc>${z.kind} · raio ${Math.round(z.radius)}m</desc>
|
||||
`;
|
||||
for(const [lat,lng] of points){
|
||||
gpx+=` <rtept lat="${lat.toFixed(6)}" lon="${lng.toFixed(6)}"></rtept>
|
||||
`;
|
||||
}
|
||||
gpx+=` </rte>
|
||||
`;
|
||||
}
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1951,6 +1951,33 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
|
|||
</div>
|
||||
<div id="share-result" style="font-family:var(--f-mono);font-size:11px;color:var(--ink-deep);margin-top:6px;letter-spacing:.04em;line-height:1.55"></div>
|
||||
</div>
|
||||
<!-- Cartas Náuticas -->
|
||||
<div class="export-card" id="charts-card">
|
||||
<div class="export-card-title">⚓ Cartas Náuticas</div>
|
||||
<div class="export-card-text" style="margin-bottom:10px">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.</div>
|
||||
<div class="field"><label class="field-label">Provedor</label>
|
||||
<select id="chart-provider" onchange="saveChartCfg()">
|
||||
<option value="opensea">OpenSeaMap (grátis · padrão)</option>
|
||||
<option value="navionics">Navionics (requer chave)</option>
|
||||
<option value="osm">Apenas OSM (sem cartas náuticas)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Chave Navionics (navKey)</label>
|
||||
<input type="password" id="chart-nav-key" placeholder="cole a chave após aprovação Garmin" style="font-family:var(--f-mono);font-size:11px" onchange="saveChartCfg()">
|
||||
<div class="field-hint" style="margin-top:6px">Solicite em <a href="https://www.garmin.com/en-US/forms/navionics-web-api/" target="_blank" rel="noopener" style="color:var(--m-accent,#06b6d4);text-decoration:underline">garmin.com/forms/navionics-web-api</a>. Após aprovação, seu domínio (shivao.pontualtech.work) precisa ser autorizado pela Navionics.</div>
|
||||
</div>
|
||||
<button class="btn btn-block" onclick="testChartCfg()">Testar e salvar</button>
|
||||
<div id="chart-status" style="font-family:var(--f-mono);font-size:10.5px;color:var(--sepia);margin-top:8px;letter-spacing:.04em;line-height:1.55"></div>
|
||||
</div>
|
||||
|
||||
<!-- Export OpenCPN -->
|
||||
<div class="export-card" id="opencpn-card">
|
||||
<div class="export-card-title">🗺️ Exportar para OpenCPN</div>
|
||||
<div class="export-card-text" style="margin-bottom:10px"><a href="https://opencpn.org/" target="_blank" rel="noopener" style="color:var(--m-accent,#06b6d4);text-decoration:underline">OpenCPN</a> é 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.</div>
|
||||
<button class="btn btn-block btn-primary" onclick="exportOpenCPN()">↓ Baixar GPX completo</button>
|
||||
<div class="field-hint" style="margin-top:8px">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.</div>
|
||||
</div>
|
||||
|
||||
<div class="export-card" style="border-color:var(--ocean);background:linear-gradient(180deg,var(--bg-paper),#dde6ec)">
|
||||
<div class="export-card-title">🌬 Meteorologia · Windy Point Forecast</div>
|
||||
<div class="export-card-text" style="margin-bottom:10px">Cole sua chave da <a href="https://api.windy.com/" target="_blank" style="color:var(--ocean);font-style:normal;text-decoration:underline">Windy API</a> para usar dados premium (vento u/v, ondas, modelos GFS/ECMWF). Sem chave, usa Open-Meteo grátis.</div>
|
||||
|
|
@ -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=`<span style="color:var(--algae)">Chave configurada · usando Windy ${state.weatherCfg.model}</span>`;
|
||||
else st.innerHTML=`<span style="color:var(--sepia)">Sem chave · usando Open-Meteo (grátis)</span>`;
|
||||
// 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='<span style="color:var(--m-ok,#10b981)">Navionics ativo · cartas oficiais</span>';
|
||||
}else if(state.chartCfg?.provider==='opensea'||!state.chartCfg?.provider){
|
||||
st.innerHTML='<span style="color:var(--m-text-mid,#b3c5d6)">OpenSeaMap ativo · cartas náuticas grátis</span>';
|
||||
}else{
|
||||
st.innerHTML='<span style="color:var(--m-text-soft,#7d97ad)">Apenas OSM · sem overlay náutico</span>';
|
||||
}
|
||||
}
|
||||
|
||||
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=`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="Shivao Diario de Bordo" xmlns="http://www.topografix.com/GPX/1/1">
|
||||
<metadata>
|
||||
<name>Shivao — ${gpxEscape(activeBoat()?.name||'Diário de Bordo')}</name>
|
||||
<desc>Export consolidado de viagens, fundeios e zonas</desc>
|
||||
<time>${now}</time>
|
||||
</metadata>
|
||||
`;
|
||||
// Waypoints: anchorages históricos
|
||||
const anchors=state.anchorHistory||[];
|
||||
for(const a of anchors){
|
||||
if(!a.anchorPos)continue;
|
||||
gpx+=` <wpt lat="${a.anchorPos.lat.toFixed(6)}" lon="${a.anchorPos.lng.toFixed(6)}">
|
||||
<name>⚓ ${gpxEscape(a.label||'Fundeio '+new Date(a.startedAt||Date.now()).toLocaleDateString('pt-BR'))}</name>
|
||||
<desc>Raio ${a.radius||'?'}m · ${a.duration?Math.round(a.duration/60000)+'min':'desconhecido'}</desc>
|
||||
<sym>Anchor</sym>
|
||||
<type>anchorage</type>
|
||||
</wpt>
|
||||
`;
|
||||
}
|
||||
// Tracks: cada viagem com pontos GPS
|
||||
const trips=(state.trips||[]).filter(t=>t.track&&t.track.length>0);
|
||||
for(const t of trips){
|
||||
gpx+=` <trk>
|
||||
<name>${gpxEscape(t.destination||'Travessia '+new Date(t.dateStart||Date.now()).toLocaleDateString('pt-BR'))}</name>
|
||||
<desc>${gpxEscape((t.notes||'').slice(0,200))}</desc>
|
||||
<trkseg>
|
||||
`;
|
||||
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+=` <trkpt lat="${lat.toFixed(6)}" lon="${lng.toFixed(6)}">${ts?`<time>${new Date(ts).toISOString()}</time>`:''}</trkpt>
|
||||
`;
|
||||
}
|
||||
gpx+=` </trkseg>
|
||||
</trk>
|
||||
`;
|
||||
}
|
||||
// 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+=` <rte>
|
||||
<name>${z.kind==='forbidden'?'⛔ ':'⚠ '}${gpxEscape(z.name||'Zona')}</name>
|
||||
<desc>${z.kind} · raio ${Math.round(z.radius)}m</desc>
|
||||
`;
|
||||
for(const [lat,lng] of points){
|
||||
gpx+=` <rtept lat="${lat.toFixed(6)}" lon="${lng.toFixed(6)}"></rtept>
|
||||
`;
|
||||
}
|
||||
gpx+=` </rte>
|
||||
`;
|
||||
}
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue