feat(fleet): sistema multi-embarcação + calculadora de fundeio v1.4.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
— Frota: gerencia múltiplas embarcações (veleiro/motor/cata/RIB/outro) — Selector de barco ativo no header, modal de gerência completo — Campos por barco: nome, tipo, modelo, comprimento, boca, calado, amarra, ano — Toggle global de unidades (metros/pés) com conversão em todos displays — Calculadora de fundeio: scope ratio, raio de giro, raio sugerido p/ alarme — Dicas adaptativas por vento (auto-fetch Windy/OpenMeteo) + tipo de embarcação — Migration automática state.boat → state.boats[] (compat retroativa) — Storage interno sempre em metros (ISO), conversão só no boundary Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe78a4afa9
commit
7ccaa18bfa
3 changed files with 1240 additions and 20 deletions
|
|
@ -89,11 +89,29 @@ header{
|
|||
padding:1px 0;width:100%;
|
||||
}
|
||||
.boat-name:focus{outline:none;border-bottom:1px dashed var(--brass)}
|
||||
.boat-meta{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;color:rgba(250,242,221,.65);margin-top:2px}
|
||||
.boat-selector{
|
||||
display:inline-flex;align-items:center;gap:6px;cursor:pointer;
|
||||
text-align:left;width:auto;
|
||||
transition:opacity .15s;
|
||||
}
|
||||
.boat-selector:hover{opacity:.8}
|
||||
.boat-selector:active{transform:scale(.98)}
|
||||
.boat-chevron{font-size:14px;opacity:.6;font-style:normal;transform:translateY(1px)}
|
||||
.boat-meta{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;color:rgba(250,242,221,.65);margin-top:2px;display:flex;align-items:center;gap:8px}
|
||||
.boat-meta input{
|
||||
background:transparent;border:none;color:inherit;
|
||||
font:inherit;padding:1px 0;width:100%;
|
||||
}
|
||||
.boat-model-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.boat-edit-btn{
|
||||
background:rgba(250,242,221,.08);border:1px solid rgba(250,242,221,.18);
|
||||
color:rgba(250,242,221,.7);cursor:pointer;
|
||||
width:22px;height:22px;border-radius:6px;
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
font-size:11px;flex-shrink:0;
|
||||
transition:all .15s;
|
||||
}
|
||||
.boat-edit-btn:hover{background:rgba(250,242,221,.15);color:var(--bg-paper)}
|
||||
.boat-meta input::placeholder{color:rgba(250,242,221,.4)}
|
||||
.boat-meta input:focus{outline:none;border-bottom:1px dashed var(--brass)}
|
||||
.boat-coord{font-family:var(--f-mono);font-size:9.5px;color:var(--brass-bright);letter-spacing:.1em;text-align:right;flex-shrink:0;line-height:1.5}
|
||||
|
|
@ -465,6 +483,93 @@ header{
|
|||
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
.field-hint{font-size:10px;color:var(--sepia);margin-top:4px;font-style:italic;font-family:var(--f-display)}
|
||||
|
||||
/* === Fleet Manager === */
|
||||
.fleet-list{display:flex;flex-direction:column;gap:8px;margin-bottom:18px;max-height:50vh;overflow-y:auto}
|
||||
.fleet-item{
|
||||
display:flex;align-items:center;gap:12px;
|
||||
padding:12px 14px;background:var(--bg-canvas);
|
||||
border:1px solid var(--rule);cursor:pointer;
|
||||
transition:all .15s;
|
||||
}
|
||||
.fleet-item:hover{background:var(--bg-aged);border-color:var(--sepia)}
|
||||
.fleet-item.active{border-left:3px solid var(--brass);background:var(--bg-aged)}
|
||||
.fleet-icon{font-size:24px;flex-shrink:0;width:32px;text-align:center}
|
||||
.fleet-info{flex:1;min-width:0}
|
||||
.fleet-name{font-family:var(--f-display);font-style:italic;font-size:17px;color:var(--ink-deep);font-weight:500}
|
||||
.fleet-meta{font-family:var(--f-mono);font-size:10px;letter-spacing:.06em;color:var(--sepia);margin-top:2px;text-transform:uppercase}
|
||||
.fleet-active-badge{
|
||||
background:var(--brass);color:var(--bg-paper);
|
||||
font-family:var(--f-mono);font-size:9px;letter-spacing:.12em;
|
||||
padding:3px 8px;text-transform:uppercase;
|
||||
}
|
||||
.fleet-edit-icon{
|
||||
background:transparent;border:1px solid var(--rule);
|
||||
width:28px;height:28px;display:flex;align-items:center;justify-content:center;
|
||||
cursor:pointer;color:var(--sepia);font-size:12px;flex-shrink:0;
|
||||
}
|
||||
.fleet-edit-icon:hover{border-color:var(--brass);color:var(--brass)}
|
||||
.fleet-empty{
|
||||
text-align:center;padding:24px;color:var(--sepia);
|
||||
font-family:var(--f-display);font-style:italic;font-size:14px;
|
||||
border:1px dashed var(--rule);
|
||||
}
|
||||
.fleet-units-row{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:14px 0 0;border-top:1px solid var(--rule);
|
||||
}
|
||||
.fleet-units-label{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--sepia)}
|
||||
.fleet-units-toggle{display:flex;border:1px solid var(--rule)}
|
||||
.fleet-units-toggle button{
|
||||
background:var(--bg-canvas);border:none;
|
||||
padding:7px 14px;font-family:var(--f-mono);font-size:11px;
|
||||
letter-spacing:.08em;cursor:pointer;color:var(--ink-mid);
|
||||
transition:all .15s;
|
||||
}
|
||||
.fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)}
|
||||
.fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)}
|
||||
|
||||
/* === Anchor Calculator === */
|
||||
.anchor-calc{
|
||||
background:var(--bg-canvas);border:1px solid var(--rule);
|
||||
border-left:3px solid var(--ocean);
|
||||
padding:14px;margin:14px 0;
|
||||
}
|
||||
.anchor-calc-head{
|
||||
font-family:var(--f-mono);font-size:11px;letter-spacing:.14em;
|
||||
text-transform:uppercase;color:var(--ocean);
|
||||
margin-bottom:10px;font-weight:600;
|
||||
}
|
||||
.anchor-calc .field{margin-bottom:10px}
|
||||
.anchor-calc-result{
|
||||
display:grid;grid-template-columns:1fr 1fr;gap:8px;
|
||||
margin-top:10px;
|
||||
}
|
||||
.anchor-calc-result.full{grid-template-columns:1fr}
|
||||
.anchor-calc-stat{
|
||||
background:var(--bg-paper);padding:10px;border:1px solid var(--rule-soft);
|
||||
}
|
||||
.anchor-calc-stat-label{
|
||||
font-family:var(--f-mono);font-size:9.5px;letter-spacing:.14em;
|
||||
text-transform:uppercase;color:var(--sepia);margin-bottom:4px;
|
||||
}
|
||||
.anchor-calc-stat-value{
|
||||
font-family:var(--f-display);font-style:italic;font-size:20px;
|
||||
color:var(--ink-deep);font-weight:500;line-height:1;
|
||||
}
|
||||
.anchor-calc-stat-value.ok{color:var(--algae,#4a8c4a)}
|
||||
.anchor-calc-stat-value.warn{color:var(--sun,#c8943a)}
|
||||
.anchor-calc-stat-value.danger{color:var(--storm,#b04040)}
|
||||
.anchor-calc-advice{
|
||||
grid-column:1/-1;
|
||||
padding:10px 12px;background:var(--ocean-soft,rgba(50,90,140,.08));
|
||||
border-left:2px solid var(--ocean);
|
||||
font-family:var(--f-display);font-style:italic;font-size:13px;
|
||||
color:var(--ink-deep);line-height:1.5;
|
||||
}
|
||||
.anchor-calc-advice.danger{background:var(--storm-soft,rgba(176,64,64,.08));border-color:var(--storm)}
|
||||
.anchor-calc-advice.warn{background:var(--sun-soft,rgba(200,148,58,.1));border-color:var(--sun)}
|
||||
.anchor-calc-advice.ok{background:rgba(74,140,74,.08);border-color:var(--algae,#4a8c4a)}
|
||||
|
||||
.pax-input-row{display:flex;gap:6px}
|
||||
.pax-input-row input{flex:1}
|
||||
.pax-list{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px;min-height:24px}
|
||||
|
|
@ -1235,8 +1340,14 @@ header{
|
|||
</svg>
|
||||
<div class="boat-info">
|
||||
<div class="boat-tagline">Diário de Bordo · Logbook</div>
|
||||
<input class="boat-name" id="boat-name" value="Shivao" maxlength="40" spellcheck="false">
|
||||
<div class="boat-meta"><input id="boat-model" placeholder="Modelo / Classe / Marina" maxlength="48"></div>
|
||||
<button type="button" class="boat-name boat-selector" id="boat-selector" onclick="openFleetManager()" title="Gerenciar frota">
|
||||
<span id="boat-name-display">Shivao</span>
|
||||
<span class="boat-chevron" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div class="boat-meta">
|
||||
<span id="boat-model-display" class="boat-model-text">⛵ Veleiro</span>
|
||||
<button type="button" class="boat-edit-btn" onclick="openFleetManager()" title="Editar embarcação">✎</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -1513,19 +1624,38 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
|
|||
<!-- Anchor Setup Modal -->
|
||||
<div class="modal-backdrop" id="anchor-setup-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-head"><h3>Fundear o Shivao</h3><button class="icon-btn" onclick="closeModal('anchor-setup-modal')">✕</button></div>
|
||||
<div class="modal-head"><h3 id="anchor-modal-title">Fundear</h3><button class="icon-btn" onclick="closeModal('anchor-setup-modal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<div id="anchor-setup-status" style="font-family:var(--f-display);font-style:italic;font-size:14px;color:var(--sepia);margin-bottom:14px;text-align:center;padding:16px;background:var(--bg-canvas);border:1px dashed var(--rule)">Aguardando posição do GPS…</div>
|
||||
<div class="field"><label class="field-label">Posição registrada</label>
|
||||
<div id="anchor-coord" style="font-family:var(--f-mono);font-size:13px;color:var(--ink-deep);padding:10px 12px;background:var(--bg-canvas);border:1px solid var(--rule)">—</div>
|
||||
</div>
|
||||
|
||||
<!-- Anchor Calculator -->
|
||||
<div class="anchor-calc">
|
||||
<div class="anchor-calc-head">⚓ Calculadora de fundeio</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Profundidade <span id="anchor-depth-unit">(m)</span></label>
|
||||
<input type="number" id="anchor-depth" step="0.5" min="0" placeholder="0.0" oninput="recalcAnchor()">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Amarra lançada <span id="anchor-chain-unit">(m)</span></label>
|
||||
<input type="number" id="anchor-chain" step="1" min="0" placeholder="0" oninput="recalcAnchor()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Vento atual (nós) <span id="anchor-wind-source" style="font-weight:400;color:var(--sepia);text-transform:none;letter-spacing:0">manual</span></label>
|
||||
<input type="number" id="anchor-wind" step="1" min="0" placeholder="ex: 12" oninput="recalcAnchor()">
|
||||
</div>
|
||||
<div id="anchor-calc-result" class="anchor-calc-result"></div>
|
||||
<button type="button" class="btn btn-block btn-sm" onclick="applyRecommendedRadius()" id="anchor-apply-recommend" style="display:none;margin-top:8px">⚓ Aplicar raio sugerido</button>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">Raio de segurança</label>
|
||||
<label class="field-label">Raio de segurança (alarme)</label>
|
||||
<div class="anchor-radius-row">
|
||||
<input type="range" id="anchor-radius" min="15" max="200" step="5" value="50" oninput="updateRadiusLabel(this.value)">
|
||||
<span class="anchor-radius-val" id="anchor-radius-val">50 m</span>
|
||||
</div>
|
||||
<div class="field-hint">Distância máxima da âncora antes de disparar o alarme. Considere: linha de fundeio + maré + giro do barco.</div>
|
||||
<div class="field-hint">Distância máxima da âncora antes de disparar o alarme. Use a calculadora acima pra estimar.</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Contatos de emergência</label>
|
||||
<div id="anchor-contacts-summary" style="font-family:var(--f-mono);font-size:11px;color:var(--sepia);padding:8px 0">Nenhum contato configurado</div>
|
||||
|
|
@ -1715,11 +1845,255 @@ Hora: {HORA}</textarea>
|
|||
</div>
|
||||
|
||||
<button class="fab" id="fab" onclick="quickAdd()" title="Adicionar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
|
||||
<!-- Fleet Manager Modal -->
|
||||
<div class="modal-backdrop" id="fleet-modal" onclick="if(event.target===this)closeModal('fleet-modal')">
|
||||
<div class="modal" style="max-width:520px">
|
||||
<div class="modal-head">
|
||||
<h3>⚓ Minha Frota</h3>
|
||||
<button class="icon-btn" onclick="closeModal('fleet-modal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="fleet-list" class="fleet-list"></div>
|
||||
<div class="fleet-units-row">
|
||||
<span class="fleet-units-label">Unidades</span>
|
||||
<div class="fleet-units-toggle" id="fleet-units-toggle">
|
||||
<button type="button" data-unit="metric" onclick="setUnits('metric')">Metros</button>
|
||||
<button type="button" data-unit="imperial" onclick="setUnits('imperial')">Pés</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" onclick="closeModal('fleet-modal')">Fechar</button>
|
||||
<button class="btn btn-primary" onclick="openBoatEditor(null)">+ Nova embarcação</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boat Editor Modal -->
|
||||
<div class="modal-backdrop" id="boat-editor-modal" onclick="if(event.target===this)closeModal('boat-editor-modal')">
|
||||
<div class="modal" style="max-width:480px">
|
||||
<div class="modal-head">
|
||||
<h3 id="boat-editor-title">Nova embarcação</h3>
|
||||
<button class="icon-btn" onclick="closeModal('boat-editor-modal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="boat-editor-form" onsubmit="saveBoatFromForm(event)">
|
||||
<input type="hidden" id="boat-edit-id">
|
||||
<div class="field"><label class="field-label">Nome</label>
|
||||
<input type="text" id="boat-edit-name" required maxlength="40" placeholder="ex: Shivao, Maré Alta">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Tipo de embarcação</label>
|
||||
<select id="boat-edit-type">
|
||||
<option value="sailing">⛵ Veleiro</option>
|
||||
<option value="motor">🛥️ Lancha / Motor</option>
|
||||
<option value="catamaran">🚤 Catamarã</option>
|
||||
<option value="rib">🚣 Bote / RIB</option>
|
||||
<option value="other">🛳️ Outro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Modelo / Classe (opcional)</label>
|
||||
<input type="text" id="boat-edit-model" maxlength="48" placeholder="Beneteau 36, Delta 28...">
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Comprimento <span id="boat-len-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-length" step="0.1" min="0" placeholder="0.0">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Boca / Largura <span id="boat-beam-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-beam" step="0.1" min="0" placeholder="0.0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Calado <span id="boat-draft-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-draft" step="0.1" min="0" placeholder="profundidade casco">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Amarra a bordo <span id="boat-chain-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-chain" step="1" min="0" placeholder="total disponível">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Ano (opcional)</label>
|
||||
<input type="number" id="boat-edit-year" min="1900" max="2100" placeholder="ex: 2018">
|
||||
</div>
|
||||
<div class="field-hint">⚓ Comprimento + calado são usados pra calcular raio de giro no fundeio.</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn btn-danger" id="boat-edit-delete" onclick="deleteBoatFromEditor()" style="display:none">Excluir</button>
|
||||
<button class="btn" onclick="closeModal('boat-editor-modal')">Cancelar</button>
|
||||
<button class="btn btn-primary" onclick="document.getElementById('boat-editor-form').requestSubmit()">Salvar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="zone-toast" id="zone-toast"></div>
|
||||
|
||||
<script>
|
||||
const state={boat:{name:'Shivao',model:''},trips:[],maint:[],pending:[],passengers:[],contacts:[],alarmTemplate:'🚨 ALERTA: Shivao saiu da posição de fundeio.\nEstou em {LAT}, {LNG}\nMapa: https://maps.google.com/?q={LAT},{LNG}\nHora: {HORA}',cloud:{url:'',token:'',lastSync:0},auth:null,license:null,webhooks:{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}},anchorHistory:[],zones:[],checklists:[],weatherCfg:{windyKey:'',model:'gfs'}};
|
||||
const state={boat:{name:'Shivao',model:''},boats:[],activeBoatId:null,units:'metric',trips:[],maint:[],pending:[],passengers:[],contacts:[],alarmTemplate:'🚨 ALERTA: Shivao saiu da posição de fundeio.\nEstou em {LAT}, {LNG}\nMapa: https://maps.google.com/?q={LAT},{LNG}\nHora: {HORA}',cloud:{url:'',token:'',lastSync:0},auth:null,license:null,webhooks:{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}},anchorHistory:[],zones:[],checklists:[],weatherCfg:{windyKey:'',model:'gfs'}};
|
||||
|
||||
// ============ NAUTICAL HELPERS (multi-boat + fleet + calculations) ============
|
||||
// Storage interno SEMPRE em metros (ISO). Conversão só pra display.
|
||||
const FT_PER_M=3.28084;
|
||||
const M_PER_FT=0.3048;
|
||||
|
||||
function isImperial(){return state.units==='imperial'}
|
||||
function lengthUnit(){return isImperial()?'ft':'m'}
|
||||
|
||||
// Converte storage (sempre metros) → display
|
||||
function lenToDisplay(meters){if(meters==null||isNaN(meters))return null;return isImperial()?meters*FT_PER_M:meters}
|
||||
// Converte display (que usuário digitou) → storage (metros)
|
||||
function lenFromInput(value){if(value==null||value==='')return null;const n=parseFloat(value);if(isNaN(n))return null;return isImperial()?n*M_PER_FT:n}
|
||||
function fmtLen(meters,decimals=1){if(meters==null||isNaN(meters))return '—';return lenToDisplay(meters).toFixed(decimals)+' '+lengthUnit()}
|
||||
|
||||
// Tipos de embarcação com defaults sensatos
|
||||
const BOAT_TYPES={
|
||||
sailing:{label:'Veleiro',icon:'⛵',defaultBeamRatio:0.30,defaultDraftRatio:0.18,scopeBonus:0},
|
||||
motor:{label:'Motor',icon:'🛥️',defaultBeamRatio:0.32,defaultDraftRatio:0.10,scopeBonus:-0.5},
|
||||
catamaran:{label:'Catamarã',icon:'🚤',defaultBeamRatio:0.55,defaultDraftRatio:0.08,scopeBonus:-0.5},
|
||||
rib:{label:'Bote/RIB',icon:'🚣',defaultBeamRatio:0.35,defaultDraftRatio:0.06,scopeBonus:-1},
|
||||
other:{label:'Outro',icon:'🛳️',defaultBeamRatio:0.28,defaultDraftRatio:0.15,scopeBonus:0},
|
||||
};
|
||||
|
||||
// Migration: state.boat (antigo) → state.boats[0] (novo)
|
||||
function migrateBoatsSchema(){
|
||||
if(!state.boats||!Array.isArray(state.boats))state.boats=[];
|
||||
if(state.boats.length===0&&state.boat&&state.boat.name){
|
||||
const b={
|
||||
id:uid(),
|
||||
name:state.boat.name||'Shivao',
|
||||
model:state.boat.model||'',
|
||||
type:'sailing',
|
||||
length:null,beam:null,draft:null,
|
||||
chainTotal:null,
|
||||
year:null,
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
state.activeBoatId=b.id;
|
||||
}
|
||||
if(!state.activeBoatId&&state.boats[0])state.activeBoatId=state.boats[0].id;
|
||||
if(!state.units)state.units='metric';
|
||||
}
|
||||
|
||||
function activeBoat(){
|
||||
return state.boats.find(b=>b.id===state.activeBoatId)||state.boats[0]||null;
|
||||
}
|
||||
|
||||
function setActiveBoat(id){
|
||||
state.activeBoatId=id;
|
||||
saveState();
|
||||
bindHeader();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function addBoat(data){
|
||||
const b={
|
||||
id:uid(),
|
||||
name:data.name||'Embarcação',
|
||||
model:data.model||'',
|
||||
type:data.type||'sailing',
|
||||
length:lenFromInput(data.length),
|
||||
beam:lenFromInput(data.beam),
|
||||
draft:lenFromInput(data.draft),
|
||||
chainTotal:lenFromInput(data.chainTotal),
|
||||
year:data.year?parseInt(data.year):null,
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
if(!state.activeBoatId)state.activeBoatId=b.id;
|
||||
saveState();
|
||||
return b;
|
||||
}
|
||||
|
||||
function updateBoat(id,data){
|
||||
const b=state.boats.find(x=>x.id===id);
|
||||
if(!b)return;
|
||||
if('name' in data)b.name=data.name;
|
||||
if('model' in data)b.model=data.model;
|
||||
if('type' in data)b.type=data.type;
|
||||
if('length' in data)b.length=lenFromInput(data.length);
|
||||
if('beam' in data)b.beam=lenFromInput(data.beam);
|
||||
if('draft' in data)b.draft=lenFromInput(data.draft);
|
||||
if('chainTotal' in data)b.chainTotal=lenFromInput(data.chainTotal);
|
||||
if('year' in data)b.year=data.year?parseInt(data.year):null;
|
||||
saveState();
|
||||
}
|
||||
|
||||
function removeBoat(id){
|
||||
if(state.boats.length<=1){toast('Mantenha pelo menos 1 embarcação');return}
|
||||
state.boats=state.boats.filter(b=>b.id!==id);
|
||||
if(state.activeBoatId===id)state.activeBoatId=state.boats[0]?.id||null;
|
||||
saveState();
|
||||
}
|
||||
|
||||
// ============ ANCHOR CALCULATIONS (regras náuticas) ============
|
||||
// Scope ratio = comprimento_amarra / profundidade
|
||||
// Recomendado: 5:1 normal, 7:1 vento moderado (15-25kn), 10:1 tempestade (>25kn)
|
||||
function scopeRatio(chainDeployedM,depthM){
|
||||
if(!depthM||depthM<=0)return null;
|
||||
return chainDeployedM/depthM;
|
||||
}
|
||||
|
||||
// Raio efetivo de giro = √(amarra² − profundidade²) + comprimento_do_barco
|
||||
// Esse é o círculo dentro do qual a popa do barco pode oscilar com a corrente/vento
|
||||
function calcSwingRadius(chainDeployedM,depthM,boatLengthM){
|
||||
if(!chainDeployedM||!depthM||chainDeployedM<=depthM)return null;
|
||||
const horizontal=Math.sqrt(chainDeployedM*chainDeployedM-depthM*depthM);
|
||||
return horizontal+(boatLengthM||0);
|
||||
}
|
||||
|
||||
// Recomenda comprimento de amarra baseado em condição
|
||||
// windKn: vento em nós; depth: profundidade
|
||||
// Retorna {chainM, ratio, condition}
|
||||
function recommendChain(depthM,windKn,boatType){
|
||||
let baseRatio=5; // calmo
|
||||
let condition='Condição calma';
|
||||
if(windKn>=25){baseRatio=10;condition='Tempestade'}
|
||||
else if(windKn>=15){baseRatio=7;condition='Vento moderado'}
|
||||
else if(windKn>=8){baseRatio=6;condition='Vento leve'}
|
||||
// Ajuste por tipo de embarcação
|
||||
const t=BOAT_TYPES[boatType]||BOAT_TYPES.sailing;
|
||||
const ratio=Math.max(3,baseRatio+t.scopeBonus);
|
||||
return {chainM:depthM*ratio,ratio,condition};
|
||||
}
|
||||
|
||||
// Status visual da configuração atual de fundeio
|
||||
function anchorStatus(chainDeployedM,depthM,windKn){
|
||||
if(!chainDeployedM||!depthM)return {level:'unknown',msg:'Informe profundidade + amarra'};
|
||||
const r=scopeRatio(chainDeployedM,depthM);
|
||||
const minR=windKn>=25?10:windKn>=15?7:5;
|
||||
if(r>=minR)return {level:'ok',msg:`Adequado (${r.toFixed(1)}:1)`};
|
||||
if(r>=minR-1)return {level:'warn',msg:`Justo (${r.toFixed(1)}:1) — folgar mais ${((minR-r)*depthM).toFixed(0)}m`};
|
||||
return {level:'danger',msg:`INSUFICIENTE (${r.toFixed(1)}:1) — necessário mínimo ${minR}:1`};
|
||||
}
|
||||
|
||||
// Recupera velocidade de vento atual (em nós) do cache de weather, qualquer provider
|
||||
function currentWindKnots(){
|
||||
try{
|
||||
if(!weather||!weather.data)return null;
|
||||
const d=weather.data;
|
||||
if(d.provider==='openmeteo'){
|
||||
return d.forecast?.current?.wind_speed_10m??null;
|
||||
}
|
||||
if(d.provider==='windy'){
|
||||
const m=d.main;
|
||||
const u=m?.['wind_u-surface']?.[0];
|
||||
const v=m?.['wind_v-surface']?.[0];
|
||||
if(u==null||v==null)return null;
|
||||
return Math.sqrt(u*u+v*v)*1.94384;
|
||||
}
|
||||
}catch(e){}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dica geral baseada em vento + condição
|
||||
function anchorAdvice(windKn,boatType){
|
||||
const t=BOAT_TYPES[boatType]||BOAT_TYPES.sailing;
|
||||
if(windKn>=30)return `🌊 Tempestade · scope mínimo 10:1, considere segunda âncora`;
|
||||
if(windKn>=20)return `🌬️ Vento forte · scope ${t.scopeBonus<0?'7':'8'}:1 + reforçar amarras de bordo`;
|
||||
if(windKn>=12)return `💨 Vento moderado · scope 7:1 ${t.label==='Catamarã'?'(catamarã: vigiar deriva lateral)':''}`;
|
||||
if(windKn>=5)return `🍃 Vento leve · scope 5-6:1 padrão`;
|
||||
return `⚓ Calmo · scope mínimo 5:1`;
|
||||
}
|
||||
const STORAGE_KEY='diario_bordo_v3';
|
||||
const TRACKING_KEY='diario_tracking_v3';
|
||||
const ANCHOR_KEY='diario_anchor_v3';
|
||||
|
|
@ -1736,7 +2110,8 @@ function dbPut(item){return new Promise((r,j)=>{const q=db.transaction('media','
|
|||
function dbDelete(id){return new Promise((r,j)=>{const q=db.transaction('media','readwrite').objectStore('media').delete(id);q.onsuccess=()=>r();q.onerror=()=>j(q.error)})}
|
||||
function dbAll(){return new Promise((r,j)=>{const q=db.transaction('media','readonly').objectStore('media').getAll();q.onsuccess=()=>r(q.result);q.onerror=()=>j(q.error)})}
|
||||
|
||||
function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'}}}catch(e){console.warn(e)}
|
||||
function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'};state.boats=d.boats||[];state.activeBoatId=d.activeBoatId||null;state.units=d.units||'metric'}}catch(e){console.warn(e)}
|
||||
migrateBoatsSchema();
|
||||
// popular checklists padrão se vazio
|
||||
if(!state.checklists.length){
|
||||
state.checklists=[
|
||||
|
|
@ -1865,7 +2240,142 @@ function renderGPSBanner(){
|
|||
}
|
||||
}
|
||||
|
||||
function bindHeader(){const n=document.getElementById('boat-name'),m=document.getElementById('boat-model');n.value=state.boat.name||'Shivao';m.value=state.boat.model||'';n.addEventListener('input',e=>{state.boat.name=e.target.value;saveState()});m.addEventListener('input',e=>{state.boat.model=e.target.value;saveState()})}
|
||||
function bindHeader(){
|
||||
const nameEl=document.getElementById('boat-name-display');
|
||||
const metaEl=document.getElementById('boat-model-display');
|
||||
if(!nameEl||!metaEl)return;
|
||||
const b=activeBoat();
|
||||
if(!b){nameEl.textContent='Sem embarcação';metaEl.textContent='Toque para adicionar';return}
|
||||
nameEl.textContent=b.name||'Embarcação';
|
||||
const t=BOAT_TYPES[b.type]||BOAT_TYPES.sailing;
|
||||
const parts=[`${t.icon} ${t.label}`];
|
||||
if(b.model)parts.push(b.model);
|
||||
if(b.length)parts.push(fmtLen(b.length,1));
|
||||
metaEl.textContent=parts.join(' · ');
|
||||
// sync legacy state.boat para compat com qualquer código antigo que ainda use
|
||||
state.boat={name:b.name,model:b.model};
|
||||
}
|
||||
|
||||
// ============ FLEET MANAGER ============
|
||||
function openFleetManager(){
|
||||
renderFleetList();
|
||||
syncUnitsToggle();
|
||||
openModal('fleet-modal');
|
||||
}
|
||||
|
||||
function renderFleetList(){
|
||||
const el=document.getElementById('fleet-list');
|
||||
if(!el)return;
|
||||
if(!state.boats||state.boats.length===0){
|
||||
el.innerHTML=`<div class="fleet-empty">Nenhuma embarcação ainda.<br>Toque "+ Nova" pra começar.</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML=state.boats.map(b=>{
|
||||
const t=BOAT_TYPES[b.type]||BOAT_TYPES.sailing;
|
||||
const isActive=b.id===state.activeBoatId;
|
||||
const meta=[t.label];
|
||||
if(b.model)meta.push(b.model);
|
||||
if(b.length)meta.push(fmtLen(b.length,1));
|
||||
return `<div class="fleet-item ${isActive?'active':''}" onclick="setActiveBoatAndClose('${b.id}')">
|
||||
<div class="fleet-icon">${t.icon}</div>
|
||||
<div class="fleet-info">
|
||||
<div class="fleet-name">${escapeHtml(b.name)}</div>
|
||||
<div class="fleet-meta">${meta.map(escapeHtml).join(' · ')}</div>
|
||||
</div>
|
||||
${isActive?'<div class="fleet-active-badge">ATIVA</div>':''}
|
||||
<button type="button" class="fleet-edit-icon" onclick="event.stopPropagation();openBoatEditor('${b.id}')" title="Editar">✎</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function setActiveBoatAndClose(id){
|
||||
setActiveBoat(id);
|
||||
renderFleetList();
|
||||
toast('Embarcação ativa: '+(activeBoat()?.name||''));
|
||||
}
|
||||
|
||||
function openBoatEditor(id){
|
||||
const isNew=!id;
|
||||
document.getElementById('boat-editor-title').textContent=isNew?'Nova embarcação':'Editar embarcação';
|
||||
document.getElementById('boat-edit-id').value=id||'';
|
||||
const b=isNew?{}:state.boats.find(x=>x.id===id)||{};
|
||||
document.getElementById('boat-edit-name').value=b.name||'';
|
||||
document.getElementById('boat-edit-type').value=b.type||'sailing';
|
||||
document.getElementById('boat-edit-model').value=b.model||'';
|
||||
// Mostrar valores convertidos pra unidade atual
|
||||
const setLen=(elId,m)=>{const el=document.getElementById(elId);el.value=m==null?'':lenToDisplay(m).toFixed(1)};
|
||||
setLen('boat-edit-length',b.length);
|
||||
setLen('boat-edit-beam',b.beam);
|
||||
setLen('boat-edit-draft',b.draft);
|
||||
// Amarra usa metros inteiros normalmente
|
||||
const chainEl=document.getElementById('boat-edit-chain');
|
||||
chainEl.value=b.chainTotal==null?'':lenToDisplay(b.chainTotal).toFixed(0);
|
||||
document.getElementById('boat-edit-year').value=b.year||'';
|
||||
// Atualizar labels de unidade
|
||||
const unitLabel=`(${lengthUnit()})`;
|
||||
['boat-len-unit','boat-beam-unit','boat-draft-unit','boat-chain-unit'].forEach(id=>{
|
||||
const el=document.getElementById(id);if(el)el.textContent=unitLabel;
|
||||
});
|
||||
// Botão excluir só aparece em edição e se houver mais de 1 barco
|
||||
document.getElementById('boat-edit-delete').style.display=(!isNew&&state.boats.length>1)?'inline-flex':'none';
|
||||
closeModal('fleet-modal');
|
||||
openModal('boat-editor-modal');
|
||||
setTimeout(()=>document.getElementById('boat-edit-name').focus(),100);
|
||||
}
|
||||
|
||||
function saveBoatFromForm(ev){
|
||||
ev.preventDefault();
|
||||
const id=document.getElementById('boat-edit-id').value;
|
||||
const data={
|
||||
name:document.getElementById('boat-edit-name').value.trim(),
|
||||
type:document.getElementById('boat-edit-type').value,
|
||||
model:document.getElementById('boat-edit-model').value.trim(),
|
||||
length:document.getElementById('boat-edit-length').value,
|
||||
beam:document.getElementById('boat-edit-beam').value,
|
||||
draft:document.getElementById('boat-edit-draft').value,
|
||||
chainTotal:document.getElementById('boat-edit-chain').value,
|
||||
year:document.getElementById('boat-edit-year').value,
|
||||
};
|
||||
if(!data.name){toast('Informe o nome da embarcação');return}
|
||||
if(id){updateBoat(id,data);toast('Embarcação atualizada')}
|
||||
else{const b=addBoat(data);state.activeBoatId=b.id;saveState();toast('Embarcação adicionada')}
|
||||
closeModal('boat-editor-modal');
|
||||
bindHeader();
|
||||
renderAll();
|
||||
openFleetManager();
|
||||
}
|
||||
|
||||
function deleteBoatFromEditor(){
|
||||
const id=document.getElementById('boat-edit-id').value;
|
||||
if(!id)return;
|
||||
const b=state.boats.find(x=>x.id===id);
|
||||
if(!b)return;
|
||||
if(!confirm(`Excluir "${b.name}"? Esta ação não pode ser desfeita.`))return;
|
||||
removeBoat(id);
|
||||
closeModal('boat-editor-modal');
|
||||
bindHeader();
|
||||
renderAll();
|
||||
openFleetManager();
|
||||
toast('Embarcação removida');
|
||||
}
|
||||
|
||||
function setUnits(u){
|
||||
if(u!=='metric'&&u!=='imperial')return;
|
||||
state.units=u;
|
||||
saveState();
|
||||
syncUnitsToggle();
|
||||
bindHeader();
|
||||
renderAll();
|
||||
toast(u==='metric'?'Unidade: metros':'Unidade: pés');
|
||||
}
|
||||
|
||||
function syncUnitsToggle(){
|
||||
const wrap=document.getElementById('fleet-units-toggle');
|
||||
if(!wrap)return;
|
||||
wrap.querySelectorAll('button').forEach(b=>{
|
||||
b.classList.toggle('active',b.dataset.unit===state.units);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(t=>{t.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));t.classList.add('active');document.getElementById('panel-'+t.dataset.panel).classList.add('active');document.getElementById('fab').style.display=['trips','maintenance','pending','zones'].includes(t.dataset.panel)?'flex':'none';if(t.dataset.panel==='export'){updateStorageInfo();bindCloudInputs();renderCloudStatus();renderShareList();bindWeatherInputs();renderAuthBox()}if(t.dataset.panel==='zones')renderZones();window.scrollTo(0,0)})});
|
||||
function quickAdd(){const a=document.querySelector('.tab.active').dataset.panel;if(a==='maintenance')openMaintModal();else if(a==='pending')openPendingModal();else if(a==='zones')openZoneEditor();else openTripModal()}
|
||||
|
|
@ -2279,6 +2789,7 @@ async function openAnchorSetup(){
|
|||
updateRadiusLabel(50);
|
||||
pendingAnchorPos=null;
|
||||
updateContactsSummary();
|
||||
initAnchorCalc();
|
||||
openModal('anchor-setup-modal');
|
||||
// tentar obter posição
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
|
|
@ -2293,6 +2804,105 @@ async function openAnchorSetup(){
|
|||
);
|
||||
}
|
||||
|
||||
// ============ ANCHOR CALCULATOR (modal) ============
|
||||
let anchorCalcLastRecommendedRadius=null;
|
||||
|
||||
function initAnchorCalc(){
|
||||
// Atualiza label de embarcação no título
|
||||
const b=activeBoat();
|
||||
const titleEl=document.getElementById('anchor-modal-title');
|
||||
if(titleEl)titleEl.textContent=b?`Fundear · ${b.name}`:'Fundear';
|
||||
// Atualiza unidades nos labels
|
||||
const u=`(${lengthUnit()})`;
|
||||
const dEl=document.getElementById('anchor-depth-unit');if(dEl)dEl.textContent=u;
|
||||
const cEl=document.getElementById('anchor-chain-unit');if(cEl)cEl.textContent=u;
|
||||
// Reset inputs
|
||||
document.getElementById('anchor-depth').value='';
|
||||
document.getElementById('anchor-chain').value='';
|
||||
// Pré-popular vento se Windy/OpenMeteo já fetchou
|
||||
const wKn=currentWindKnots();
|
||||
const wEl=document.getElementById('anchor-wind');
|
||||
const srcEl=document.getElementById('anchor-wind-source');
|
||||
if(wKn!=null){
|
||||
wEl.value=Math.round(wKn);
|
||||
if(srcEl)srcEl.textContent=`auto: ${weather.data?.provider==='windy'?'Windy':'Open-Meteo'}`;
|
||||
}else{
|
||||
wEl.value='';
|
||||
if(srcEl)srcEl.textContent='manual (sem GPS na meteo ainda)';
|
||||
}
|
||||
document.getElementById('anchor-apply-recommend').style.display='none';
|
||||
document.getElementById('anchor-calc-result').innerHTML='';
|
||||
recalcAnchor();
|
||||
}
|
||||
|
||||
function recalcAnchor(){
|
||||
const depthDisplay=parseFloat(document.getElementById('anchor-depth').value);
|
||||
const chainDisplay=parseFloat(document.getElementById('anchor-chain').value);
|
||||
const windKn=parseFloat(document.getElementById('anchor-wind').value)||0;
|
||||
const depth=isNaN(depthDisplay)?null:lenFromInput(depthDisplay);
|
||||
const chain=isNaN(chainDisplay)?null:lenFromInput(chainDisplay);
|
||||
const b=activeBoat();
|
||||
const boatLength=b?.length||0;
|
||||
const boatType=b?.type||'sailing';
|
||||
|
||||
const out=document.getElementById('anchor-calc-result');
|
||||
if(!depth){
|
||||
out.classList.add('full');
|
||||
out.innerHTML=`<div class="anchor-calc-advice">${anchorAdvice(windKn,boatType)}<br><small style="color:var(--sepia)">Informe a profundidade pra calcular.</small></div>`;
|
||||
document.getElementById('anchor-apply-recommend').style.display='none';
|
||||
return;
|
||||
}
|
||||
out.classList.remove('full');
|
||||
|
||||
const rec=recommendChain(depth,windKn,boatType);
|
||||
const status=anchorStatus(chain,depth,windKn);
|
||||
const swing=calcSwingRadius(chain,depth,boatLength);
|
||||
|
||||
// Sugestão de raio: swing radius + buffer GPS (15m) ou se não tiver chain, baseado em recomendação
|
||||
const recommendedRadiusM=swing
|
||||
? Math.ceil(swing+15)
|
||||
: Math.ceil(Math.sqrt(Math.max(0,rec.chainM*rec.chainM-depth*depth))+boatLength+15);
|
||||
anchorCalcLastRecommendedRadius=recommendedRadiusM;
|
||||
|
||||
const advice=anchorAdvice(windKn,boatType);
|
||||
const adviceClass=status.level==='danger'?'danger':status.level==='warn'?'warn':status.level==='ok'?'ok':'';
|
||||
|
||||
out.innerHTML=`
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Amarra recomendada</div>
|
||||
<div class="anchor-calc-stat-value">${fmtLen(rec.chainM,0)}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">${rec.ratio.toFixed(1)}:1 · ${rec.condition}</div>
|
||||
</div>
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Status atual</div>
|
||||
<div class="anchor-calc-stat-value ${status.level}">${status.level==='ok'?'✓ OK':status.level==='warn'?'⚠ JUSTO':status.level==='danger'?'✗ INSUF.':'—'}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">${escapeHtml(status.msg)}</div>
|
||||
</div>
|
||||
${swing?`
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Raio de giro</div>
|
||||
<div class="anchor-calc-stat-value">${fmtLen(swing,0)}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">amarra→popa + barco</div>
|
||||
</div>
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Raio sugerido (alarme)</div>
|
||||
<div class="anchor-calc-stat-value">${fmtLen(recommendedRadiusM,0)}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">+15m buffer GPS</div>
|
||||
</div>`:''}
|
||||
<div class="anchor-calc-advice ${adviceClass}">${advice}${b?` · <strong>${BOAT_TYPES[boatType].icon} ${b.name}</strong> ${b.length?`(${fmtLen(b.length,1)})`:''}`:''}</div>
|
||||
`;
|
||||
document.getElementById('anchor-apply-recommend').style.display=swing?'inline-flex':'none';
|
||||
}
|
||||
|
||||
function applyRecommendedRadius(){
|
||||
if(!anchorCalcLastRecommendedRadius)return;
|
||||
// Slider só aceita 15-200m
|
||||
const v=Math.max(15,Math.min(200,anchorCalcLastRecommendedRadius));
|
||||
document.getElementById('anchor-radius').value=v;
|
||||
updateRadiusLabel(v);
|
||||
toast(`Raio de alarme ajustado para ${v}m`);
|
||||
}
|
||||
|
||||
function updateRadiusLabel(v){document.getElementById('anchor-radius-val').textContent=`${v} m`}
|
||||
function updateContactsSummary(){
|
||||
const el=document.getElementById('anchor-contacts-summary');
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ android {
|
|||
applicationId "br.com.pontualtech.shivao"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 3
|
||||
versionName "1.3.0"
|
||||
versionCode 4
|
||||
versionName "1.4.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
|||
|
|
@ -89,11 +89,29 @@ header{
|
|||
padding:1px 0;width:100%;
|
||||
}
|
||||
.boat-name:focus{outline:none;border-bottom:1px dashed var(--brass)}
|
||||
.boat-meta{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;color:rgba(250,242,221,.65);margin-top:2px}
|
||||
.boat-selector{
|
||||
display:inline-flex;align-items:center;gap:6px;cursor:pointer;
|
||||
text-align:left;width:auto;
|
||||
transition:opacity .15s;
|
||||
}
|
||||
.boat-selector:hover{opacity:.8}
|
||||
.boat-selector:active{transform:scale(.98)}
|
||||
.boat-chevron{font-size:14px;opacity:.6;font-style:normal;transform:translateY(1px)}
|
||||
.boat-meta{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;color:rgba(250,242,221,.65);margin-top:2px;display:flex;align-items:center;gap:8px}
|
||||
.boat-meta input{
|
||||
background:transparent;border:none;color:inherit;
|
||||
font:inherit;padding:1px 0;width:100%;
|
||||
}
|
||||
.boat-model-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.boat-edit-btn{
|
||||
background:rgba(250,242,221,.08);border:1px solid rgba(250,242,221,.18);
|
||||
color:rgba(250,242,221,.7);cursor:pointer;
|
||||
width:22px;height:22px;border-radius:6px;
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
font-size:11px;flex-shrink:0;
|
||||
transition:all .15s;
|
||||
}
|
||||
.boat-edit-btn:hover{background:rgba(250,242,221,.15);color:var(--bg-paper)}
|
||||
.boat-meta input::placeholder{color:rgba(250,242,221,.4)}
|
||||
.boat-meta input:focus{outline:none;border-bottom:1px dashed var(--brass)}
|
||||
.boat-coord{font-family:var(--f-mono);font-size:9.5px;color:var(--brass-bright);letter-spacing:.1em;text-align:right;flex-shrink:0;line-height:1.5}
|
||||
|
|
@ -465,6 +483,93 @@ header{
|
|||
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
.field-hint{font-size:10px;color:var(--sepia);margin-top:4px;font-style:italic;font-family:var(--f-display)}
|
||||
|
||||
/* === Fleet Manager === */
|
||||
.fleet-list{display:flex;flex-direction:column;gap:8px;margin-bottom:18px;max-height:50vh;overflow-y:auto}
|
||||
.fleet-item{
|
||||
display:flex;align-items:center;gap:12px;
|
||||
padding:12px 14px;background:var(--bg-canvas);
|
||||
border:1px solid var(--rule);cursor:pointer;
|
||||
transition:all .15s;
|
||||
}
|
||||
.fleet-item:hover{background:var(--bg-aged);border-color:var(--sepia)}
|
||||
.fleet-item.active{border-left:3px solid var(--brass);background:var(--bg-aged)}
|
||||
.fleet-icon{font-size:24px;flex-shrink:0;width:32px;text-align:center}
|
||||
.fleet-info{flex:1;min-width:0}
|
||||
.fleet-name{font-family:var(--f-display);font-style:italic;font-size:17px;color:var(--ink-deep);font-weight:500}
|
||||
.fleet-meta{font-family:var(--f-mono);font-size:10px;letter-spacing:.06em;color:var(--sepia);margin-top:2px;text-transform:uppercase}
|
||||
.fleet-active-badge{
|
||||
background:var(--brass);color:var(--bg-paper);
|
||||
font-family:var(--f-mono);font-size:9px;letter-spacing:.12em;
|
||||
padding:3px 8px;text-transform:uppercase;
|
||||
}
|
||||
.fleet-edit-icon{
|
||||
background:transparent;border:1px solid var(--rule);
|
||||
width:28px;height:28px;display:flex;align-items:center;justify-content:center;
|
||||
cursor:pointer;color:var(--sepia);font-size:12px;flex-shrink:0;
|
||||
}
|
||||
.fleet-edit-icon:hover{border-color:var(--brass);color:var(--brass)}
|
||||
.fleet-empty{
|
||||
text-align:center;padding:24px;color:var(--sepia);
|
||||
font-family:var(--f-display);font-style:italic;font-size:14px;
|
||||
border:1px dashed var(--rule);
|
||||
}
|
||||
.fleet-units-row{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:14px 0 0;border-top:1px solid var(--rule);
|
||||
}
|
||||
.fleet-units-label{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--sepia)}
|
||||
.fleet-units-toggle{display:flex;border:1px solid var(--rule)}
|
||||
.fleet-units-toggle button{
|
||||
background:var(--bg-canvas);border:none;
|
||||
padding:7px 14px;font-family:var(--f-mono);font-size:11px;
|
||||
letter-spacing:.08em;cursor:pointer;color:var(--ink-mid);
|
||||
transition:all .15s;
|
||||
}
|
||||
.fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)}
|
||||
.fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)}
|
||||
|
||||
/* === Anchor Calculator === */
|
||||
.anchor-calc{
|
||||
background:var(--bg-canvas);border:1px solid var(--rule);
|
||||
border-left:3px solid var(--ocean);
|
||||
padding:14px;margin:14px 0;
|
||||
}
|
||||
.anchor-calc-head{
|
||||
font-family:var(--f-mono);font-size:11px;letter-spacing:.14em;
|
||||
text-transform:uppercase;color:var(--ocean);
|
||||
margin-bottom:10px;font-weight:600;
|
||||
}
|
||||
.anchor-calc .field{margin-bottom:10px}
|
||||
.anchor-calc-result{
|
||||
display:grid;grid-template-columns:1fr 1fr;gap:8px;
|
||||
margin-top:10px;
|
||||
}
|
||||
.anchor-calc-result.full{grid-template-columns:1fr}
|
||||
.anchor-calc-stat{
|
||||
background:var(--bg-paper);padding:10px;border:1px solid var(--rule-soft);
|
||||
}
|
||||
.anchor-calc-stat-label{
|
||||
font-family:var(--f-mono);font-size:9.5px;letter-spacing:.14em;
|
||||
text-transform:uppercase;color:var(--sepia);margin-bottom:4px;
|
||||
}
|
||||
.anchor-calc-stat-value{
|
||||
font-family:var(--f-display);font-style:italic;font-size:20px;
|
||||
color:var(--ink-deep);font-weight:500;line-height:1;
|
||||
}
|
||||
.anchor-calc-stat-value.ok{color:var(--algae,#4a8c4a)}
|
||||
.anchor-calc-stat-value.warn{color:var(--sun,#c8943a)}
|
||||
.anchor-calc-stat-value.danger{color:var(--storm,#b04040)}
|
||||
.anchor-calc-advice{
|
||||
grid-column:1/-1;
|
||||
padding:10px 12px;background:var(--ocean-soft,rgba(50,90,140,.08));
|
||||
border-left:2px solid var(--ocean);
|
||||
font-family:var(--f-display);font-style:italic;font-size:13px;
|
||||
color:var(--ink-deep);line-height:1.5;
|
||||
}
|
||||
.anchor-calc-advice.danger{background:var(--storm-soft,rgba(176,64,64,.08));border-color:var(--storm)}
|
||||
.anchor-calc-advice.warn{background:var(--sun-soft,rgba(200,148,58,.1));border-color:var(--sun)}
|
||||
.anchor-calc-advice.ok{background:rgba(74,140,74,.08);border-color:var(--algae,#4a8c4a)}
|
||||
|
||||
.pax-input-row{display:flex;gap:6px}
|
||||
.pax-input-row input{flex:1}
|
||||
.pax-list{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px;min-height:24px}
|
||||
|
|
@ -1235,8 +1340,14 @@ header{
|
|||
</svg>
|
||||
<div class="boat-info">
|
||||
<div class="boat-tagline">Diário de Bordo · Logbook</div>
|
||||
<input class="boat-name" id="boat-name" value="Shivao" maxlength="40" spellcheck="false">
|
||||
<div class="boat-meta"><input id="boat-model" placeholder="Modelo / Classe / Marina" maxlength="48"></div>
|
||||
<button type="button" class="boat-name boat-selector" id="boat-selector" onclick="openFleetManager()" title="Gerenciar frota">
|
||||
<span id="boat-name-display">Shivao</span>
|
||||
<span class="boat-chevron" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div class="boat-meta">
|
||||
<span id="boat-model-display" class="boat-model-text">⛵ Veleiro</span>
|
||||
<button type="button" class="boat-edit-btn" onclick="openFleetManager()" title="Editar embarcação">✎</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -1513,19 +1624,38 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
|
|||
<!-- Anchor Setup Modal -->
|
||||
<div class="modal-backdrop" id="anchor-setup-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-head"><h3>Fundear o Shivao</h3><button class="icon-btn" onclick="closeModal('anchor-setup-modal')">✕</button></div>
|
||||
<div class="modal-head"><h3 id="anchor-modal-title">Fundear</h3><button class="icon-btn" onclick="closeModal('anchor-setup-modal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<div id="anchor-setup-status" style="font-family:var(--f-display);font-style:italic;font-size:14px;color:var(--sepia);margin-bottom:14px;text-align:center;padding:16px;background:var(--bg-canvas);border:1px dashed var(--rule)">Aguardando posição do GPS…</div>
|
||||
<div class="field"><label class="field-label">Posição registrada</label>
|
||||
<div id="anchor-coord" style="font-family:var(--f-mono);font-size:13px;color:var(--ink-deep);padding:10px 12px;background:var(--bg-canvas);border:1px solid var(--rule)">—</div>
|
||||
</div>
|
||||
|
||||
<!-- Anchor Calculator -->
|
||||
<div class="anchor-calc">
|
||||
<div class="anchor-calc-head">⚓ Calculadora de fundeio</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Profundidade <span id="anchor-depth-unit">(m)</span></label>
|
||||
<input type="number" id="anchor-depth" step="0.5" min="0" placeholder="0.0" oninput="recalcAnchor()">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Amarra lançada <span id="anchor-chain-unit">(m)</span></label>
|
||||
<input type="number" id="anchor-chain" step="1" min="0" placeholder="0" oninput="recalcAnchor()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Vento atual (nós) <span id="anchor-wind-source" style="font-weight:400;color:var(--sepia);text-transform:none;letter-spacing:0">manual</span></label>
|
||||
<input type="number" id="anchor-wind" step="1" min="0" placeholder="ex: 12" oninput="recalcAnchor()">
|
||||
</div>
|
||||
<div id="anchor-calc-result" class="anchor-calc-result"></div>
|
||||
<button type="button" class="btn btn-block btn-sm" onclick="applyRecommendedRadius()" id="anchor-apply-recommend" style="display:none;margin-top:8px">⚓ Aplicar raio sugerido</button>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">Raio de segurança</label>
|
||||
<label class="field-label">Raio de segurança (alarme)</label>
|
||||
<div class="anchor-radius-row">
|
||||
<input type="range" id="anchor-radius" min="15" max="200" step="5" value="50" oninput="updateRadiusLabel(this.value)">
|
||||
<span class="anchor-radius-val" id="anchor-radius-val">50 m</span>
|
||||
</div>
|
||||
<div class="field-hint">Distância máxima da âncora antes de disparar o alarme. Considere: linha de fundeio + maré + giro do barco.</div>
|
||||
<div class="field-hint">Distância máxima da âncora antes de disparar o alarme. Use a calculadora acima pra estimar.</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Contatos de emergência</label>
|
||||
<div id="anchor-contacts-summary" style="font-family:var(--f-mono);font-size:11px;color:var(--sepia);padding:8px 0">Nenhum contato configurado</div>
|
||||
|
|
@ -1715,11 +1845,255 @@ Hora: {HORA}</textarea>
|
|||
</div>
|
||||
|
||||
<button class="fab" id="fab" onclick="quickAdd()" title="Adicionar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
|
||||
<!-- Fleet Manager Modal -->
|
||||
<div class="modal-backdrop" id="fleet-modal" onclick="if(event.target===this)closeModal('fleet-modal')">
|
||||
<div class="modal" style="max-width:520px">
|
||||
<div class="modal-head">
|
||||
<h3>⚓ Minha Frota</h3>
|
||||
<button class="icon-btn" onclick="closeModal('fleet-modal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="fleet-list" class="fleet-list"></div>
|
||||
<div class="fleet-units-row">
|
||||
<span class="fleet-units-label">Unidades</span>
|
||||
<div class="fleet-units-toggle" id="fleet-units-toggle">
|
||||
<button type="button" data-unit="metric" onclick="setUnits('metric')">Metros</button>
|
||||
<button type="button" data-unit="imperial" onclick="setUnits('imperial')">Pés</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" onclick="closeModal('fleet-modal')">Fechar</button>
|
||||
<button class="btn btn-primary" onclick="openBoatEditor(null)">+ Nova embarcação</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boat Editor Modal -->
|
||||
<div class="modal-backdrop" id="boat-editor-modal" onclick="if(event.target===this)closeModal('boat-editor-modal')">
|
||||
<div class="modal" style="max-width:480px">
|
||||
<div class="modal-head">
|
||||
<h3 id="boat-editor-title">Nova embarcação</h3>
|
||||
<button class="icon-btn" onclick="closeModal('boat-editor-modal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="boat-editor-form" onsubmit="saveBoatFromForm(event)">
|
||||
<input type="hidden" id="boat-edit-id">
|
||||
<div class="field"><label class="field-label">Nome</label>
|
||||
<input type="text" id="boat-edit-name" required maxlength="40" placeholder="ex: Shivao, Maré Alta">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Tipo de embarcação</label>
|
||||
<select id="boat-edit-type">
|
||||
<option value="sailing">⛵ Veleiro</option>
|
||||
<option value="motor">🛥️ Lancha / Motor</option>
|
||||
<option value="catamaran">🚤 Catamarã</option>
|
||||
<option value="rib">🚣 Bote / RIB</option>
|
||||
<option value="other">🛳️ Outro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Modelo / Classe (opcional)</label>
|
||||
<input type="text" id="boat-edit-model" maxlength="48" placeholder="Beneteau 36, Delta 28...">
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Comprimento <span id="boat-len-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-length" step="0.1" min="0" placeholder="0.0">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Boca / Largura <span id="boat-beam-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-beam" step="0.1" min="0" placeholder="0.0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Calado <span id="boat-draft-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-draft" step="0.1" min="0" placeholder="profundidade casco">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Amarra a bordo <span id="boat-chain-unit">(m)</span></label>
|
||||
<input type="number" id="boat-edit-chain" step="1" min="0" placeholder="total disponível">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Ano (opcional)</label>
|
||||
<input type="number" id="boat-edit-year" min="1900" max="2100" placeholder="ex: 2018">
|
||||
</div>
|
||||
<div class="field-hint">⚓ Comprimento + calado são usados pra calcular raio de giro no fundeio.</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn btn-danger" id="boat-edit-delete" onclick="deleteBoatFromEditor()" style="display:none">Excluir</button>
|
||||
<button class="btn" onclick="closeModal('boat-editor-modal')">Cancelar</button>
|
||||
<button class="btn btn-primary" onclick="document.getElementById('boat-editor-form').requestSubmit()">Salvar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="zone-toast" id="zone-toast"></div>
|
||||
|
||||
<script>
|
||||
const state={boat:{name:'Shivao',model:''},trips:[],maint:[],pending:[],passengers:[],contacts:[],alarmTemplate:'🚨 ALERTA: Shivao saiu da posição de fundeio.\nEstou em {LAT}, {LNG}\nMapa: https://maps.google.com/?q={LAT},{LNG}\nHora: {HORA}',cloud:{url:'',token:'',lastSync:0},auth:null,license:null,webhooks:{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}},anchorHistory:[],zones:[],checklists:[],weatherCfg:{windyKey:'',model:'gfs'}};
|
||||
const state={boat:{name:'Shivao',model:''},boats:[],activeBoatId:null,units:'metric',trips:[],maint:[],pending:[],passengers:[],contacts:[],alarmTemplate:'🚨 ALERTA: Shivao saiu da posição de fundeio.\nEstou em {LAT}, {LNG}\nMapa: https://maps.google.com/?q={LAT},{LNG}\nHora: {HORA}',cloud:{url:'',token:'',lastSync:0},auth:null,license:null,webhooks:{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}},anchorHistory:[],zones:[],checklists:[],weatherCfg:{windyKey:'',model:'gfs'}};
|
||||
|
||||
// ============ NAUTICAL HELPERS (multi-boat + fleet + calculations) ============
|
||||
// Storage interno SEMPRE em metros (ISO). Conversão só pra display.
|
||||
const FT_PER_M=3.28084;
|
||||
const M_PER_FT=0.3048;
|
||||
|
||||
function isImperial(){return state.units==='imperial'}
|
||||
function lengthUnit(){return isImperial()?'ft':'m'}
|
||||
|
||||
// Converte storage (sempre metros) → display
|
||||
function lenToDisplay(meters){if(meters==null||isNaN(meters))return null;return isImperial()?meters*FT_PER_M:meters}
|
||||
// Converte display (que usuário digitou) → storage (metros)
|
||||
function lenFromInput(value){if(value==null||value==='')return null;const n=parseFloat(value);if(isNaN(n))return null;return isImperial()?n*M_PER_FT:n}
|
||||
function fmtLen(meters,decimals=1){if(meters==null||isNaN(meters))return '—';return lenToDisplay(meters).toFixed(decimals)+' '+lengthUnit()}
|
||||
|
||||
// Tipos de embarcação com defaults sensatos
|
||||
const BOAT_TYPES={
|
||||
sailing:{label:'Veleiro',icon:'⛵',defaultBeamRatio:0.30,defaultDraftRatio:0.18,scopeBonus:0},
|
||||
motor:{label:'Motor',icon:'🛥️',defaultBeamRatio:0.32,defaultDraftRatio:0.10,scopeBonus:-0.5},
|
||||
catamaran:{label:'Catamarã',icon:'🚤',defaultBeamRatio:0.55,defaultDraftRatio:0.08,scopeBonus:-0.5},
|
||||
rib:{label:'Bote/RIB',icon:'🚣',defaultBeamRatio:0.35,defaultDraftRatio:0.06,scopeBonus:-1},
|
||||
other:{label:'Outro',icon:'🛳️',defaultBeamRatio:0.28,defaultDraftRatio:0.15,scopeBonus:0},
|
||||
};
|
||||
|
||||
// Migration: state.boat (antigo) → state.boats[0] (novo)
|
||||
function migrateBoatsSchema(){
|
||||
if(!state.boats||!Array.isArray(state.boats))state.boats=[];
|
||||
if(state.boats.length===0&&state.boat&&state.boat.name){
|
||||
const b={
|
||||
id:uid(),
|
||||
name:state.boat.name||'Shivao',
|
||||
model:state.boat.model||'',
|
||||
type:'sailing',
|
||||
length:null,beam:null,draft:null,
|
||||
chainTotal:null,
|
||||
year:null,
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
state.activeBoatId=b.id;
|
||||
}
|
||||
if(!state.activeBoatId&&state.boats[0])state.activeBoatId=state.boats[0].id;
|
||||
if(!state.units)state.units='metric';
|
||||
}
|
||||
|
||||
function activeBoat(){
|
||||
return state.boats.find(b=>b.id===state.activeBoatId)||state.boats[0]||null;
|
||||
}
|
||||
|
||||
function setActiveBoat(id){
|
||||
state.activeBoatId=id;
|
||||
saveState();
|
||||
bindHeader();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function addBoat(data){
|
||||
const b={
|
||||
id:uid(),
|
||||
name:data.name||'Embarcação',
|
||||
model:data.model||'',
|
||||
type:data.type||'sailing',
|
||||
length:lenFromInput(data.length),
|
||||
beam:lenFromInput(data.beam),
|
||||
draft:lenFromInput(data.draft),
|
||||
chainTotal:lenFromInput(data.chainTotal),
|
||||
year:data.year?parseInt(data.year):null,
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
if(!state.activeBoatId)state.activeBoatId=b.id;
|
||||
saveState();
|
||||
return b;
|
||||
}
|
||||
|
||||
function updateBoat(id,data){
|
||||
const b=state.boats.find(x=>x.id===id);
|
||||
if(!b)return;
|
||||
if('name' in data)b.name=data.name;
|
||||
if('model' in data)b.model=data.model;
|
||||
if('type' in data)b.type=data.type;
|
||||
if('length' in data)b.length=lenFromInput(data.length);
|
||||
if('beam' in data)b.beam=lenFromInput(data.beam);
|
||||
if('draft' in data)b.draft=lenFromInput(data.draft);
|
||||
if('chainTotal' in data)b.chainTotal=lenFromInput(data.chainTotal);
|
||||
if('year' in data)b.year=data.year?parseInt(data.year):null;
|
||||
saveState();
|
||||
}
|
||||
|
||||
function removeBoat(id){
|
||||
if(state.boats.length<=1){toast('Mantenha pelo menos 1 embarcação');return}
|
||||
state.boats=state.boats.filter(b=>b.id!==id);
|
||||
if(state.activeBoatId===id)state.activeBoatId=state.boats[0]?.id||null;
|
||||
saveState();
|
||||
}
|
||||
|
||||
// ============ ANCHOR CALCULATIONS (regras náuticas) ============
|
||||
// Scope ratio = comprimento_amarra / profundidade
|
||||
// Recomendado: 5:1 normal, 7:1 vento moderado (15-25kn), 10:1 tempestade (>25kn)
|
||||
function scopeRatio(chainDeployedM,depthM){
|
||||
if(!depthM||depthM<=0)return null;
|
||||
return chainDeployedM/depthM;
|
||||
}
|
||||
|
||||
// Raio efetivo de giro = √(amarra² − profundidade²) + comprimento_do_barco
|
||||
// Esse é o círculo dentro do qual a popa do barco pode oscilar com a corrente/vento
|
||||
function calcSwingRadius(chainDeployedM,depthM,boatLengthM){
|
||||
if(!chainDeployedM||!depthM||chainDeployedM<=depthM)return null;
|
||||
const horizontal=Math.sqrt(chainDeployedM*chainDeployedM-depthM*depthM);
|
||||
return horizontal+(boatLengthM||0);
|
||||
}
|
||||
|
||||
// Recomenda comprimento de amarra baseado em condição
|
||||
// windKn: vento em nós; depth: profundidade
|
||||
// Retorna {chainM, ratio, condition}
|
||||
function recommendChain(depthM,windKn,boatType){
|
||||
let baseRatio=5; // calmo
|
||||
let condition='Condição calma';
|
||||
if(windKn>=25){baseRatio=10;condition='Tempestade'}
|
||||
else if(windKn>=15){baseRatio=7;condition='Vento moderado'}
|
||||
else if(windKn>=8){baseRatio=6;condition='Vento leve'}
|
||||
// Ajuste por tipo de embarcação
|
||||
const t=BOAT_TYPES[boatType]||BOAT_TYPES.sailing;
|
||||
const ratio=Math.max(3,baseRatio+t.scopeBonus);
|
||||
return {chainM:depthM*ratio,ratio,condition};
|
||||
}
|
||||
|
||||
// Status visual da configuração atual de fundeio
|
||||
function anchorStatus(chainDeployedM,depthM,windKn){
|
||||
if(!chainDeployedM||!depthM)return {level:'unknown',msg:'Informe profundidade + amarra'};
|
||||
const r=scopeRatio(chainDeployedM,depthM);
|
||||
const minR=windKn>=25?10:windKn>=15?7:5;
|
||||
if(r>=minR)return {level:'ok',msg:`Adequado (${r.toFixed(1)}:1)`};
|
||||
if(r>=minR-1)return {level:'warn',msg:`Justo (${r.toFixed(1)}:1) — folgar mais ${((minR-r)*depthM).toFixed(0)}m`};
|
||||
return {level:'danger',msg:`INSUFICIENTE (${r.toFixed(1)}:1) — necessário mínimo ${minR}:1`};
|
||||
}
|
||||
|
||||
// Recupera velocidade de vento atual (em nós) do cache de weather, qualquer provider
|
||||
function currentWindKnots(){
|
||||
try{
|
||||
if(!weather||!weather.data)return null;
|
||||
const d=weather.data;
|
||||
if(d.provider==='openmeteo'){
|
||||
return d.forecast?.current?.wind_speed_10m??null;
|
||||
}
|
||||
if(d.provider==='windy'){
|
||||
const m=d.main;
|
||||
const u=m?.['wind_u-surface']?.[0];
|
||||
const v=m?.['wind_v-surface']?.[0];
|
||||
if(u==null||v==null)return null;
|
||||
return Math.sqrt(u*u+v*v)*1.94384;
|
||||
}
|
||||
}catch(e){}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dica geral baseada em vento + condição
|
||||
function anchorAdvice(windKn,boatType){
|
||||
const t=BOAT_TYPES[boatType]||BOAT_TYPES.sailing;
|
||||
if(windKn>=30)return `🌊 Tempestade · scope mínimo 10:1, considere segunda âncora`;
|
||||
if(windKn>=20)return `🌬️ Vento forte · scope ${t.scopeBonus<0?'7':'8'}:1 + reforçar amarras de bordo`;
|
||||
if(windKn>=12)return `💨 Vento moderado · scope 7:1 ${t.label==='Catamarã'?'(catamarã: vigiar deriva lateral)':''}`;
|
||||
if(windKn>=5)return `🍃 Vento leve · scope 5-6:1 padrão`;
|
||||
return `⚓ Calmo · scope mínimo 5:1`;
|
||||
}
|
||||
const STORAGE_KEY='diario_bordo_v3';
|
||||
const TRACKING_KEY='diario_tracking_v3';
|
||||
const ANCHOR_KEY='diario_anchor_v3';
|
||||
|
|
@ -1736,7 +2110,8 @@ function dbPut(item){return new Promise((r,j)=>{const q=db.transaction('media','
|
|||
function dbDelete(id){return new Promise((r,j)=>{const q=db.transaction('media','readwrite').objectStore('media').delete(id);q.onsuccess=()=>r();q.onerror=()=>j(q.error)})}
|
||||
function dbAll(){return new Promise((r,j)=>{const q=db.transaction('media','readonly').objectStore('media').getAll();q.onsuccess=()=>r(q.result);q.onerror=()=>j(q.error)})}
|
||||
|
||||
function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'}}}catch(e){console.warn(e)}
|
||||
function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'};state.boats=d.boats||[];state.activeBoatId=d.activeBoatId||null;state.units=d.units||'metric'}}catch(e){console.warn(e)}
|
||||
migrateBoatsSchema();
|
||||
// popular checklists padrão se vazio
|
||||
if(!state.checklists.length){
|
||||
state.checklists=[
|
||||
|
|
@ -1865,7 +2240,142 @@ function renderGPSBanner(){
|
|||
}
|
||||
}
|
||||
|
||||
function bindHeader(){const n=document.getElementById('boat-name'),m=document.getElementById('boat-model');n.value=state.boat.name||'Shivao';m.value=state.boat.model||'';n.addEventListener('input',e=>{state.boat.name=e.target.value;saveState()});m.addEventListener('input',e=>{state.boat.model=e.target.value;saveState()})}
|
||||
function bindHeader(){
|
||||
const nameEl=document.getElementById('boat-name-display');
|
||||
const metaEl=document.getElementById('boat-model-display');
|
||||
if(!nameEl||!metaEl)return;
|
||||
const b=activeBoat();
|
||||
if(!b){nameEl.textContent='Sem embarcação';metaEl.textContent='Toque para adicionar';return}
|
||||
nameEl.textContent=b.name||'Embarcação';
|
||||
const t=BOAT_TYPES[b.type]||BOAT_TYPES.sailing;
|
||||
const parts=[`${t.icon} ${t.label}`];
|
||||
if(b.model)parts.push(b.model);
|
||||
if(b.length)parts.push(fmtLen(b.length,1));
|
||||
metaEl.textContent=parts.join(' · ');
|
||||
// sync legacy state.boat para compat com qualquer código antigo que ainda use
|
||||
state.boat={name:b.name,model:b.model};
|
||||
}
|
||||
|
||||
// ============ FLEET MANAGER ============
|
||||
function openFleetManager(){
|
||||
renderFleetList();
|
||||
syncUnitsToggle();
|
||||
openModal('fleet-modal');
|
||||
}
|
||||
|
||||
function renderFleetList(){
|
||||
const el=document.getElementById('fleet-list');
|
||||
if(!el)return;
|
||||
if(!state.boats||state.boats.length===0){
|
||||
el.innerHTML=`<div class="fleet-empty">Nenhuma embarcação ainda.<br>Toque "+ Nova" pra começar.</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML=state.boats.map(b=>{
|
||||
const t=BOAT_TYPES[b.type]||BOAT_TYPES.sailing;
|
||||
const isActive=b.id===state.activeBoatId;
|
||||
const meta=[t.label];
|
||||
if(b.model)meta.push(b.model);
|
||||
if(b.length)meta.push(fmtLen(b.length,1));
|
||||
return `<div class="fleet-item ${isActive?'active':''}" onclick="setActiveBoatAndClose('${b.id}')">
|
||||
<div class="fleet-icon">${t.icon}</div>
|
||||
<div class="fleet-info">
|
||||
<div class="fleet-name">${escapeHtml(b.name)}</div>
|
||||
<div class="fleet-meta">${meta.map(escapeHtml).join(' · ')}</div>
|
||||
</div>
|
||||
${isActive?'<div class="fleet-active-badge">ATIVA</div>':''}
|
||||
<button type="button" class="fleet-edit-icon" onclick="event.stopPropagation();openBoatEditor('${b.id}')" title="Editar">✎</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function setActiveBoatAndClose(id){
|
||||
setActiveBoat(id);
|
||||
renderFleetList();
|
||||
toast('Embarcação ativa: '+(activeBoat()?.name||''));
|
||||
}
|
||||
|
||||
function openBoatEditor(id){
|
||||
const isNew=!id;
|
||||
document.getElementById('boat-editor-title').textContent=isNew?'Nova embarcação':'Editar embarcação';
|
||||
document.getElementById('boat-edit-id').value=id||'';
|
||||
const b=isNew?{}:state.boats.find(x=>x.id===id)||{};
|
||||
document.getElementById('boat-edit-name').value=b.name||'';
|
||||
document.getElementById('boat-edit-type').value=b.type||'sailing';
|
||||
document.getElementById('boat-edit-model').value=b.model||'';
|
||||
// Mostrar valores convertidos pra unidade atual
|
||||
const setLen=(elId,m)=>{const el=document.getElementById(elId);el.value=m==null?'':lenToDisplay(m).toFixed(1)};
|
||||
setLen('boat-edit-length',b.length);
|
||||
setLen('boat-edit-beam',b.beam);
|
||||
setLen('boat-edit-draft',b.draft);
|
||||
// Amarra usa metros inteiros normalmente
|
||||
const chainEl=document.getElementById('boat-edit-chain');
|
||||
chainEl.value=b.chainTotal==null?'':lenToDisplay(b.chainTotal).toFixed(0);
|
||||
document.getElementById('boat-edit-year').value=b.year||'';
|
||||
// Atualizar labels de unidade
|
||||
const unitLabel=`(${lengthUnit()})`;
|
||||
['boat-len-unit','boat-beam-unit','boat-draft-unit','boat-chain-unit'].forEach(id=>{
|
||||
const el=document.getElementById(id);if(el)el.textContent=unitLabel;
|
||||
});
|
||||
// Botão excluir só aparece em edição e se houver mais de 1 barco
|
||||
document.getElementById('boat-edit-delete').style.display=(!isNew&&state.boats.length>1)?'inline-flex':'none';
|
||||
closeModal('fleet-modal');
|
||||
openModal('boat-editor-modal');
|
||||
setTimeout(()=>document.getElementById('boat-edit-name').focus(),100);
|
||||
}
|
||||
|
||||
function saveBoatFromForm(ev){
|
||||
ev.preventDefault();
|
||||
const id=document.getElementById('boat-edit-id').value;
|
||||
const data={
|
||||
name:document.getElementById('boat-edit-name').value.trim(),
|
||||
type:document.getElementById('boat-edit-type').value,
|
||||
model:document.getElementById('boat-edit-model').value.trim(),
|
||||
length:document.getElementById('boat-edit-length').value,
|
||||
beam:document.getElementById('boat-edit-beam').value,
|
||||
draft:document.getElementById('boat-edit-draft').value,
|
||||
chainTotal:document.getElementById('boat-edit-chain').value,
|
||||
year:document.getElementById('boat-edit-year').value,
|
||||
};
|
||||
if(!data.name){toast('Informe o nome da embarcação');return}
|
||||
if(id){updateBoat(id,data);toast('Embarcação atualizada')}
|
||||
else{const b=addBoat(data);state.activeBoatId=b.id;saveState();toast('Embarcação adicionada')}
|
||||
closeModal('boat-editor-modal');
|
||||
bindHeader();
|
||||
renderAll();
|
||||
openFleetManager();
|
||||
}
|
||||
|
||||
function deleteBoatFromEditor(){
|
||||
const id=document.getElementById('boat-edit-id').value;
|
||||
if(!id)return;
|
||||
const b=state.boats.find(x=>x.id===id);
|
||||
if(!b)return;
|
||||
if(!confirm(`Excluir "${b.name}"? Esta ação não pode ser desfeita.`))return;
|
||||
removeBoat(id);
|
||||
closeModal('boat-editor-modal');
|
||||
bindHeader();
|
||||
renderAll();
|
||||
openFleetManager();
|
||||
toast('Embarcação removida');
|
||||
}
|
||||
|
||||
function setUnits(u){
|
||||
if(u!=='metric'&&u!=='imperial')return;
|
||||
state.units=u;
|
||||
saveState();
|
||||
syncUnitsToggle();
|
||||
bindHeader();
|
||||
renderAll();
|
||||
toast(u==='metric'?'Unidade: metros':'Unidade: pés');
|
||||
}
|
||||
|
||||
function syncUnitsToggle(){
|
||||
const wrap=document.getElementById('fleet-units-toggle');
|
||||
if(!wrap)return;
|
||||
wrap.querySelectorAll('button').forEach(b=>{
|
||||
b.classList.toggle('active',b.dataset.unit===state.units);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(t=>{t.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));t.classList.add('active');document.getElementById('panel-'+t.dataset.panel).classList.add('active');document.getElementById('fab').style.display=['trips','maintenance','pending','zones'].includes(t.dataset.panel)?'flex':'none';if(t.dataset.panel==='export'){updateStorageInfo();bindCloudInputs();renderCloudStatus();renderShareList();bindWeatherInputs();renderAuthBox()}if(t.dataset.panel==='zones')renderZones();window.scrollTo(0,0)})});
|
||||
function quickAdd(){const a=document.querySelector('.tab.active').dataset.panel;if(a==='maintenance')openMaintModal();else if(a==='pending')openPendingModal();else if(a==='zones')openZoneEditor();else openTripModal()}
|
||||
|
|
@ -2279,6 +2789,7 @@ async function openAnchorSetup(){
|
|||
updateRadiusLabel(50);
|
||||
pendingAnchorPos=null;
|
||||
updateContactsSummary();
|
||||
initAnchorCalc();
|
||||
openModal('anchor-setup-modal');
|
||||
// tentar obter posição
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
|
|
@ -2293,6 +2804,105 @@ async function openAnchorSetup(){
|
|||
);
|
||||
}
|
||||
|
||||
// ============ ANCHOR CALCULATOR (modal) ============
|
||||
let anchorCalcLastRecommendedRadius=null;
|
||||
|
||||
function initAnchorCalc(){
|
||||
// Atualiza label de embarcação no título
|
||||
const b=activeBoat();
|
||||
const titleEl=document.getElementById('anchor-modal-title');
|
||||
if(titleEl)titleEl.textContent=b?`Fundear · ${b.name}`:'Fundear';
|
||||
// Atualiza unidades nos labels
|
||||
const u=`(${lengthUnit()})`;
|
||||
const dEl=document.getElementById('anchor-depth-unit');if(dEl)dEl.textContent=u;
|
||||
const cEl=document.getElementById('anchor-chain-unit');if(cEl)cEl.textContent=u;
|
||||
// Reset inputs
|
||||
document.getElementById('anchor-depth').value='';
|
||||
document.getElementById('anchor-chain').value='';
|
||||
// Pré-popular vento se Windy/OpenMeteo já fetchou
|
||||
const wKn=currentWindKnots();
|
||||
const wEl=document.getElementById('anchor-wind');
|
||||
const srcEl=document.getElementById('anchor-wind-source');
|
||||
if(wKn!=null){
|
||||
wEl.value=Math.round(wKn);
|
||||
if(srcEl)srcEl.textContent=`auto: ${weather.data?.provider==='windy'?'Windy':'Open-Meteo'}`;
|
||||
}else{
|
||||
wEl.value='';
|
||||
if(srcEl)srcEl.textContent='manual (sem GPS na meteo ainda)';
|
||||
}
|
||||
document.getElementById('anchor-apply-recommend').style.display='none';
|
||||
document.getElementById('anchor-calc-result').innerHTML='';
|
||||
recalcAnchor();
|
||||
}
|
||||
|
||||
function recalcAnchor(){
|
||||
const depthDisplay=parseFloat(document.getElementById('anchor-depth').value);
|
||||
const chainDisplay=parseFloat(document.getElementById('anchor-chain').value);
|
||||
const windKn=parseFloat(document.getElementById('anchor-wind').value)||0;
|
||||
const depth=isNaN(depthDisplay)?null:lenFromInput(depthDisplay);
|
||||
const chain=isNaN(chainDisplay)?null:lenFromInput(chainDisplay);
|
||||
const b=activeBoat();
|
||||
const boatLength=b?.length||0;
|
||||
const boatType=b?.type||'sailing';
|
||||
|
||||
const out=document.getElementById('anchor-calc-result');
|
||||
if(!depth){
|
||||
out.classList.add('full');
|
||||
out.innerHTML=`<div class="anchor-calc-advice">${anchorAdvice(windKn,boatType)}<br><small style="color:var(--sepia)">Informe a profundidade pra calcular.</small></div>`;
|
||||
document.getElementById('anchor-apply-recommend').style.display='none';
|
||||
return;
|
||||
}
|
||||
out.classList.remove('full');
|
||||
|
||||
const rec=recommendChain(depth,windKn,boatType);
|
||||
const status=anchorStatus(chain,depth,windKn);
|
||||
const swing=calcSwingRadius(chain,depth,boatLength);
|
||||
|
||||
// Sugestão de raio: swing radius + buffer GPS (15m) ou se não tiver chain, baseado em recomendação
|
||||
const recommendedRadiusM=swing
|
||||
? Math.ceil(swing+15)
|
||||
: Math.ceil(Math.sqrt(Math.max(0,rec.chainM*rec.chainM-depth*depth))+boatLength+15);
|
||||
anchorCalcLastRecommendedRadius=recommendedRadiusM;
|
||||
|
||||
const advice=anchorAdvice(windKn,boatType);
|
||||
const adviceClass=status.level==='danger'?'danger':status.level==='warn'?'warn':status.level==='ok'?'ok':'';
|
||||
|
||||
out.innerHTML=`
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Amarra recomendada</div>
|
||||
<div class="anchor-calc-stat-value">${fmtLen(rec.chainM,0)}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">${rec.ratio.toFixed(1)}:1 · ${rec.condition}</div>
|
||||
</div>
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Status atual</div>
|
||||
<div class="anchor-calc-stat-value ${status.level}">${status.level==='ok'?'✓ OK':status.level==='warn'?'⚠ JUSTO':status.level==='danger'?'✗ INSUF.':'—'}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">${escapeHtml(status.msg)}</div>
|
||||
</div>
|
||||
${swing?`
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Raio de giro</div>
|
||||
<div class="anchor-calc-stat-value">${fmtLen(swing,0)}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">amarra→popa + barco</div>
|
||||
</div>
|
||||
<div class="anchor-calc-stat">
|
||||
<div class="anchor-calc-stat-label">Raio sugerido (alarme)</div>
|
||||
<div class="anchor-calc-stat-value">${fmtLen(recommendedRadiusM,0)}</div>
|
||||
<div style="font-family:var(--f-mono);font-size:9.5px;color:var(--sepia);margin-top:3px">+15m buffer GPS</div>
|
||||
</div>`:''}
|
||||
<div class="anchor-calc-advice ${adviceClass}">${advice}${b?` · <strong>${BOAT_TYPES[boatType].icon} ${b.name}</strong> ${b.length?`(${fmtLen(b.length,1)})`:''}`:''}</div>
|
||||
`;
|
||||
document.getElementById('anchor-apply-recommend').style.display=swing?'inline-flex':'none';
|
||||
}
|
||||
|
||||
function applyRecommendedRadius(){
|
||||
if(!anchorCalcLastRecommendedRadius)return;
|
||||
// Slider só aceita 15-200m
|
||||
const v=Math.max(15,Math.min(200,anchorCalcLastRecommendedRadius));
|
||||
document.getElementById('anchor-radius').value=v;
|
||||
updateRadiusLabel(v);
|
||||
toast(`Raio de alarme ajustado para ${v}m`);
|
||||
}
|
||||
|
||||
function updateRadiusLabel(v){document.getElementById('anchor-radius-val').textContent=`${v} m`}
|
||||
function updateContactsSummary(){
|
||||
const el=document.getElementById('anchor-contacts-summary');
|
||||
|
|
|
|||
Loading…
Reference in a new issue