Sail Band

Bridge log · Captain

Контрольная карта Чеклист капитана дня

Тапни по строке — отметить. По кружку справа — назначить ответственного. Всё сохраняется на устройстве и работает оффлайн.

0/0
0%
Saved · offline
Made by Vladyslav Zaiets
') + ')', 'gi'); title = title.replace(re, '$1'); text = text.replace(re, '$1'); } html += '
'; html += '
' + title + '
'; html += '
' + text + '
'; html += '
'; } html += ''; } list.innerHTML = html; } (function bindGlossarySearch() { const inp = document.getElementById('glossSearch'); const clr = document.getElementById('glossClear'); if (!inp) return; let timer = null; inp.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(() => renderGlossary(inp.value), 120); }); if (clr) clr.addEventListener('click', () => { inp.value = ''; renderGlossary(''); inp.focus(); }); // initial render renderGlossary(''); })(); /* === CORE MODES vs MODULE MODES === */ const CORE_MODES_ONLY = ['checklist', 'glossary']; const APP_MODULES_SRC = 'app-modules.js'; async function ensureAppModulesLoaded() { if (window.AppModulesLoaded) return; if (window.AppModulesLoading) return window.AppModulesLoading; // Wait briefly so inline scripts (in offline bundle) get a chance to execute if (document.readyState === 'loading') { await new Promise(r => document.addEventListener('DOMContentLoaded', r, { once: true })); } else { await new Promise(r => setTimeout(r, 0)); } if (window.AppModulesLoaded) return; // Inline script present (offline bundle parsed but maybe still pending)? const inline = document.querySelector('script[data-src="' + APP_MODULES_SRC + '"]'); if (inline) { // give it a couple of ticks for (let i = 0; i < 20 && !window.AppModulesLoaded; i++) { await new Promise(r => setTimeout(r, 16)); } if (window.AppModulesLoaded) return; // fallback: eval inline content try { // eslint-disable-next-line no-new-func new Function(inline.textContent)(); } catch(e) { console.error('Inline module eval failed:', e); } return; } // Online: fetch as external script window.AppModulesLoading = loadScript(APP_MODULES_SRC).catch(err => { window.AppModulesLoading = null; console.error('Failed to load app-modules.js:', err); }); return window.AppModulesLoading; } // Wrap setMode to lazy-load modules const _setModeBeforeModules = setMode; setMode = function(m) { _setModeBeforeModules(m); if (!CORE_MODES_ONLY.includes(m) && m !== 'lights' && m !== 'exam') { ensureAppModulesLoaded(); } }; // Prefetch modules.js a bit after first paint so subsequent tab switches are instant function _kickoffPrefetch() { if (window.AppModulesLoaded || window.AppModulesLoading) return; ensureAppModulesLoaded(); } if (typeof window !== 'undefined') { if ('requestIdleCallback' in window) { window.requestIdleCallback(_kickoffPrefetch, { timeout: 2500 }); } else { setTimeout(_kickoffPrefetch, 1500); } } // If startup mode requires modules, trigger load right after init if (!CORE_MODES_ONLY.includes(state.mode || 'checklist') && state.mode !== 'lights' && state.mode !== 'exam') { ensureAppModulesLoaded(); } // Cross-reference links: clicks on [data-go-mode] switch to that mode document.addEventListener('click', function(e) { const t = e.target.closest && e.target.closest('[data-go-mode]'); if (!t) return; e.preventDefault(); const m = t.dataset.goMode; if (m && typeof setMode === 'function') setMode(m); }); /* ======================================================================== THEME SWITCHER (Авто / Светлая / Тёмная / Морская) ======================================================================== */ const THEME_KEY = 'sailband.theme'; function getTheme() { try { return localStorage.getItem(THEME_KEY) || 'auto'; } catch(e) { return 'auto'; } } function setTheme(t) { try { localStorage.setItem(THEME_KEY, t); } catch(e) {} applyTheme(t); } function applyTheme(t) { const root = document.documentElement; if (t === 'auto') { root.removeAttribute('data-theme'); } else { root.setAttribute('data-theme', t); } } applyTheme(getTheme()); /* ======================================================================== GLOBAL SEARCH 🔍 ======================================================================== */ function ensureSearchMarkup() { if (document.getElementById('globalSearch')) return; const overlay = document.createElement('div'); overlay.className = 'global-search'; overlay.id = 'globalSearch'; overlay.innerHTML = ''; document.body.appendChild(overlay); overlay.addEventListener('click', e => { if (e.target === overlay) closeSearch(); }); document.getElementById('gsClose').addEventListener('click', closeSearch); const inp = document.getElementById('gsInput'); inp.addEventListener('input', () => runSearch(inp.value)); inp.addEventListener('keydown', e => { if (e.key === 'Escape') closeSearch(); }); } function openSearch() { ensureSearchMarkup(); document.getElementById('globalSearch').classList.add('is-open'); setTimeout(() => document.getElementById('gsInput').focus(), 30); runSearch(''); } function closeSearch() { const el = document.getElementById('globalSearch'); if (el) el.classList.remove('is-open'); } function runSearch(q) { q = (q || '').trim().toLowerCase(); const results = document.getElementById('gsResults'); const hint = document.getElementById('gsHint'); if (!q) { results.innerHTML = ''; hint.textContent = 'Ищем по терминам, пунктам чеклистов, разделам и модулям'; return; } const hits = []; // 1. Modes (top-level navigation) if (typeof MODE_META === 'object') { for (const id of Object.keys(MODE_META)) { const m = MODE_META[id]; const blob = (m.ttl + ' ' + m.lead).toLowerCase(); if (blob.includes(q)) { hits.push({ kind: 'Раздел', title: m.ttl, snippet: m.lead, mode: id }); } } } // 2. Glossary if (typeof GLOSSARY === 'object') { for (const key of Object.keys(GLOSSARY)) { const g = GLOSSARY[key]; const blob = (g.title + ' ' + (g.text||'') + ' ' + (g.forms||[]).join(' ')).toLowerCase(); if (blob.includes(q)) { hits.push({ kind: 'Термин', title: g.title, snippet: (g.text || '').slice(0, 140), mode: 'glossary', glossKey: key }); } } } // 3. Checklist items if (typeof SECTIONS !== 'undefined') { for (const sec of SECTIONS) { for (const g of sec.groups) { for (const it of g.items) { const blob = (it.t + ' ' + (it.how||'') + ' ' + (it.why||'')).toLowerCase(); if (blob.includes(q)) { const t = it.t.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_,k,l) => (l||k)); hits.push({ kind: sec.title, title: t.slice(0, 80) + (t.length > 80 ? '…' : ''), snippet: g.title, mode: 'checklist', secId: sec.id }); } } } } } // Cap and sort const top = hits.slice(0, 50); hint.textContent = top.length + ' результатов' + (hits.length > top.length ? ' (показаны первые 50)' : ''); if (top.length === 0) { results.innerHTML = '
Ничего не найдено. Попробуйте другое слово.
'; return; } results.innerHTML = top.map(h => { const highlight = (s) => { if (!s) return ''; const safe = escapeHtml(s); const reQ = q.replace(/[.*+?^${}()|[\]\\]/g, '\\'); return safe.replace(new RegExp('(' + reQ + ')', 'gi'), '$1'); }; return ''; }).join(''); results.querySelectorAll('.gs-row').forEach(row => { row.addEventListener('click', () => { const m = row.dataset.mode; const gk = row.dataset.gloss; const sec = row.dataset.sec; closeSearch(); if (m && typeof setMode === 'function') setMode(m); // jump-to for glossary if (m === 'glossary' && gk) { setTimeout(() => { const inp = document.getElementById('glossSearch'); if (inp) { inp.value = (GLOSSARY[gk] && GLOSSARY[gk].title) || gk; if (typeof renderGlossary === 'function') renderGlossary(inp.value); } }, 80); } // jump-to checklist section if (m === 'checklist' && sec) { setTimeout(() => { state.tab = sec; if (typeof save === 'function') save(); if (typeof renderTabs === 'function') renderTabs(); if (typeof renderLists === 'function') renderLists(); }, 80); } }); }); } // Wire keyboard shortcut + button if present document.addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); } else if (e.key === '/' && !['INPUT','TEXTAREA','SEARCH'].includes(document.activeElement.tagName)) { e.preventDefault(); openSearch(); } }); (function bindSearchBtn() { const b = document.getElementById('searchBtn'); if (b) b.addEventListener('click', openSearch); })(); /* ======================================================================== GROUP NAV — категории в верхней нав. панели ======================================================================== */ const MODE_GROUPS = [ { label: 'Журнал', modes: ['checklist'] }, { label: 'Яхта', modes: ['rig','yacht','engine','knots','anchor','commands'] }, { label: 'Навигация', modes: ['courses','nav','tides','passage','weather','buoyage'] }, { label: 'Безопасность', modes: ['colregs','lights','sounds','flags','radio','distress','safety'] }, { label: 'Шкипер', modes: ['skipper','firstaid'] }, { label: 'Учёба', modes: ['exam','glossary'] }, ]; function regroupModeNav() { const navRoot = document.getElementById('modes'); if (!navRoot) return; // Collect existing buttons const buttons = {}; navRoot.querySelectorAll('.mode').forEach(b => { buttons[b.dataset.mode] = b; }); // Build grouped wrap const wrap = document.createElement('div'); wrap.className = 'modes-grouped'; // Track which buttons we've placed const placed = new Set(); for (const grp of MODE_GROUPS) { const cat = document.createElement('div'); cat.className = 'mode-cat'; cat.innerHTML = '
' + escapeHtml(grp.label) + '
'; const inner = document.createElement('div'); inner.className = 'modes-grid'; for (const id of grp.modes) { if (buttons[id]) { inner.appendChild(buttons[id]); placed.add(id); } } cat.appendChild(inner); wrap.appendChild(cat); } // Any orphan buttons (not in any MODE_GROUPS) — append to last category as "Прочее" const orphans = Object.keys(buttons).filter(id => !placed.has(id)); if (orphans.length > 0) { const cat = document.createElement('div'); cat.className = 'mode-cat'; cat.innerHTML = '
Прочее
'; const inner = document.createElement('div'); inner.className = 'modes-grid'; for (const id of orphans) inner.appendChild(buttons[id]); cat.appendChild(inner); wrap.appendChild(cat); } // Replace nav root content (preserves the button elements & their listeners) navRoot.innerHTML = ''; while (wrap.firstChild) navRoot.appendChild(wrap.firstChild); navRoot.classList.add('is-grouped'); } regroupModeNav(); /* ======================================================================== ARROWS BETWEEN CHECKLIST SECTIONS ======================================================================== */ function addNextSectionArrows() { const lists = document.getElementById('lists'); if (!lists || typeof SECTIONS === 'undefined') return; // Add a "next →" element at the end of each rendered section lists.querySelectorAll('section.list').forEach(secEl => { if (secEl.querySelector('.next-section')) return; // already added const secId = secEl.dataset.id; const idx = SECTIONS.findIndex(s => s.id === secId); if (idx === -1) return; const isLast = idx === SECTIONS.length - 1; const prev = idx > 0 ? SECTIONS[idx-1] : null; const next = !isLast ? SECTIONS[idx+1] : null; const navHtml = '
' + (prev ? '' : '') + (next ? '' : '') + '
'; const div = document.createElement('div'); div.innerHTML = navHtml; div.firstChild.classList.add('next-section'); secEl.appendChild(div.firstChild); }); } // Re-run after every renderLists const _origRenderLists = renderLists; renderLists = function() { _origRenderLists(); addNextSectionArrows(); }; addNextSectionArrows(); // Handle clicks on [data-go-sec] document.addEventListener('click', e => { const t = e.target.closest && e.target.closest('[data-go-sec]'); if (!t) return; const sec = t.dataset.goSec; if (!sec) return; state.tab = sec; save(); if (typeof renderTabs === 'function') renderTabs(); if (typeof renderLists === 'function') renderLists(); window.scrollTo({ top: 0, behavior: 'smooth' }); }); /* ======================================================================== ONBOARDING — первое посещение ======================================================================== */ const ONBOARD_KEY = 'sailband.onboard.seen.v1'; function shouldShowOnboarding() { try { return !localStorage.getItem(ONBOARD_KEY); } catch(e) { return false; } } function markOnboardingSeen() { try { localStorage.setItem(ONBOARD_KEY, '1'); } catch(e) {} } function showOnboarding() { if (document.getElementById('onbOverlay')) return; const ov = document.createElement('div'); ov.id = 'onbOverlay'; ov.className = 'onb-overlay'; ov.innerHTML = '
' + '
' + '
📋
' + '

Чеклисты — основное

' + '

Тапни по строке — отметить выполнено. Тапни по кружку справа — назначить кто отвечает.

' + '
' + '' + '' + '' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(ov); const totalSteps = 4; let step = 1; const dots = document.getElementById('onbDots'); for (let i = 1; i <= totalSteps; i++) { const d = document.createElement('span'); d.className = 'onb-dot' + (i === step ? ' active' : ''); dots.appendChild(d); } function setStep(s) { step = s; ov.querySelectorAll('.onb-step').forEach(e => { e.hidden = parseInt(e.dataset.step, 10) !== s; }); dots.querySelectorAll('.onb-dot').forEach((d, i) => d.classList.toggle('active', i + 1 === s)); ov.querySelector('.onb-next').textContent = s === totalSteps ? 'Готово' : 'Дальше'; } ov.querySelector('.onb-skip').addEventListener('click', () => { markOnboardingSeen(); ov.remove(); }); ov.querySelector('.onb-next').addEventListener('click', () => { if (step < totalSteps) setStep(step + 1); else { markOnboardingSeen(); ov.remove(); } }); ov.addEventListener('click', e => { if (e.target === ov) { markOnboardingSeen(); ov.remove(); } }); } if (shouldShowOnboarding()) { setTimeout(showOnboarding, 300); } /* ======================================================================== CALCULATORS — 5 tools (CADET, Tides, Anchor scope, Hull Speed, GPS Watch) ======================================================================== */ // Register mode meta + group if (typeof MODE_META === 'object') { MODE_META.calc = { sub:'Инструменты', ttl:'Калькуляторы шкипера', lead:'Курсы, приливы, якорная цепь, скорость корпуса и якорная вахта по GPS.' }; MODE_META.log = { sub:'Журнал', ttl:'Судовой лог', lead:'Что сделано, когда и кто отвечал. Авто-заполняется из чеклистов.' }; } // Augment MODE_GROUPS if present (already declared in earlier section) try { const gIdx = (typeof MODE_GROUPS !== 'undefined') ? MODE_GROUPS.findIndex(g => g.label === 'Учёба') : -1; if (gIdx >= 0) { // insert "Инструменты" group BEFORE "Учёба" MODE_GROUPS.splice(gIdx, 0, { label: 'Инструменты', modes: ['calc', 'log'] }); // Re-run regroup if (typeof regroupModeNav === 'function') { // need to rebuild buttons; safer: just call regroup // But our current regroup ASSUMES grouped state already. We re-flatten first. const navRoot = document.getElementById('modes'); if (navRoot && navRoot.classList.contains('is-grouped')) { // Move all .mode children up out of categories const allModes = [...navRoot.querySelectorAll('.mode')]; navRoot.innerHTML = ''; allModes.forEach(b => navRoot.appendChild(b)); navRoot.classList.remove('is-grouped'); regroupModeNav(); } } } } catch(e) { console.warn('Group augment:', e); } // === CALC RENDER === function renderCalc() { const panel = document.getElementById('mode-calc'); if (!panel) return; if (panel.dataset.calcInit === 'true') return; panel.innerHTML = '
'; const grid = panel.querySelector('#toolsCalc'); function makeCard(opts) { const card = document.createElement('div'); card.className = 'tool-card' + (opts.full ? ' tool-card-full' : ''); const fieldsHtml = (opts.fields || []).map(f => { const hint = f.hint ? '' + f.hint + '' : ''; const inp = f.type === 'select' ? '' : ''; return '
' + f.label + '
' + inp + (hint ? '
' + f.hint + '
' : '') + '
'; }).join(''); const cols = (opts.fields || []).length; const colsClass = cols >= 3 ? 'cols-3' : cols === 2 ? 'cols-2' : ''; const fieldRows = (opts.fieldRows || [opts.fields]).map(row => { const html = (row || []).map(f => { const inp = f.type === 'select' ? '' : ''; return '
' + f.label + '
' + inp + (f.hint ? '
' + f.hint + '
' : '') + '
'; }).join(''); const c = (row || []).length; const cc = c >= 3 ? 'cols-3' : c === 2 ? 'cols-2' : ''; return '
' + html + '
'; }).join(''); const actionsHtml = opts.actions ? '
' + opts.actions.map(a => '').join('') + '
' : ''; card.innerHTML = '
' + '
' + (opts.icon || '🛠') + '
' + '

' + opts.title + '

' + opts.desc + '

' + '
' + fieldRows + actionsHtml + '
' + (opts.placeholder || 'Заполни поля выше.') + '
' + (opts.tip ? '
' + opts.tip + '
' : ''); grid.appendChild(card); return card; } function setOut(outEl, html, isPlaceholder) { outEl.innerHTML = html; outEl.classList.toggle('placeholder', !!isPlaceholder); } // ---- 1. CADET ---- const cadet = makeCard({ icon: '🧭', title: 'Перевод курсов (CADET)', desc: 'Истинный ↔ Магнитный ↔ Компасный с учётом склонения и девиации.', fieldRows: [ [ { id:'cadetT', label:'Истинный курс', placeholder:'045', step:1 }, { id:'cadetM', label:'Магнитный', placeholder:'—', step:1 }, { id:'cadetC', label:'Компасный', placeholder:'—', step:1 }, ], [ { id:'cadetV', label:'Variation °', placeholder:'−4', step:0.5, hint:'минус = W, плюс = E' }, { id:'cadetD', label:'Deviation °', placeholder:'+2', step:0.5, hint:'минус = W, плюс = E' }, ], ], outId: 'cadetOut', placeholder: 'Введи V, D и любой из курсов — остальные посчитаются.', tip: '💡 CADET: Compass + dev = Mag → + var = True. East плюс, West минус.', }); function recalcCadet(driver) { const $ = sel => cadet.querySelector(sel); const T = parseFloat($('#cadetT').value); const M = parseFloat($('#cadetM').value); const C = parseFloat($('#cadetC').value); const V = parseFloat($('#cadetV').value); const D = parseFloat($('#cadetD').value); const out = $('#cadetOut'); function n(x) { return ((x % 360) + 360) % 360; } if (isNaN(V) || isNaN(D)) { setOut(out, 'Введи V и D, чтобы посчитать.', true); return; } if (driver === 'T' && !isNaN(T)) { const Mc = n(T - V), Cc = n(Mc - D); $('#cadetM').value = Mc.toFixed(1); $('#cadetC').value = Cc.toFixed(1); setOut(out, 'True ' + T + '° → Mag ' + Mc.toFixed(1) + '° → Compass ' + Cc.toFixed(1) + '°'); } else if (driver === 'M' && !isNaN(M)) { const Tc = n(M + V), Cc = n(M - D); $('#cadetT').value = Tc.toFixed(1); $('#cadetC').value = Cc.toFixed(1); setOut(out, 'Mag ' + M + '° → True ' + Tc.toFixed(1) + '° · Compass ' + Cc.toFixed(1) + '°'); } else if (driver === 'C' && !isNaN(C)) { const Mc = n(C + D), Tc = n(Mc + V); $('#cadetM').value = Mc.toFixed(1); $('#cadetT').value = Tc.toFixed(1); setOut(out, 'Compass ' + C + '° → Mag ' + Mc.toFixed(1) + '° → True ' + Tc.toFixed(1) + '°'); } } cadet.querySelector('#cadetT').addEventListener('input', () => recalcCadet('T')); cadet.querySelector('#cadetM').addEventListener('input', () => recalcCadet('M')); cadet.querySelector('#cadetC').addEventListener('input', () => recalcCadet('C')); cadet.querySelector('#cadetV').addEventListener('input', () => recalcCadet('T')); cadet.querySelector('#cadetD').addEventListener('input', () => recalcCadet('T')); // ---- 2. Tides ---- const tide = makeCard({ icon: '🌊', title: 'Приливы — правило 12-х', desc: 'Высота воды сейчас между HW и LW. Подъём по часам 1·2·3·3·2·1.', fieldRows: [ [ { id:'tHWh', label:'Высота HW (м)', step:0.1, placeholder:'4.8' }, { id:'tLWh', label:'Высота LW (м)', step:0.1, placeholder:'0.6' }, ], [ { id:'tHWt', label:'Время HW', type:'time' }, { id:'tLWt', label:'Время LW', type:'time' }, ], [ { id:'tNow', label:'Сейчас', type:'time' }, ], ], actions: [{ id:'tNowBtn', label:'⏱ Сейчас', cls:'subtle' }], outId: 'tideOut', placeholder: 'Введи HW/LW, времена и текущее время.', tip: '💡 Минимальная глубина = карта + LW. Карта 2.0м, LW 0.6м → реально 2.6м.', }); function recalcTide() { const $ = s => tide.querySelector(s); const HWh = parseFloat($('#tHWh').value), LWh = parseFloat($('#tLWh').value); const HWt = $('#tHWt').value, LWt = $('#tLWt').value, now = $('#tNow').value; const out = $('#tideOut'); if (isNaN(HWh) || isNaN(LWh) || !HWt || !LWt || !now) { setOut(out, 'Введи HW/LW, времена и текущее время.', true); return; } function toMin(s) { const [h,m] = s.split(':').map(Number); return h*60+m; } const tHW = toMin(HWt), tLW = toMin(LWt), tN = toMin(now); const range = HWh - LWh; const rising = tHW > tLW; const start = rising ? tLW : tHW; const fr = [0,1,3,6,9,11,12]; let pos = ((tN - start + 1440) % 1440) / 60; if (pos > 6) pos = 6; if (pos < 0) pos = 0; const lo = Math.floor(pos), hi = Math.min(6, lo+1); const f = fr[lo] + (fr[hi] - fr[lo]) * (pos - lo); const heightFromStart = (f/12) * range; const startH = rising ? LWh : HWh; const cur = startH + (rising ? +1 : -1) * heightFromStart; setOut(out, 'Прошло ' + pos.toFixed(1) + ' ч от ' + (rising ? 'LW' : 'HW') + '. Высота воды: ' + cur.toFixed(2) + ' м (' + (rising ? 'прилив' : 'отлив') + ').'); } tide.querySelectorAll('input').forEach(i => i.addEventListener('input', recalcTide)); tide.querySelector('#tNowBtn').addEventListener('click', () => { const d = new Date(); tide.querySelector('#tNow').value = String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0'); recalcTide(); }); // ---- 3. Anchor scope ---- const anch = makeCard({ icon: '⚓', title: 'Якорная цепь — scope', desc: 'Сколько цепи травить с учётом прилива и клюза. Норма 5:1, шторм 7-10:1.', fieldRows: [ [ { id:'aDep', label:'Глубина (м)', step:0.5, placeholder:'5' }, { id:'aTide', label:'Подъём (м)', step:0.5, placeholder:'2', hint:'макс. прилив за стоянку' }, { id:'aFb', label:'Клюз (м)', step:0.1, placeholder:'1' }, ], [ { id:'aScope', label:'Scope ratio', type:'select', options:[ { v:3, l:'3:1 — кратко, тихо' }, { v:5, l:'5:1 — норма', selected:true }, { v:7, l:'7:1 — свежий ветер' }, { v:10, l:'10:1 — шторм' }, ]}, ], ], outId: 'anchOut', placeholder: 'Введи глубину.', tip: '💡 На верёвке (rope) добавь +1 к ratio. Меньше 3:1 — якорь не возьмёт.', }); function recalcAnch() { const $ = s => anch.querySelector(s); const d = parseFloat($('#aDep').value); const t = parseFloat($('#aTide').value) || 0; const f = parseFloat($('#aFb').value) || 0; const r = parseFloat($('#aScope').value); const out = $('#anchOut'); if (isNaN(d) || isNaN(r)) { setOut(out, 'Введи глубину и выбери scope.', true); return; } const tot = d + t + f; setOut(out, 'Глубина + прилив + клюз = ' + tot.toFixed(1) + 'м. При scope ' + r + ':1 травить ' + (tot*r).toFixed(1) + 'м цепи.'); } anch.querySelectorAll('input, select').forEach(i => i.addEventListener('input', recalcAnch)); // ---- 4. Hull Speed ---- const hull = makeCard({ icon: '🚤', title: 'Hull Speed', desc: 'Теоретическая макс. скорость водоизмещающего корпуса.', fields: [ { id:'hLwl', label:'LWL — длина по ватерлинии (м)', step:0.1, placeholder:'9.5' }, ], outId: 'hullOut', placeholder: 'Введи длину по ватерлинии.', tip: '💡 После hull speed лодка тратит силы на свою же волну. Глиссировать не может.', }); hull.querySelector('#hLwl').addEventListener('input', () => { const out = hull.querySelector('#hullOut'); const v = parseFloat(hull.querySelector('#hLwl').value); if (isNaN(v)) { setOut(out, 'Введи длину по ватерлинии.', true); return; } const ft = v * 3.28084; const kn = 1.34 * Math.sqrt(ft); setOut(out, 'LWL = ' + v.toFixed(1) + 'м (' + ft.toFixed(1) + ' ft). Hull speed: ' + kn.toFixed(2) + ' уз (' + (kn*1.852).toFixed(1) + ' км/ч).'); }); // ---- 5. Anchor watch (GPS) ---- const watch = makeCard({ icon: '📡', title: 'Якорная вахта (GPS)', desc: 'Запоминает позицию якоря и сигналит если снесло.', fields: [ { id:'wRadius', label:'Радиус допуска (м)', step:5, value:50 }, ], actions: [ { id:'wStart', label:'🎯 Зафиксировать', cls:'primary' }, { id:'wStop', label:'⏹ Стоп', cls:'danger', disabled: true }, ], outId: 'watchOut', placeholder: 'Нажми «Зафиксировать», когда якорь сел.', tip: '⚠️ GPS точность ±5–15м. Меньше 30м обычно бесполезно. Не закрывай вкладку.', }); let watchAnchor = null, watchId = null, _watchAlarmTriggered = false; function dist(a, b) { const R = 6371000, toRad = x => x * Math.PI / 180; const dLat = toRad(b.lat - a.lat), dLon = toRad(b.lon - a.lon); const aV = Math.sin(dLat/2)**2 + Math.cos(toRad(a.lat))*Math.cos(toRad(b.lat))*Math.sin(dLon/2)**2; return 2 * R * Math.asin(Math.sqrt(aV)); } function beepAlarm() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(), gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 880; gain.gain.value = 0.3; osc.start(); setTimeout(() => { osc.stop(); ctx.close(); }, 1500); } catch(e) {} } const watchOut = watch.querySelector('#watchOut'); const wStart = watch.querySelector('#wStart'), wStop = watch.querySelector('#wStop'); wStart.addEventListener('click', () => { if (!navigator.geolocation) { setOut(watchOut, 'GPS не поддерживается.'); return; } setOut(watchOut, 'Получаю GPS-координаты…'); navigator.geolocation.getCurrentPosition(pos => { watchAnchor = { lat: pos.coords.latitude, lon: pos.coords.longitude }; wStart.disabled = true; wStop.disabled = false; setOut(watchOut, '✅ Зафиксировано: ' + watchAnchor.lat.toFixed(5) + ', ' + watchAnchor.lon.toFixed(5) + '. Жду движения…'); _watchAlarmTriggered = false; if (watchId !== null) navigator.geolocation.clearWatch(watchId); watchId = navigator.geolocation.watchPosition(p => { if (!watchAnchor) return; const cur = { lat: p.coords.latitude, lon: p.coords.longitude }; const d = dist(watchAnchor, cur); const r = parseFloat(watch.querySelector('#wRadius').value) || 50; if (d > r) { setOut(watchOut, '🚨 ДРЕЙФ! Снесло на ' + d.toFixed(0) + 'м (предел ' + r + 'м)'); if (!_watchAlarmTriggered) { _watchAlarmTriggered = true; beepAlarm(); if (navigator.vibrate) navigator.vibrate([400,200,400,200,400,200,400]); } } else { setOut(watchOut, '📍 Дистанция от якоря: ' + d.toFixed(1) + 'м / ' + r + 'м · точность ±' + (p.coords.accuracy||0).toFixed(0) + 'м'); } }, e => setOut(watchOut, '' + e.message + ''), { enableHighAccuracy: true, maximumAge: 5000 }); }, e => setOut(watchOut, 'GPS: ' + e.message + ''), { enableHighAccuracy: true, timeout: 15000 }); }); wStop.addEventListener('click', () => { if (watchId !== null) navigator.geolocation.clearWatch(watchId); watchId = null; watchAnchor = null; wStart.disabled = false; wStop.disabled = true; setOut(watchOut, 'Остановлено.', true); }); // ---- 6. VHF range ---- const vhf = makeCard({ icon: '📻', title: 'VHF дальность связи', desc: 'Радиогоризонт зависит от высоты антенн. D = 2.2 × (√h1 + √h2).', fields: [ { id:'vhfA1', label:'Своя антенна (м)', step:0.5, placeholder:'12' }, { id:'vhfA2', label:'Их антенна (м)', step:0.5, placeholder:'50' }, ], outId: 'vhfOut', placeholder: 'Введи высоты двух антенн.', tip: '💡 До маяка ~25 миль · до другой яхты 7-10 миль · до корабля 25-30 миль.', }); function recalcVhf() { const h1 = parseFloat(vhf.querySelector('#vhfA1').value); const h2 = parseFloat(vhf.querySelector('#vhfA2').value); const out = vhf.querySelector('#vhfOut'); if (isNaN(h1) || isNaN(h2)) { setOut(out, 'Введи высоты двух антенн.', true); return; } const nm = 2.2 * (Math.sqrt(h1) + Math.sqrt(h2)); setOut(out, 'Радиогоризонт: ' + nm.toFixed(1) + ' м.миль (' + (nm*1.852).toFixed(1) + ' км).'); } vhf.querySelectorAll('input').forEach(i => i.addEventListener('input', recalcVhf)); // ---- 7. SST ---- const sst = makeCard({ icon: '📐', title: 'Скорость / расстояние / время', desc: 'Введи любые 2 — третий посчитается. D = S × T.', fields: [ { id:'sstS', label:'Скорость (уз)', step:0.1, placeholder:'6' }, { id:'sstD', label:'Расстояние (миль)', step:0.1, placeholder:'—' }, { id:'sstT', label:'Время (часы)', step:0.1, placeholder:'—' }, ], outId: 'sstOut', placeholder: 'Введи 2 значения, оставь одно пустым.', tip: '💡 Один час под мотором 6 узлов = 6 миль. ETA по пропорции.', }); function recalcSst() { const $ = s => sst.querySelector(s); const S = parseFloat($('#sstS').value), D = parseFloat($('#sstD').value), T = parseFloat($('#sstT').value); const out = $('#sstOut'); const filled = [!isNaN(S), !isNaN(D), !isNaN(T)].filter(Boolean).length; if (filled < 2) { setOut(out, 'Введи любые 2 — третий посчитаю.', true); return; } if (filled === 3) { setOut(out, 'Очисти одно поле.', true); return; } if (isNaN(D)) { const v = S*T; $('#sstD').value = v.toFixed(2); setOut(out, 'D = S × T = ' + v.toFixed(2) + ' миль'); } else if (isNaN(T)) { const v = D/S; $('#sstT').value = v.toFixed(2); setOut(out, 'T = D/S = ' + v.toFixed(2) + ' ч (' + Math.floor(v) + 'ч ' + Math.round((v-Math.floor(v))*60) + 'мин)'); } else if (isNaN(S)) { const v = D/T; $('#sstS').value = v.toFixed(2); setOut(out, 'S = D/T = ' + v.toFixed(2) + ' уз'); } } sst.querySelectorAll('input').forEach(i => i.addEventListener('input', recalcSst)); // ---- 8. Fuel ---- const fuel = makeCard({ icon: '⛽', title: 'Дальность под мотором', desc: 'Сколько миль на полном баке с запасом 20%.', fields: [ { id:'fT', label:'Бак (л)', step:1, placeholder:'120' }, { id:'fC', label:'Расход (л/ч)', step:0.5, placeholder:'3' }, { id:'fS', label:'Скорость (уз)', step:0.1, placeholder:'5' }, ], outId: 'fuelOut', placeholder: 'Введи бак, расход и скорость.', tip: '💡 Дизель парусной обычно 2-4 л/ч. Береги последние 20% — на швартовку.', }); function recalcFuel() { const $ = s => fuel.querySelector(s); const T = parseFloat($('#fT').value), C = parseFloat($('#fC').value), S = parseFloat($('#fS').value); const out = $('#fuelOut'); if (isNaN(T) || isNaN(C) || isNaN(S) || C <= 0) { setOut(out, 'Введи бак, расход и скорость.', true); return; } const h = T/C, m = h*S; setOut(out, 'Часов на баке: ' + h.toFixed(1) + '. Дальность: ' + m.toFixed(0) + ' миль. С запасом 20%: ' + (m*0.8).toFixed(0) + ' миль.'); } fuel.querySelectorAll('input').forEach(i => i.addEventListener('input', recalcFuel)); // ---- 9. Sun ---- const sun = makeCard({ icon: '🌅', title: 'Восход / Закат', desc: 'Когда стемнеет. Civil twilight даёт ещё 30 минут.', fieldRows: [ [ { id:'sLat', label:'Широта (°)', step:0.1, placeholder:'43.5' }, { id:'sLon', label:'Долгота (°)', step:0.1, placeholder:'16.5' }, ], [ { id:'sDate', label:'Дата', type:'date' }, ], ], actions: [ { id:'sUseGps', label:'📍 Из GPS', cls:'subtle' }, { id:'sToday', label:'📅 Сегодня', cls:'subtle' }, ], outId: 'sunOut', placeholder: 'Введи координаты и дату.', tip: '💡 Civil twilight — солнце −6°. После — ходовые огни обязательны.', }); function recalcSun() { const $ = s => sun.querySelector(s); const lat = parseFloat($('#sLat').value), lon = parseFloat($('#sLon').value); const date = $('#sDate').value; const out = $('#sunOut'); if (isNaN(lat) || isNaN(lon) || !date) { setOut(out, 'Введи координаты и дату.', true); return; } const d = new Date(date + 'T12:00:00Z'); const day = Math.floor((d - new Date(d.getFullYear(), 0, 0))/86400000); const gamma = 2 * Math.PI / 365 * (day - 1); const decl = 0.006918 - 0.399912*Math.cos(gamma) + 0.070257*Math.sin(gamma) - 0.006758*Math.cos(2*gamma) + 0.000907*Math.sin(2*gamma) - 0.002697*Math.cos(3*gamma) + 0.00148*Math.sin(3*gamma); const latRad = lat * Math.PI / 180; function solve(altDeg) { const altRad = altDeg * Math.PI / 180; const cosH = (Math.sin(altRad) - Math.sin(latRad)*Math.sin(decl)) / (Math.cos(latRad)*Math.cos(decl)); if (cosH > 1 || cosH < -1) return null; return Math.acos(cosH) * 180 / Math.PI / 15; } const hSet = solve(-0.833), hCiv = solve(-6); if (hSet == null) { setOut(out, 'Солнце не заходит/восходит в эти даты.', true); return; } const noonUTC = 12 - lon/15; function fmt(utcH) { const baseMs = d.getTime() - 12*3600*1000; const dt = new Date(baseMs + utcH * 3600 * 1000); return String(dt.getHours()).padStart(2,'0') + ':' + String(dt.getMinutes()).padStart(2,'0'); } setOut(out, '🌅 Восход ' + fmt(noonUTC - hSet) + ' · 🌇 Закат ' + fmt(noonUTC + hSet) + '
🌆 Civil dawn ' + fmt(noonUTC - hCiv) + ' · 🌃 Civil dusk ' + fmt(noonUTC + hCiv) + ''); } sun.querySelectorAll('input').forEach(i => i.addEventListener('input', recalcSun)); sun.querySelector('#sUseGps').addEventListener('click', () => { if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition(p => { sun.querySelector('#sLat').value = p.coords.latitude.toFixed(2); sun.querySelector('#sLon').value = p.coords.longitude.toFixed(2); recalcSun(); }); }); sun.querySelector('#sToday').addEventListener('click', () => { const d = new Date(); sun.querySelector('#sDate').value = d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); recalcSun(); }); // ---- 10. CTS ---- const cts = makeCard({ icon: '🎯', title: 'Course To Steer (CTS)', desc: 'Какой курс держать при сносе течением.', fieldRows: [ [ { id:'cTrack', label:'Желаемый путь °', step:1, placeholder:'090' }, { id:'cBs', label:'Скорость яхты (уз)', step:0.1, placeholder:'5' }, ], [ { id:'cSet', label:'Set течения °', step:1, placeholder:'150' }, { id:'cDrift', label:'Drift (уз)', step:0.1, placeholder:'1' }, ], ], outId: 'ctsOut', placeholder: 'Введи все 4 значения.', tip: '💡 Сносит ВПРАВО — целься ЛЕВЕЕ. Отклонение обычно 5-15°.', }); function recalcCts() { const $ = s => cts.querySelector(s); const t = parseFloat($('#cTrack').value), bs = parseFloat($('#cBs').value); const set = parseFloat($('#cSet').value), dr = parseFloat($('#cDrift').value); const out = $('#ctsOut'); if ([t,bs,set,dr].some(isNaN) || bs <= 0) { setOut(out, 'Введи все 4 значения.', true); return; } const dRad = (set - t) * Math.PI / 180; const sinH = (dr * Math.sin(dRad)) / bs; if (Math.abs(sinH) > 1) { setOut(out, 'Течение слишком сильное — путь невозможен.', false); return; } const off = Math.asin(sinH) * 180 / Math.PI; const hd = ((t - off) % 360 + 360) % 360; const sog = bs * Math.cos(off * Math.PI / 180) + dr * Math.cos(dRad); setOut(out, 'CTS: ' + hd.toFixed(1) + '° (отворот ' + (off >= 0 ? 'влево' : 'вправо') + ' на ' + Math.abs(off).toFixed(1) + '°)
SOG: ' + sog.toFixed(2) + ' уз'); } cts.querySelectorAll('input').forEach(i => i.addEventListener('input', recalcCts)); panel.dataset.calcInit = 'true'; } /* ======================================================================== LOGBOOK — Судовой лог ======================================================================== */ const LOG_KEY_PREFIX = 'sailband.log.'; function logKeyFor(sessId) { return LOG_KEY_PREFIX + sessId; } function getLog(sessId) { try { const raw = localStorage.getItem(logKeyFor(sessId || getActiveSessId())); if (raw) return JSON.parse(raw); } catch(e) {} return []; } function appendLog(entry) { const sid = getActiveSessId(); const log = getLog(sid); log.unshift({ ts: Date.now(), ...entry }); // cap at 500 entries if (log.length > 500) log.length = 500; try { localStorage.setItem(logKeyFor(sid), JSON.stringify(log)); } catch(e) {} } // Hook: when user toggles a checklist item to "done", log it (function hookChecklistLogging() { // Wait one tick so this hook is added after renderLists exists if (typeof state === 'undefined') return; // We can monkey-patch the global click handler? No — easier to wrap save(). // Use a MutationObserver? Cleaner: emit a custom event from li toggle. // Easiest approach: wrap save() and detect newly-done items let prevDone = Object.assign({}, state.done); const _origSave = save; save = function() { // diff against prev for (const k of Object.keys(state.done)) { if (state.done[k] && !prevDone[k]) { // newly done — find sec/item by key const [secId, gIdx, iIdx] = k.split('.'); let title = ''; try { const sec = SECTIONS.find(s => s.id === secId); if (sec) { const grp = sec.groups[parseInt(gIdx,10)]; const item = grp.items[parseInt(iIdx,10)]; const text = (item && item.t || '').replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_,a,b) => (b||a)); const assignees = (state.assign[k] || []).map(id => { const c = (typeof CREW !== 'undefined') ? CREW.find(c => c.id === id) : null; return c ? c.short : id; }).join(', '); title = '✓ ' + text.slice(0, 120); appendLog({ type: 'check', sec: sec.title, item: text, assignees }); } } catch(e) {} } } prevDone = Object.assign({}, state.done); return _origSave.apply(this, arguments); }; })(); function renderLog() { const panel = document.getElementById('mode-log'); if (!panel) return; const log = getLog(); const session = (window.SailSessions && window.SailSessions.getActive()) || { name:'—' }; // Compute stats const totalChecks = log.filter(e => e.type === 'check').length; // Group by date const byDay = {}; for (const e of log) { const d = new Date(e.ts); const key = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; byDay[key] = byDay[key] || []; byDay[key].push(e); } const days = Object.keys(byDay).sort().reverse(); let html = `
${totalChecks}пунктов выполнено
${days.length}дней с активностью
${escapeHtml(session.name)}текущая сессия
`; if (log.length === 0) { html += '
Лог пуст. Отмечай пункты в чеклистах — они автоматически попадают сюда.
'; } else { for (const day of days) { const d = new Date(day + 'T00:00:00'); const wk = ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'][d.getDay()]; const mo = ['янв','фев','мар','апр','май','июн','июл','авг','сен','окт','ноя','дек'][d.getMonth()]; html += '
' + wk + ', ' + d.getDate() + ' ' + mo + ' ' + d.getFullYear() + '
'; const entries = byDay[day].sort((a,b) => b.ts - a.ts); for (const e of entries) { const t = new Date(e.ts); const tStr = `${String(t.getHours()).padStart(2,'0')}:${String(t.getMinutes()).padStart(2,'0')}`; if (e.type === 'check') { html += `
${tStr}
${escapeHtml(e.item || '')}
${escapeHtml(e.sec || '')}${e.assignees ? ' · ' + escapeHtml(e.assignees) : ''}
`; } else if (e.type === 'note') { html += `
${tStr} 📝
${escapeHtml(e.text || '')}
`; } } html += '
'; } } // Note input html += `
`; panel.innerHTML = html; panel.querySelector('#logExport').addEventListener('click', () => { const lines = ['Судовой лог · ' + session.name + ' · ' + new Date().toLocaleString('ru-RU'), '']; for (const day of days) { const d = new Date(day + 'T00:00:00'); lines.push('=== ' + d.toLocaleDateString('ru-RU') + ' ==='); const entries = byDay[day].sort((a,b) => b.ts - a.ts); for (const e of entries) { const t = new Date(e.ts); const tStr = `${String(t.getHours()).padStart(2,'0')}:${String(t.getMinutes()).padStart(2,'0')}`; if (e.type === 'check') lines.push(`${tStr} ✓ ${e.item}${e.assignees ? ' [' + e.assignees + ']' : ''} (${e.sec})`); else if (e.type === 'note') lines.push(`${tStr} 📝 ${e.text}`); } lines.push(''); } const text = lines.join('\n'); if (navigator.clipboard) { navigator.clipboard.writeText(text).then(() => toast('Лог скопирован в буфер')); } else { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); toast('Лог скопирован в буфер'); } catch(e) { toast('Не удалось скопировать'); } ta.remove(); } }); panel.querySelector('#logClear').addEventListener('click', () => { if (!confirm('Удалить весь лог текущей сессии «' + session.name + '»? Действие необратимо.')) return; try { localStorage.removeItem(logKeyFor(getActiveSessId())); } catch(e) {} renderLog(); toast('Лог очищен'); }); const noteInput = panel.querySelector('#logNoteInput'); const noteAdd = panel.querySelector('#logNoteAdd'); function submitNote() { const v = noteInput.value.trim(); if (!v) return; appendLog({ type: 'note', text: v }); noteInput.value = ''; renderLog(); } noteAdd.addEventListener('click', submitNote); noteInput.addEventListener('keydown', e => { if (e.key === 'Enter') submitNote(); }); } // Re-render log on session change if (window.SailSessions && Array.isArray(window.SailSessions.onChange)) { window.SailSessions.onChange.push(() => { if (document.getElementById('mode-log') && !document.getElementById('mode-log').hidden) renderLog(); }); } // Hook setMode to render calc/log on first show const _setModeForCalcLog = setMode; setMode = function(m) { _setModeForCalcLog(m); if (m === 'calc') renderCalc(); else if (m === 'log') renderLog(); }; /* ======================================================================== SOS OVERLAY — emergency procedure cheat sheet ======================================================================== */ function ensureSosMarkup() { if (document.getElementById('sosOverlay')) return; const ov = document.createElement('div'); ov.id = 'sosOverlay'; ov.className = 'sos-overlay'; ov.innerHTML = `
🚨 SOS — Mayday процедура
⚠️ Только при непосредственной угрозе жизни судна или экипажа. Иначе — PAN-PAN.
1. Включи VHF на канал 16, максимальная мощность 25W
2. Передай по схеме MIPDANIO:
«MAYDAY MAYDAY MAYDAY» — 3 раза
«THIS IS YACHT [name] [name] [name]» — название 3 раза
MAYDAY YACHT [name]» — еще раз короче
M — Mayday + название
I — Identification: позывной / MMSI / название
P — Position: широта/долгота (с GPS) или пеленг и расстояние до ориентира
D — Distress: что случилось (пожар / затопление / MOB / медицина)
A — Assistance: что нужно (буксир / эвакуация / скорая)
N — Number of POB: сколько людей на борту
I — Info: тип яхты, цвет, дополнительно
O — Over: «OVER» — жду ответа
3. Если есть DSC — нажми красную кнопку 5 секунд

DSC отправит позицию автоматически на канал 70. Потом всё равно говори голосом на 16.

4. Параллельно
💡 Если на 16 ответа нет 15 секунд — повторяй. Если 3 минуты тишина — MAYDAY RELAY ретранслируй сам или ищи другие способы связи.
`; document.body.appendChild(ov); const close = () => ov.classList.remove('is-open'); document.getElementById('sosClose').addEventListener('click', close); document.getElementById('sosCloseBtm').addEventListener('click', close); ov.addEventListener('click', e => { if (e.target === ov) close(); }); document.getElementById('sosCopyTpl').addEventListener('click', () => { const tpl = 'MAYDAY MAYDAY MAYDAY\nTHIS IS YACHT [NAME] [NAME] [NAME]\nMAYDAY YACHT [NAME]\nMY POSITION IS [LAT N/S] [LON E/W]\nNATURE OF DISTRESS: [ПРИЧИНА]\nI REQUIRE [ПОМОЩЬ]\nNUMBER OF PERSONS ON BOARD: [N]\nYACHT TYPE: [ТИП ЯХТЫ, ЦВЕТ]\nOVER'; if (navigator.clipboard) navigator.clipboard.writeText(tpl).then(() => toast('Шаблон скопирован')); else toast('Скопируй вручную'); }); } function openSos() { ensureSosMarkup(); document.getElementById('sosOverlay').classList.add('is-open'); } (function bindSosBtn() { const b = document.getElementById('sosBtn'); if (b) b.addEventListener('click', openSos); })(); /* ======================================================================== EXTEND MODE_META / MODE_GROUPS for Приборы ======================================================================== */ if (typeof MODE_META === 'object') { MODE_META.instr = { sub: 'Сенсоры', ttl: 'Приборы — компас, скорость, крен', lead: 'Используют сенсоры твоего телефона. Работают только когда страница открыта. Для GPS дай разрешение.' }; } try { if (typeof MODE_GROUPS !== 'undefined') { const gIdx = MODE_GROUPS.findIndex(g => g.label === 'Инструменты'); if (gIdx >= 0 && !MODE_GROUPS[gIdx].modes.includes('instr')) { MODE_GROUPS[gIdx].modes.push('instr'); } } // re-flatten and regroup const navRoot = document.getElementById('modes'); if (navRoot && navRoot.classList.contains('is-grouped') && typeof regroupModeNav === 'function') { const allModes = [...navRoot.querySelectorAll('.mode')]; navRoot.innerHTML = ''; allModes.forEach(b => navRoot.appendChild(b)); navRoot.classList.remove('is-grouped'); regroupModeNav(); } } catch(e) { console.warn('Instr group:', e); } /* ======================================================================== ПРИБОРЫ (instruments) — compass, SOG, heel, mini-map ======================================================================== */ const INSTR_STATE = { compass: { heading: null, watching: false }, gps: { lat: null, lon: null, sog: null, cog: null, acc: null, watchId: null, trail: [] }, motion: { heel: null, pitch: null, watching: false }, }; function renderInstruments() { const panel = document.getElementById('mode-instr'); if (!panel) return; if (panel.dataset.instrInit === 'true') return; panel.innerHTML = '
'; const grid = panel.querySelector('#toolsInstr'); // --- Compass card --- const compass = document.createElement('div'); compass.className = 'tool-card'; compass.innerHTML = '' + '
' + '
🧭
' + '

Компас

Магнитометр телефона. На iOS — даст разрешение.

' + '
' + '
' + '
' + '' + '
' + '
' + '
—°
' + '
курс по компасу
' + '
' + '
' + '
' + '
Поверни телефон лицом вверх и плоско. На iPhone — разреши доступ к ориентации.
'; grid.appendChild(compass); // --- GPS card --- const gps = document.createElement('div'); gps.className = 'tool-card'; gps.innerHTML = '' + '
' + '
📍
' + '

GPS · скорость и курс

SOG и COG в реальном времени с координатами.

' + '
' + '
' + '
SOG · уз
' + '
—°
COG
' + '
' + '
' + '
Lat
' + '
Lon
' + '
Точность
' + '
' + '
' + '' + '' + '
'; grid.appendChild(gps); // --- Heel/Pitch card --- const heel = document.createElement('div'); heel.className = 'tool-card'; heel.innerHTML = '' + '
' + '
⚖️
' + '

Крен и дифферент

Акселерометр телефона. Положи телефон ровно на яхте.

' + '
' + '
' + '
' + '' + '' + '' + 'port' + 'stbd' + '' + '' + '' + '' + '' + '' + '
' + '
' + '
—°
крен
' + '
—°
дифферент
' + '
' + '
' + '
'; grid.appendChild(heel); // --- Mini map card --- const map = document.createElement('div'); map.className = 'tool-card tool-card-full'; map.innerHTML = '' + '
' + '
🗺
' + '

След последних 10 минут

Включи GPS чтобы записывать трек. Без онлайн-карты.

' + '
' + '
Включи GPS чтобы увидеть путь.
' + '
Точки: 0
'; grid.appendChild(map); panel.dataset.instrInit = 'true'; bindInstruments(panel); } function bindInstruments(panel) { // ----- Compass ----- const compStart = panel.querySelector('#compStart'); const compHdg = panel.querySelector('#compHdg'); const compRose = panel.querySelector('#compassRose'); const compNote = panel.querySelector('#compNote'); async function startCompass() { if (typeof DeviceOrientationEvent === 'undefined') { compNote.textContent = 'Этот браузер не поддерживает ориентацию.'; return; } if (typeof DeviceOrientationEvent.requestPermission === 'function') { try { const r = await DeviceOrientationEvent.requestPermission(); if (r !== 'granted') { compNote.textContent = 'Разрешение не получено.'; return; } } catch(e) { compNote.textContent = 'Ошибка: ' + e.message; return; } } INSTR_STATE.compass.watching = true; compStart.textContent = 'Остановить'; compStart.classList.remove('primary'); compStart.classList.add('danger'); window.addEventListener('deviceorientation', onCompassEvent, true); window.addEventListener('deviceorientationabsolute', onCompassEvent, true); compNote.innerHTML = 'Поверни телефон — стрелка следит за севером.'; } function stopCompass() { INSTR_STATE.compass.watching = false; compStart.textContent = 'Включить компас'; compStart.classList.remove('danger'); compStart.classList.add('primary'); window.removeEventListener('deviceorientation', onCompassEvent, true); window.removeEventListener('deviceorientationabsolute', onCompassEvent, true); } function onCompassEvent(e) { let h = null; if (e.webkitCompassHeading != null) h = e.webkitCompassHeading; else if (e.absolute && e.alpha != null) h = 360 - e.alpha; if (h == null) return; h = ((h % 360) + 360) % 360; INSTR_STATE.compass.heading = h; compHdg.textContent = Math.round(h) + '°'; if (compRose) compRose.setAttribute('transform', 'rotate(' + (-h) + ')'); } compStart.addEventListener('click', () => INSTR_STATE.compass.watching ? stopCompass() : startCompass()); // ----- GPS ----- const gpsStart = panel.querySelector('#gpsStart'), gpsStop = panel.querySelector('#gpsStop'); const sogVal = panel.querySelector('#sogVal'), cogVal = panel.querySelector('#cogVal'); const latVal = panel.querySelector('#latVal'), lonVal = panel.querySelector('#lonVal'), accVal = panel.querySelector('#accVal'); const mapEl = panel.querySelector('#instrMap'), mapMeta = panel.querySelector('#mapMeta'); function startGps() { if (!navigator.geolocation) return; INSTR_STATE.gps.watchId = navigator.geolocation.watchPosition(pos => { const c = pos.coords; INSTR_STATE.gps.lat = c.latitude; INSTR_STATE.gps.lon = c.longitude; INSTR_STATE.gps.sog = (c.speed != null && !isNaN(c.speed)) ? c.speed * 1.94384 : null; INSTR_STATE.gps.cog = (c.heading != null && !isNaN(c.heading)) ? c.heading : null; INSTR_STATE.gps.acc = c.accuracy; INSTR_STATE.gps.trail.push({ lat: c.latitude, lon: c.longitude, ts: Date.now() }); const cut = Date.now() - 10*60*1000; INSTR_STATE.gps.trail = INSTR_STATE.gps.trail.filter(p => p.ts > cut).slice(-200); updateGpsUI(); }, err => console.warn('GPS:', err), { enableHighAccuracy: true, maximumAge: 3000, timeout: 20000 }); gpsStart.disabled = true; gpsStop.disabled = false; } function stopGps() { if (INSTR_STATE.gps.watchId != null) navigator.geolocation.clearWatch(INSTR_STATE.gps.watchId); INSTR_STATE.gps.watchId = null; gpsStart.disabled = false; gpsStop.disabled = true; } function updateGpsUI() { const g = INSTR_STATE.gps; sogVal.textContent = g.sog != null ? g.sog.toFixed(1) : '—'; cogVal.textContent = (g.cog != null ? Math.round(g.cog) : '—') + '°'; latVal.textContent = g.lat != null ? g.lat.toFixed(5) + '°' : '—'; lonVal.textContent = g.lon != null ? g.lon.toFixed(5) + '°' : '—'; accVal.textContent = g.acc != null ? '±' + Math.round(g.acc) + 'м' : '—'; drawMiniMap(); } function drawMiniMap() { const t = INSTR_STATE.gps.trail; if (t.length === 0) { mapEl.innerHTML = '
Включи GPS чтобы увидеть путь.
'; mapMeta.textContent = 'Точки: 0'; return; } const lats = t.map(p => p.lat), lons = t.map(p => p.lon); const minLat = Math.min(...lats), maxLat = Math.max(...lats); const minLon = Math.min(...lons), maxLon = Math.max(...lons); const pad = 0.0001; const dLat = Math.max(maxLat - minLat, pad), dLon = Math.max(maxLon - minLon, pad); const W = 800, H = 500; const sx = lon => ((lon - minLon) / dLon) * (W - 60) + 30; const sy = lat => H - ((lat - minLat) / dLat) * (H - 60) - 30; let pts = t.map(p => sx(p.lon) + ',' + sy(p.lat)).join(' '); const cur = t[t.length - 1]; mapEl.innerHTML = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; mapMeta.textContent = 'Точек: ' + t.length + ' · окно ' + (((Date.now() - t[0].ts)/60000).toFixed(1)) + ' мин'; } gpsStart.addEventListener('click', startGps); gpsStop.addEventListener('click', stopGps); // ----- Heel ----- const heelStart = panel.querySelector('#heelStart'); const heelVal = panel.querySelector('#heelVal'), pitchVal = panel.querySelector('#pitchVal'); const heelBoat = panel.querySelector('#heelBoat'); async function startHeel() { if (typeof DeviceOrientationEvent === 'undefined') return; if (typeof DeviceOrientationEvent.requestPermission === 'function') { try { const r = await DeviceOrientationEvent.requestPermission(); if (r !== 'granted') return; } catch(e) { return; } } INSTR_STATE.motion.watching = true; heelStart.textContent = 'Остановить'; heelStart.classList.remove('primary'); heelStart.classList.add('danger'); window.addEventListener('deviceorientation', onHeelEvent, true); } function stopHeel() { INSTR_STATE.motion.watching = false; heelStart.textContent = 'Включить'; heelStart.classList.remove('danger'); heelStart.classList.add('primary'); window.removeEventListener('deviceorientation', onHeelEvent, true); } function onHeelEvent(e) { const heel = e.gamma, pitch = e.beta; if (heel == null || pitch == null) return; INSTR_STATE.motion.heel = heel; INSTR_STATE.motion.pitch = pitch; heelVal.textContent = Math.abs(heel).toFixed(0) + '° ' + (heel > 0 ? '↗' : heel < 0 ? '↖' : '·'); pitchVal.textContent = Math.abs(pitch).toFixed(0) + '° ' + (pitch > 0 ? '↑' : pitch < 0 ? '↓' : '·'); if (heelBoat) heelBoat.setAttribute('transform', 'rotate(' + heel + ')'); } heelStart.addEventListener('click', () => INSTR_STATE.motion.watching ? stopHeel() : startHeel()); } // hook setMode for instr const _setModeForInstr = setMode; setMode = function(m) { _setModeForInstr(m); if (m === 'instr') renderInstruments(); }; /* ======================================================================== KNOT REPLAY ANIMATION ======================================================================== */ function injectKnotReplay() { const grid = document.getElementById('knotsGrid'); if (!grid) return; grid.querySelectorAll('.knot-card').forEach(card => { if (card.dataset.replayInit === 'true') return; const svg = card.querySelector('.kn-svg svg'); if (!svg) return; // find the "orange" path (working end) — usually 2nd path const allPaths = svg.querySelectorAll('path'); let drawPath = null; allPaths.forEach(p => { const stroke = p.getAttribute('stroke') || ''; if (stroke.includes('c2410c') || stroke.toLowerCase().includes('orange')) drawPath = p; }); if (!drawPath) drawPath = allPaths[allPaths.length - 1]; if (!drawPath) return; // measure length once let len; try { len = drawPath.getTotalLength(); } catch(e) { len = 200; } drawPath.style.strokeDasharray = len; drawPath.style.strokeDashoffset = 0; drawPath.style.transition = 'stroke-dashoffset 2.4s ease-in-out'; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'knot-replay-btn'; btn.innerHTML = '▶ Показать как вяжется'; card.querySelector('.kn-svg').after(btn); btn.addEventListener('click', e => { e.stopPropagation(); drawPath.style.transition = 'none'; drawPath.style.strokeDashoffset = len; // force reflow void drawPath.getBoundingClientRect(); drawPath.style.transition = 'stroke-dashoffset 2.4s ease-in-out'; drawPath.style.strokeDashoffset = 0; }); card.dataset.replayInit = 'true'; }); } // Hook into mode switch: when knots opens, init replay const _setModeForKnots = setMode; setMode = function(m) { _setModeForKnots(m); if (m === 'knots') { // wait for modules.js to have rendered const t = setInterval(() => { if (document.querySelectorAll('#knotsGrid .knot-card').length > 0) { clearInterval(t); injectKnotReplay(); } }, 100); setTimeout(() => clearInterval(t), 5000); } }; "); const formMap = new Map(); const alt = allForms.map(f => { formMap.set(f.form.toLowerCase(), f.key); return esc(f.form); }).join("|"); if (!alt) return; // (? { if (!node) return; if (node.nodeType === 3) { // text const txt = node.nodeValue; if (!txt || txt.length < 3) return; re.lastIndex = 0; if (!re.test(txt)) return; re.lastIndex = 0; const frag = document.createDocumentFragment(); let last = 0, m; while ((m = re.exec(txt))) { const before = txt.slice(last, m.index); if (before) frag.appendChild(document.createTextNode(before)); const found = m[1]; const key = formMap.get(found.toLowerCase()); if (key && GLOSSARY[key]) { const span = document.createElement("span"); span.className = "term"; span.dataset.term = key; span.setAttribute("tabindex", "0"); span.setAttribute("role", "button"); // последнее слово + ? const parts = found.split(/(\s+)/); if (parts.length > 1) { const lastIdx = parts.length - 1; for (let i=0; i `
Канал ${ch.ch}
${ch.name}
${ch.d}
`).join(""); const procBlock = (title, steps, cls="") => `
${title}
${steps.map((s,i) => `
${i+1}
${s.t}
${s.d ? `
${s.d}
` : ""}
`).join("")}
`; $("#radioMayday").innerHTML = procBlock("Канал 16 - процедура MAYDAY", MAYDAY_STEPS, "is-warn"); $("#radioPanpan").innerHTML = procBlock("Канал 16 - процедура PAN-PAN", PANPAN_STEPS, ""); $("#radioSecurite").innerHTML = procBlock("Канал 16 - процедура SECURITE", SECURITE_STEPS, "is-info"); $("#radioAlpha").innerHTML = `
${ NATO_ALPHA.map(([l,w]) => `
${l}${w}
`).join("") }
`; $("#radioDSC").innerHTML = DSC_INFO.map(it => `
${it.t}
${it.d}
`).join(""); $("#radioEtiquette").innerHTML = VHF_ETIQUETTE.map(it => `
${it.t}
${it.d}
`).join(""); autoTermify($("#mode-radio")); } /* ============ KNOTS ============ */ const KNOT_SVG = { bowline: `петля`, figure8: ``, cleat: `восьмёрки`, reef: `концы параллельно!`, clove: `стойка`, sheetbend: `толстыйтонкий`, roundturn: `кольцо`, timber: `свая3-4 витка`, }; const KNOTS = [ { name:"Булинь", en:"Bowline", svg:"bowline", use:"Незатягивающаяся петля. САМЫЙ важный узел - для швартовки, крепления шкотов к парусу, спасения. Если учишь один узел - учи этот.", tip:"«Кролик выходит из норки, обегает дерево и прыгает обратно в норку». Сделай петлю (норка), конец идёт снизу вверх через петлю, обходит коренной конец сзади и возвращается в петлю сверху вниз.", critical:true }, { name:"Восьмёрка", en:"Figure-8", svg:"figure8", use:"Стопорный узел на концах шкотов и фалов. Не даёт тросу проскочить через блок или стопор. Вяжется за 2 секунды.", tip:"Конец ходовой обводишь вокруг коренного снизу вверх, потом продеваешь в получившуюся петлю сверху. Получается форма 8.", critical:true }, { name:"На утку", en:"Cleat hitch", svg:"cleat", use:"Крепление швартова или фала на утку (рогатку). Используется КАЖДЫЙ раз при швартовке. Должен быть автоматическим.", tip:"Один полный оборот вокруг основания утки. Потом восьмёрки через рога. Последний шлаг - с переворотом (петлёй), чтобы зафиксировать.", critical:true }, { name:"Рифовый", en:"Reef knot", svg:"reef", use:"Связывание двух одинаковых по толщине концов. Для сезней при уборке грота и рифлении. ТОЛЬКО для одинаковых!", tip:"Правый поверх левого, завяжи. Потом левый поверх правого, завяжи. Концы лежат ПАРАЛЛЕЛЬНО, не торчат. Если торчат - это «бабий узел», развяжется." }, { name:"Выбленочный", en:"Clove hitch", svg:"clove", use:"Быстрое временное крепление к стойке или рейлингу. Для кранцев на леере, временных креплений. НЕ для основной швартовки - может ползти!", tip:"Два шлага вокруг стойки, второй перекрещивает первый. Конец заводится под перекрестье. Для надёжности - добавь полуштык." }, { name:"Шкотовый", en:"Sheet bend", svg:"sheetbend", use:"Связывание двух тросов РАЗНОЙ толщины. Для наращивания швартова или буксирного конца.", tip:"Толстый трос сложи петлёй. Тонкий - снизу вверх через петлю, обведи оба конца толстого, заведи под себя. Тонкий конец должен быть с той же стороны, что и его коренной." }, { name:"Полуштык со шлагом", en:"Round turn & 2 half hitches", svg:"roundturn", use:"Надёжное крепление к кольцу, рыму или серьге. Для швартовки к причальному кольцу. Очень надёжен.", tip:"Полный оборот (шлаг) вокруг кольца/рыма. Потом два полуштыка на коренном конце. Второй полуштык в ту же сторону, что и первый." }, { name:"Удавка", en:"Timber hitch", svg:"timber", use:"Крепление к бревну, свае, рейлингу для буксировки. Быстро вяжется, легко развязывается после снятия нагрузки.", tip:"Обнести конец вокруг предмета, потом ходовой обмотать 3-4 раза вокруг самого себя. Под нагрузкой - затягивается, без нагрузки - распускается." }, ]; const KNOT_CHEAT = [ { sit:"Швартов на утку причала", knot:"На утку" }, { sit:"Швартов на кольцо/рым", knot:"Полуштык со шлагом" }, { sit:"Петля на конце троса", knot:"Булинь" }, { sit:"Стопор на шкоте/фале", knot:"Восьмёрка" }, { sit:"Кранец на леере", knot:"Выбленочный" }, { sit:"Связать два троса (одинаковых)", knot:"Рифовый" }, { sit:"Связать два троса (разных)", knot:"Шкотовый" }, { sit:"Сезень на грот/гик", knot:"Рифовый" }, { sit:"Буксирный конец на сваю", knot:"Удавка" }, { sit:"Спасательная петля на человека", knot:"Булинь" }, ]; function renderKnots(){ const grid = $("#knotsGrid"); grid.innerHTML = KNOTS.map(k => `
${k.name} ${k.en}
${k.critical ? 'экзамен' : ''}
${k.svg && KNOT_SVG[k.svg] ? `
${KNOT_SVG[k.svg]}
` : ''}
${k.use}
${k.tip}
`).join(""); grid.querySelectorAll(".knot-card").forEach(card => { card.addEventListener("click", () => card.classList.toggle("open")); }); $("#knotsCheat").innerHTML = `
${ KNOT_CHEAT.map(r => `
${r.sit} ${r.knot}
`).join("") }
`; autoTermify($("#mode-knots")); } /* ============ SAFETY / MOB ============ */ const MOB_STEPS = [ { n:"1", a:"Кричи «ЧЕЛОВЕК ЗА БОРТОМ!»", d:"Немедленно. Все на палубе должны услышать. Покажи рукой направление." }, { n:"2", a:"Один человек ТОЛЬКО СМОТРИТ", d:"Назначь наблюдателя. Он показывает рукой и НЕ ОТВОДИТ ВЗГЛЯД. Потерять из вида на волне - потерять человека." }, { n:"3", a:"Бросай спасательный круг", d:"Бросать ЗА человека по ветру, чтобы снесло к нему. Если есть огонь/дым - тем более." }, { n:"4", a:"Нажми MOB на GPS", d:"Кнопка MOB на картплоттере фиксирует координаты. Если нет GPS - запомни ориентиры на берегу." }, { n:"5", a:"Передай MAYDAY или PAN-PAN", d:"Канал 16: если угроза жизни - MAYDAY. Если контролируешь ситуацию - PAN-PAN. Координаты с GPS." }, { n:"6", a:"Маневрируй для подхода", d:"Quick Stop: резкий разворот назад. Или круг Вильямсона: 60° в сторону, потом 180° обратно на обратный курс. Под мотором надёжнее." }, { n:"7", a:"Подходи С ПОДВЕТРА", d:"Чтобы яхту НЕ несло на человека. Стоп машина ЗАРАНЕЕ - винт убьёт человека в воде." }, { n:"8", a:"Поднимай на борт", d:"Через транец или фалом через лебёдку. Человек в воде быстро теряет силы - он может не помочь себе." }, ]; const SAFETY_EQUIP = [ { nm:"Спасательные жилеты", d:"По числу экипажа + запас. Надувные автоматические (>150N). ПРОВЕРИТЬ перед выходом: баллон на месте, таблетка не сработала, свисток и огонь в кармане." }, { nm:"Страховочные пояса + линь", d:"Пристёгиваться к джекстею (трос вдоль палубы) ночью, в шторм, при работе на баке. Карабин должен открываться одной рукой." }, { nm:"Спасательный круг / подкова", d:"Минимум 2 на корме. Один с автоматическим огнём и буйком, второй с линем 30м. Должны быть доступны МГНОВЕННО." }, { nm:"Огнетушители", d:"Минимум 2: один в кокпите, один в каюте. Порошковые (ABC) или CO2. Проверить срок и давление на манометре." }, { nm:"Фальшфейеры и ракеты", d:"Красные парашютные ракеты (видно 25+ миль), красные ручные фальшфейеры (ближний круг), оранжевый дым (днём для вертолёта). ПРОВЕРИТЬ СРОК." }, { nm:"ЭПИРБ (EPIRB)", d:"Аварийный радиобуй. Передаёт координаты на спутник. Автоматический всплывает и включается сам при затоплении. Ручной - активировать и бросить в воду." }, { nm:"Аптечка", d:"Морская аптечка: от морской болезни (ЗАРАНЕЕ), от ожогов, порезов, для иммобилизации. На длинный переход - антибиотики." }, { nm:"Спасательный плот", d:"Надувной в контейнере на палубе. Раскрывается рывком линя. НЕ надувать на палубе - спускать в воду сначала. На экзамене спрашивают процедуру!" }, { nm:"Нож и ведро", d:"Нож - перерезать запутавшийся трос. Ведро - для черпания воды при отказе помпы. Оба должны быть привязаны." }, { nm:"Якорь с цепью и тросом", d:"Минимум 1 основной + 1 запасной. Якорная цепь - 3-5 длин глубины. Чтобы держал - травить больше цепи." }, ]; const DISTRESS_SIGNALS = [ { s:"MAYDAY по VHF (канал 16)", type:"radio" }, { s:"DSC distress alert (канал 70)", type:"radio" }, { s:"EPIRB - аварийный радиобуй", type:"radio" }, { s:"SART - радарный транспондер", type:"radio" }, { s:"Красные парашютные ракеты", type:"visual" }, { s:"Красные ручные фальшфейеры", type:"visual" }, { s:"Оранжевый дым (днём)", type:"visual" }, { s:"Флаги N + C (November-Charlie)", type:"visual" }, { s:"Квадрат + шар (фигуры на мачте)", type:"visual" }, { s:"Медленный подъём/опускание рук", type:"visual" }, { s:"SOS (... --- ...) свистком или светом", type:"signal" }, { s:"Непрерывный звук туманного горна", type:"signal" }, { s:"Выстрел каждую минуту", type:"signal" }, ]; const FIRE_STEPS = [ { t:"Определи тип: двигатель / камбуз (газ) / электрика", d:"От этого зависит чем тушить. Воду на электрику и масло НЕЛЬЗЯ." }, { t:"Крикни «ПОЖАР!» + где именно", d:"Весь экипаж должен знать что горит и где." }, { t:"Электрика - отключи главный рубильник", d:"Обесточь яхту. Рубильник обычно у входа в каюту." }, { t:"Камбуз (газ) - перекрой газ на баллоне", d:"Кран на баллоне в рундуке кокпита, НЕ на плите." }, { t:"Двигатель - закрой моторный отсек", d:"Перекрой доступ кислорода. Используй CO2 огнетушитель через лючок." }, { t:"Туши к КОРНЮ огня, не к пламени", d:"Огнетушитель направляй на то, что горит, а не на верхушки пламени." }, { t:"Не потушил за 2 мин - готовь эвакуацию", d:"Спасай экипаж. Жилеты, плот, ЭПИРБ, документы, воду." }, ]; const BEAUFORT = [ { n:0, kn:"< 1", name:"Штиль", sea:"Зеркальная поверхность", sail:"Мотор" }, { n:1, kn:"1-3", name:"Тихий", sea:"Рябь", sail:"Мотор" }, { n:2, kn:"4-6", name:"Лёгкий", sea:"Мелкие волны 0.1-0.3м", sail:"Полные паруса, лёгкий ход" }, { n:3, kn:"7-10", name:"Слабый", sea:"Волны 0.6-1м, барашки", sail:"Хороший ход, все паруса" }, { n:4, kn:"11-16", name:"Умеренный", sea:"Волны 1-1.5м, барашки", sail:"Полные паруса, крен 15-20°" }, { n:5, kn:"17-21", name:"Свежий", sea:"Волны 2-2.5м, брызги", sail:"1-й риф, уменьшить стаксель" }, { n:6, kn:"22-27", name:"Сильный", sea:"Волны 3-4м, пена", sail:"2-й риф, малый стаксель", warn:true }, { n:7, kn:"28-33", name:"Крепкий", sea:"Волны 4-5.5м, полосы пены", sail:"3-й риф или штормовой стаксель", warn:true }, { n:8, kn:"34-40", name:"Шторм", sea:"Волны 5.5-7.5м", sail:"Штормовые паруса / голый рангоут", warn:true }, { n:9, kn:"41-47", name:"Сильный шторм", sea:"Волны 7-10м", sail:"Выживание", warn:true }, { n:10, kn:"48-55", name:"Жестокий шторм", sea:"Волны 9-12.5м", sail:"Выживание", warn:true }, ]; function renderSafety(){ const mobEl = $("#safetyMOB"); mobEl.innerHTML = `
MOB - порядок действий (выучить!)
${MOB_STEPS.map(s => `
${s.n}
${s.a}
${s.d}
`).join("")}
`; $("#safetyEquip").innerHTML = SAFETY_EQUIP.map(it => `
${it.nm}
${it.d}
`).join(""); const typeLabel = { radio:"Радио", visual:"Визуальный", signal:"Звуковой" }; const typeCls = { radio:"is-radio", visual:"is-visual", signal:"is-signal" }; $("#safetyDistress").innerHTML = `
${ DISTRESS_SIGNALS.map(s => `
${typeLabel[s.type]} ${s.s}
`).join("") }
`; $("#safetyFire").innerHTML = `
Пожар - порядок действий
${FIRE_STEPS.map((s,i) => `
${i+1}
${s.t}
${s.d ? `
${s.d}
` : ""}
`).join("")}
`; $("#safetyBeaufort").innerHTML = `
Б Узлы Описание / паруса
${BEAUFORT.map(b => `
${b.n} ${b.kn} kn ${b.name} - ${b.sea}. ${b.sail}.
`).join("")}
`; autoTermify($("#mode-safety")); } /* ============ BUOYAGE ============ */ const LATERAL = [ { side:"port", name:"Левый (порт)", color:"#dc2626", shape:"Цилиндр (банка)", topmark:"Красный цилиндр", light:"Красный, любой ритм", rule:"При входе с моря - оставляй СЛЕВА", icon:"▮" }, { side:"stbd", name:"Правый (штирборд)", color:"#16a34a", shape:"Конус", topmark:"Зелёный конус вершиной вверх", light:"Зелёный, любой ритм", rule:"При входе с моря - оставляй СПРАВА", icon:"▲" }, ]; const CARDINAL = [ { dir:"N", name:"Северный", topmark:"▲▲ оба вверх", colors:"Чёрный сверху, жёлтый снизу", pass:"Проходи к СЕВЕРУ от знака", light:"Q (непрерывный) или VQ", memo:"Стрелки вверх = проходи выше (севернее)" }, { dir:"E", name:"Восточный", topmark:"▲▼ основаниями друг к другу", colors:"Чёрный-жёлтый-чёрный", pass:"Проходи к ВОСТОКУ от знака", light:"Q(3) каждые 10с или VQ(3) 5с", memo:"3 проблеска = 3 часа на циферблате = Восток" }, { dir:"S", name:"Южный", topmark:"▼▼ оба вниз", colors:"Жёлтый сверху, чёрный снизу", pass:"Проходи к ЮГУ от знака", light:"Q(6)+LFl каждые 15с или VQ(6)+LFl 10с", memo:"Стрелки вниз = проходи ниже (южнее); 6 проблесков = 6 часов" }, { dir:"W", name:"Западный", topmark:"▼▲ вершинами друг к другу", colors:"Жёлтый-чёрный-жёлтый", pass:"Проходи к ЗАПАДУ от знака", light:"Q(9) каждые 15с или VQ(9) 10с", memo:"9 проблесков = 9 часов на циферблате = Запад" }, ]; const SPECIAL_BUOYS = [ { name:"Отдельная опасность", colors:"Чёрный + красная горизонтальная полоса", topmark:"●● два чёрных шара", light:"Fl(2) белый", d:"Стоит ПРЯМО на опасности (мель, затонувшее). Обходить можно с любой стороны - вода вокруг безопасна." }, { name:"Безопасная вода", colors:"Красно-белые вертикальные полосы", topmark:"● красный шар", light:"Iso белый или Мо(А)", d:"Глубоко со всех сторон. Ставится на осевой линии фарватера, у входа из моря. Можно подходить с любой стороны." }, { name:"Специальный знак", colors:"Жёлтый целиком", topmark:"× жёлтый крест", light:"Жёлтый, любой ритм", d:"Зоны: гонки, военные, кабели, трубопроводы, водолазные работы. Подробности - в лоции." }, { name:"Новая опасность", colors:"Сине-жёлтые полосы", topmark:"Вертикальный крест", light:"VQ или Q", d:"Ещё не нанесена на карту! Дублируется вторым знаком до нанесения. Слушай НАВТЕКС и VHF." }, ]; function renderBuoyage(){ $("#buoyLateral").innerHTML = LATERAL.map(l => `
${l.icon}
${l.name}
Форма: ${l.shape}
Топовый знак: ${l.topmark}
Огонь: ${l.light}
${l.rule}
`).join("") + `
Мнемоника IALA Region A (Европа): При входе с моря в порт: красный слева, зелёный справа. Запомни: «There is some RED PORT LEFT in the bottle» (красный - порт - лево).
`; $("#buoyCardinal").innerHTML = `
${ CARDINAL.map(c => `
${c.dir} - ${c.name}
Топовый знак: ${c.topmark}
Окраска: ${c.colors}
Огонь: ${c.light}
${c.pass}
${c.memo}
`).join("") }
`; $("#buoySpecial").innerHTML = SPECIAL_BUOYS.map(b => `
${b.name}
${b.colors} | ${b.topmark} | ${b.light}
${b.d}
`).join(""); $("#buoyMnemonic").innerHTML = `
Кардинальные знаки - как запомнить по часам:
Топовые знаки (конусы): стрелки показывают куда проходить.
N ▲▲ - оба вверх = иди ВЫШЕ (к северу)
S ▼▼ - оба вниз = иди НИЖЕ (к югу)
E ▲▼ - основаниями друг к другу = 3 проблеска (3 часа = восток)
W ▼▲ - вершинами друг к другу = 9 проблесков (9 часов = запад)

Окраска: чёрная полоса показывает направление прохода:
N - чёрный СВЕРХУ (иди к чёрному = на север)
S - чёрный СНИЗУ (иди к чёрному = на юг)
E - чёрный по краям, жёлтый в середине
W - жёлтый по краям, чёрный в середине
`; autoTermify($("#mode-buoyage")); } /* ============ YACHT PARTS (for Rig panel) ============ */ const YACHT_PARTS = [ { nm:"Бак (форпик)", en:"Bow / Forepeak", d:"Носовая часть палубы от мачты к носу. Внутри - форпиковая каюта и якорный ящик. Здесь работают с мурингами и якорем." }, { nm:"Корма", en:"Stern", d:"Задняя часть яхты. Штурвал, кокпит, транец. Откидной транец - для купания и посадки в тендер." }, { nm:"Кокпит", en:"Cockpit", d:"Открытая рабочая площадка на корме. Здесь штурвал, приборы, лебёдки, стопоры. Экипаж сидит тут." }, { nm:"Рубка", en:"Coachroof / Cabin top", d:"Надстройка над каютой. На ней стопоры, люки, иногда лебёдки. Не путать с палубой." }, { nm:"Камбуз", en:"Galley", d:"Кухня яхты. Плита (газ!), раковина, холодильник. Газовый баллон - отдельно, в вентилируемом рундуке." }, { nm:"Гальюн", en:"Head", d:"Туалет + душ. Морской унитаз с ручной помпой. Важно: кран забортной воды закрыть после использования." }, { nm:"Салон (кают-компания)", en:"Saloon", d:"Главное жилое помещение. Стол, диваны, навигационный стол. Под полом - трюмный колодец и доступ к двигателю." }, { nm:"Каюта", en:"Cabin / Berth", d:"Спальное помещение. На круизной яхте 3-4 каюты. Двуспальные кровати, рундуки для вещей." }, { nm:"Моторный отсек", en:"Engine compartment", d:"Под кокпитом или салоном. Дизельный двигатель, масляные фильтры, помпа охлаждения. Проверять масло ПЕРЕД запуском." }, { nm:"Транец", en:"Transom", d:"Кормовая стенка корпуса. Откидной транец = купальная платформа. Через него садятся в тендер и в воду." }, { nm:"Рундук", en:"Locker", d:"Ящик/отсек для хранения. Кокпитные рундуки - для спасательного оборудования, кабелей, шлангов. Каютные - для вещей." }, { nm:"Киль", en:"Keel", d:"Подводный плавник с тяжёлым грузом (свинец/чугун). Не даёт перевернуться и сносить боком. На круизной яхте - фиксированный." }, { nm:"Перо руля", en:"Rudder", d:"Подводная пластина за килем. Управляется штурвалом через систему тросов или тяг. Поворачивает яхту." }, { nm:"Форпик", en:"Forepeak", d:"Самый носовой отсек. Обычно якорный ящик сверху, каюта или склад снизу. Тут хранят паруса и снасти." }, { nm:"Палубный люк", en:"Deck hatch", d:"Открывающееся окно в палубе/рубке для вентиляции кают. ЗАКРЫВАТЬ перед выходом в море и при любой волне." }, ]; function renderYachtParts(){ const card = (it) => `
${it.nm}
${it.en}
${it.d}
`; $("#rigParts2").innerHTML = YACHT_PARTS.map(i=>card(i)).join(""); } /* ============ TIDES (for Nav panel) ============ */ const TIDES_DATA = { rule12: [ { hr:"1-й час", frac:"1/12", pct:"~8%", cum:"8%" }, { hr:"2-й час", frac:"2/12", pct:"~17%", cum:"25%" }, { hr:"3-й час", frac:"3/12", pct:"~25%", cum:"50%" }, { hr:"4-й час", frac:"3/12", pct:"~25%", cum:"75%" }, { hr:"5-й час", frac:"2/12", pct:"~17%", cum:"92%" }, { hr:"6-й час", frac:"1/12", pct:"~8%", cum:"100%" }, ], terms: [ { nm:"Прилив (Flood)", d:"Вода поднимается. Течение идёт к берегу." }, { nm:"Отлив (Ebb)", d:"Вода опускается. Течение идёт от берега." }, { nm:"Полная вода (HW)", d:"High Water - максимальный уровень. После неё начинается отлив." }, { nm:"Малая вода (LW)", d:"Low Water - минимальный уровень. После неё начинается прилив." }, { nm:"Размах прилива (Range)", d:"Разница между HW и LW. В весенние приливы - максимальная, в квадратурные - минимальная." }, { nm:"Весенний прилив (Springs)", d:"Максимальные приливы/отливы. Когда Солнце и Луна на одной линии (новолуние/полнолуние). Течения сильнее." }, { nm:"Квадратурный прилив (Neaps)", d:"Минимальные приливы/отливы. Когда Луна под 90° к Солнцу (первая/третья четверть). Течения слабее." }, { nm:"Течение (Current/Tidal stream)", d:"Горизонтальное движение воды. Приливное течение меняет направление ~каждые 6 часов. Влияет на курс яхты - её сносит." }, ], }; const CHART_SYMBOLS = [ { nm:"Глубина (числа)", en:"Sounding", d:"Числа на карте - глубина в МЕТРАХ (или футах, смотри заголовок карты). Измерена от уровня наименьшего прилива (Chart Datum)." }, { nm:"Изобата", en:"Depth contour", d:"Линия одинаковой глубины. 2м, 5м, 10м, 20м. Где линии густо - дно резко поднимается. Опасно!" }, { nm:"Якорная стоянка ⚓", en:"Anchorage", d:"Знак якоря на карте - рекомендованное место для якорной стоянки. Хороший грунт, защита от ветра." }, { nm:"Мель / осушка", en:"Drying height", d:"Подчёркнутые числа - высота осушки: сколько грунта обнажается при отливе. Опасно для киля!" }, { nm:"Затонувшее ✕", en:"Wreck", d:"Крестик или + - затонувший объект. Может быть опасен для киля. Обходить с запасом." }, { nm:"Скала ✳", en:"Rock", d:"Звёздочка - подводная скала. С числом - глубина над ней. Без числа - обнажается при отливе." }, { nm:"Буй / знак", en:"Buoy", d:"Символ буя с описанием: цвет, топовый знак, характеристика огня. Маленький кружок с точкой - точное место." }, { nm:"Магнитное склонение", en:"Variation", d:"Компасная роза на карте показывает склонение и год. Каждый год меняется - пересчитывать!" }, { nm:"Запретная зона", en:"Prohibited area", d:"Косая штриховка - нельзя заходить. Военная, природоохранная, или другие ограничения." }, { nm:"Течение →", en:"Tidal stream", d:"Стрелки с числами - направление и скорость приливного течения. Первое число - квадратура, второе - сизигия." }, ]; function renderTides(){ const el = $("#navTides"); if (!el) return; el.innerHTML = `
Правило двенадцатых (Rule of Twelfths)
За 6 часов от LW до HW (или обратно) вода поднимается/опускается неравномерно: медленно-быстро-быстро-медленно. Самые быстрые часы - 3-й и 4-й (по 3/12 = 25% размаха каждый).
Час Доля Набрано от размаха
${TIDES_DATA.rule12.map(r => `
${r.hr} ${r.frac} ${r.pct} (итого ${r.cum})
`).join("")}
Пример: LW = 0.5м, HW = 3.5м, размах = 3.0м.
Через 3 часа после LW: 0.5 + 3.0 × 50% = 2.0м
Через 4 часа после LW: 0.5 + 3.0 × 75% = 2.75м
${TIDES_DATA.terms.map(t => `
${t.nm}
${t.d}
`).join("")}
`; $("#navChartSymbols").innerHTML = CHART_SYMBOLS.map(s => `
${s.nm}
${s.en}
${s.d}
`).join(""); } /* ============ CLOUDS & LOCAL WINDS (for Safety panel) ============ */ const CLOUDS = [ { nm:"Перистые (Cirrus)", d:"Тонкие белые «волоски» высоко. Если появляются и сгущаются - через 12-24 часа возможен фронт. Если мало и не растут - хорошая погода." }, { nm:"Кучевые (Cumulus)", d:"Белые «ватные» облака с плоским основанием. Признак хорошей погоды, если небольшие. Если растут вверх (башни) - к грозе." }, { nm:"Кучево-дождевые (Cb)", d:"Огромные тёмные башни с наковальней наверху. ГРОЗА. Шквал, ливень, молнии. Если видишь Cb - готовь рифы, закрывай люки." }, { nm:"Слоистые (Stratus)", d:"Серая пелена по всему небу. Мелкий дождь или морось. Плохая видимость. Не опасно, но неприятно." }, { nm:"Высококучевые (Altocumulus)", d:"«Барашки» на среднем уровне. Если утром - к грозе после обеда. Если вечером - к хорошей погоде." }, { nm:"Барическая тенденция", d:"Давление падает больше 3 мб за 3 часа - ВНИМАНИЕ, приближается шторм. Медленный рост давления - к стабильной погоде." }, ]; const LOCAL_WINDS = [ { nm:"Мельтеми (Этезии)", d:"Сильный северный ветер в Эгейском море, июль-август. 5-7 баллов, иногда до 8. Стабильный, предсказуемый. Стихает к ночи. Главный ветер для чартера в Греции." }, { nm:"Бора", d:"Холодный сильный ветер с гор к морю. Хорватия, черногорское побережье. Зимой до 10 баллов. Шквалистый, опасный. Признак: ясное небо + резкое падение температуры." }, { nm:"Мистраль", d:"Сильный холодный северо-западный ветер. Юг Франции, Лигурийское море. До 8-9 баллов. Разгоняет большую волну. Обычно 2-3 дня." }, { nm:"Сирокко (Юго)", d:"Тёплый влажный южный ветер. Несёт пыль из Сахары. Плохая видимость. В Адриатике называется «юго» - приносит дождь и волну." }, { nm:"Бриз (морской/береговой)", d:"Днём ветер с моря на берег (морской бриз, 10-15 узлов). Ночью наоборот - с берега в море (береговой бриз, слабее). Нормальное явление в хорошую погоду." }, { nm:"Термический ветер", d:"Усиление ветра после обеда (13-16ч) из-за нагрева суши. К вечеру стихает. Учитывать при планировании переходов." }, ]; function renderWeather(){ $("#safetyClouds").innerHTML = CLOUDS.map(c => `
${c.nm}
${c.d}
`).join(""); $("#safetyWinds").innerHTML = LOCAL_WINDS.map(w => `
${w.nm}
${w.d}
`).join(""); } /* ============ SIGNAL FLAGS (for COLREGS panel) ============ */ const SIGNAL_FLAGS = [ { flag:"A", name:"Alfa", meaning:"У меня водолаз. Держитесь на расстоянии и на малом ходу.", colors:"Бело-синий (ласточкин хвост)", exam:true }, { flag:"B", name:"Bravo", meaning:"Я принимаю/выгружаю/перевожу опасный груз.", colors:"Красный (треугольный хвост)", exam:true }, { flag:"N", name:"November", meaning:"Нет (отрицание). Над флагом C = сигнал бедствия.", colors:"Сине-белая клетка", exam:true }, { flag:"C", name:"Charlie", meaning:"Да (подтверждение). Под флагом N = сигнал бедствия.", colors:"Красно-бело-синие полосы", exam:true }, { flag:"Q", name:"Quebec", meaning:"Моё судно чисто, прошу свободную практику (разрешение на вход в порт иностранного государства).", colors:"Жёлтый", exam:true }, { flag:"O", name:"Oscar", meaning:"Человек за бортом!", colors:"Красно-жёлтый по диагонали", exam:true }, { flag:"P", name:"Papa", meaning:"Все на борт, судно выходит в море. В порту: мне нужен лоцман.", colors:"Синий с белым прямоугольником", exam:false }, { flag:"V", name:"Victor", meaning:"Мне нужна помощь.", colors:"Белый с красным крестом (Андреевский)", exam:false }, { flag:"W", name:"Whiskey", meaning:"Мне нужна медицинская помощь.", colors:"Синий с белым крестом и красным квадратом", exam:false }, { flag:"H", name:"Hotel", meaning:"У меня лоцман на борту.", colors:"Красно-белый вертикально", exam:false }, ]; function renderFlags(){ const render = (container) => { if (!container) return; container.innerHTML = SIGNAL_FLAGS.map(f => `
${f.flag} - ${f.name} ${f.exam ? 'экзамен' : ''}
${f.colors}
${f.meaning}
`).join(""); }; render($("#colregsFlags")); render($("#cmdFlags")); } /* ============ ANCHOR ============ */ const ANCHOR_TYPES = [ { nm:"CQR (плуг)", en:"CQR / Plow", d:"Классический крейсерский якорь. Хорошо держит в песке, иле, глине. Плохо в камнях и водорослях. Часто стоит на круизных яхтах." }, { nm:"Дельта / Delta", en:"Delta", d:"Усовершенствованный плуг с фиксированной рукояткой. Лучше забирает грунт, хорош в песке и иле. Стандарт на современных чартерных яхтах." }, { nm:"Брюс / Bruce", en:"Bruce", d:"Три «лапки». Быстро забирает, хорош в песке. Хуже в твёрдой глине. Популярен на маленьких яхтах." }, { nm:"Danforth", en:"Danforth / Fluke", d:"Плоский с двумя большими лапами. Отличный в песке и иле. Плохо в камнях. Часто используется как запасной - компактно складывается." }, { nm:"Рокна / Rocna", en:"Rocna / New Generation", d:"Современный дизайн с рулём на лапе. Быстро забирает в любом грунте. Считается лучшим, но дорогой." }, ]; const ANCHOR_PLACE = [ { nm:"Защита от ветра", d:"Выбирай место, закрытое от предсказанного ветра. Посмотри прогноз: откуда будет дуть ночью? Берег должен закрывать от ветра." }, { nm:"Глубина", d:"Идеально 3-8м. Мелко - сядешь на мель при отливе. Глубоко - не хватит цепи. На карте проверь глубины и тип дна." }, { nm:"Грунт", d:"Лучше: песок, ил, глина. Хуже: камни, водоросли, ракушки. На карте: S = sand, M = mud, R = rock, Wd = weed." }, { nm:"Пространство для разворота", d:"Яхта на якоре разворачивается на 360° при смене ветра/течения. Радиус = длина цепи + длина яхты. Не задень соседей!" }, { nm:"Течения", d:"Сильное течение = больше нагрузка на якорь. В приливных районах цепи нужно больше. Следи за направлением течения." }, { nm:"Пути отхода", d:"Если ночью усилится ветер - сможешь сняться? Не запирайся в тупике. Запомни пеленги на выход." }, ]; const ANCHOR_WATCH = [ { nm:"GPS-якорный круг", d:"Большинство картплоттеров умеют ставить anchor alarm. Если яхта выходит за круг - пищит. Настрой перед сном!" }, { nm:"Пеленги на берег", d:"Запомни 2-3 ориентира на берегу и их пеленги. Если пеленг изменился - якорь ползёт." }, { nm:"Провис цепи", d:"Потрогай цепь рукой (осторожно!). Если вибрирует/дрожит - якорь скребёт по дну. Если спокойно провисает - держит." }, { nm:"Что делать если ползёт", d:"1) Травить больше цепи. 2) Если не помогает - завести двигатель и подтянуться к якорю. 3) Поднять и переставить в другом месте. Ночью - НЕ паниковать." }, ]; function renderAnchor(){ $("#anchorTypes").innerHTML = ANCHOR_TYPES.map(a => `
${a.nm}
${a.en}
${a.d}
`).join(""); $("#anchorScope").innerHTML = `
Scope (отношение длины цепи к глубине):

Минимум: 3:1 - три длины глубины (только в тихую погоду, ненадолго)
Норма: 5:1 - пять длин глубины (стандарт для ночёвки)
Шторм: 7:1 и больше - семь длин (сильный ветер, плохой грунт)

Пример: глубина 5м + 2м (высота клюза над дном) = 7м.
Scope 5:1 = 7 × 5 = 35м цепи.

Цепь vs трос: Цепь лучше (тяжёлая, лежит на дне, амортизирует). Трос легче, но нужен scope 7:1 минимум и обязательно 5-10м цепи у якоря.
`; const procBlock = (title, steps, cls="") => `
${title}
${steps.map((s,i) => `
${i+1}
${s.t}
${s.d ? `
${s.d}
` : ""}
`).join("")}
`; $("#anchorSet").innerHTML = procBlock("Постановка на якорь", [ { t:"Подойти к месту ПРОТИВ ветра/течения", d:"Нос на ветер, малый ход. Остановиться там, где хочешь стоять." }, { t:"Команда: «Приготовить якорь!»", d:"Снять крепление, проверить что цепь не запутана, подготовить стопор." }, { t:"Стоп машина, дать яхте остановиться", d:"Яхта должна зависнуть без хода." }, { t:"Команда: «Отдать якорь!»", d:"Якорь пошёл. НЕ бросать с грохотом - опускать контролируемо через брашпиль или руками." }, { t:"Медленно назад - травить цепь", d:"Включить реверс на холостых. Цепь ложится на дно ровно, не кучей." }, { t:"Вытравить расчётную длину цепи", d:"Scope 5:1 от глубины + высоты клюза. Пометки на цепи помогают считать." }, { t:"Дать натяжение задним ходом", d:"Подгазовать на 1500-2000 об/мин назад на 30 секунд. Якорь должен забрать грунт." }, { t:"Проверить: держит?", d:"GPS не двигается, пеленги на берег стабильны. Если ползёт - больше цепи или переставить." }, { t:"Застопорить цепь, заглушить двигатель", d:"Поставить якорный стопор. Запомнить пеленги. Включить якорный огонь если темнеет." }, ]); $("#anchorWeigh").innerHTML = procBlock("Снятие с якоря", [ { t:"Завести двигатель, прогреть", d:"Всегда под мотором. Под парусами с якоря - только для опытных." }, { t:"Команда: «Выбираем якорь!»", d:"Человек на носу начинает подтягивать цепь (брашпиль или руками)." }, { t:"Подходить вперёд к якорю", d:"Малый ход вперёд, пока нос на баке выбирает. Не тянуть цепь - подъезжать к ней." }, { t:"Цепь вертикально - «Якорь на панер!»", d:"Цепь стоит вертикально. Якорь вот-вот оторвётся от дна." }, { t:"Выдрать якорь: короткий рывок вперёд", d:"Подгазовать - якорь отрывается. Человек на носу: «Якорь чист!» или «Якорь нечист!»" }, { t:"Поднять якорь полностью", d:"Закрепить в клюзе. Промыть водой от грязи если есть." }, { t:"Осмотреть: чист?", d:"Нет ли чужих тросов, водорослей, камней на лапах. Если нечист - прочистить." }, ], "is-info"); $("#anchorWatch").innerHTML = ANCHOR_WATCH.map(a => `
${a.nm}
${a.d}
`).join(""); $("#anchorPlace").innerHTML = ANCHOR_PLACE.map(a => `
${a.nm}
${a.d}
`).join(""); autoTermify($("#mode-anchor")); } /* ============ COMMANDS ============ */ const CMD_DOCK = [ { cmd:"«Разложить швартовы по бортам!»", en:"Prepare mooring lines!", when:"Перед отходом. Концы на палубе, готовы к отдаче." }, { cmd:"«Штурвал в ДП!»", en:"Helm amidships!", when:"Руль ровно по центру." }, { cmd:"«Отдать подветренный муринг!»", en:"Let go lee mooring!", when:"Первым отдаём тот, что не натянут ветром." }, { cmd:"«Отдать наветренный муринг!»", en:"Let go weather mooring!", when:"Вторым - тот, что держит яхту против ветра." }, { cmd:"«Швартовы на руку!»", en:"Take lines in hand!", when:"Снять со свай, держать в руках, готовы отдать." }, { cmd:"«Отдать швартов!»", en:"Let go! / Cast off!", when:"Бросить конец. Быстро вытянуть на борт." }, { cmd:"«Травить швартов!»", en:"Ease the line!", when:"Медленно отпускать, контролируя натяжение." }, { cmd:"«Одержать!»", en:"Hold! / Check!", when:"Резко остановить, зафиксировать конец." }, { cmd:"«Набить швартов!»", en:"Make fast! / Heave!", when:"Подтянуть и закрепить." }, { cmd:"«Швартовы на берег!»", en:"Lines ashore!", when:"Перекинуть концы на причал." }, { cmd:"«Включить подрульку!»", en:"Bow thruster on!", when:"В марине при ветре, для разворота." }, ]; const CMD_SAIL = [ { cmd:"«В левентик!»", en:"Head to wind!", when:"Нос на ветер. Перед постановкой/уборкой грота." }, { cmd:"«Поднимаем грот!»", en:"Hoist the main!", when:"Тянуть грота-фал." }, { cmd:"«Раскатываем стаксель!»", en:"Unroll the jib!", when:"Травить линь раскатки, тянуть шкот." }, { cmd:"«Стоп фал!» / «Закрепить!»", en:"Make fast halyard!", when:"Закрыть стопор, снять с лебёдки." }, { cmd:"«К повороту!» / «Готовы?»", en:"Ready about? / Ready to tack?", when:"Предупреждение: сейчас будет поворот." }, { cmd:"«Поворот!» / «Пошёл!»", en:"Lee-ho! / Tacking!", when:"Начинаем поворот оверштаг." }, { cmd:"«Перекинуть стаксель!»", en:"Release and sheet!", when:"Отпустить старый шкот, тянуть новый." }, { cmd:"«Фордевинд! Беречь головы!»", en:"Gybe-ho! Heads down!", when:"Поворот кормой через ветер. ОПАСНО - гик летит." }, { cmd:"«Потравить шкот!»", en:"Ease the sheet!", when:"Ослабить, дать уйти парусу по ветру." }, { cmd:"«Набить шкот!»", en:"Sheet in! / Trim!", when:"Подтянуть парус к центру." }, { cmd:"«Взять рифы!»", en:"Reef the main!", when:"Уменьшить площадь грота при сильном ветре." }, { cmd:"«Убираем паруса!»", en:"Drop sails! / Douse!", when:"Перед заходом в марину." }, ]; const CMD_ANCHOR_LIST = [ { cmd:"«Приготовить якорь!»", en:"Prepare anchor! / Stand by anchor!", when:"Снять крепление, проверить цепь." }, { cmd:"«Отдать якорь!»", en:"Let go anchor!", when:"Опустить якорь в воду." }, { cmd:"«Травить цепь!»", en:"Pay out chain!", when:"Отпускать цепь контролируемо." }, { cmd:"«Стоп цепь!» / «Застопорить!»", en:"Hold chain! / Make fast!", when:"Прекратить травить, закрепить." }, { cmd:"«Выбираем якорь!»", en:"Weigh anchor! / Heave up!", when:"Начать подтягивать цепь." }, { cmd:"«Якорь на панер!»", en:"Anchor aweigh!", when:"Цепь вертикально, якорь отрывается от дна." }, { cmd:"«Якорь чист!»", en:"Anchor clear!", when:"Якорь поднят, ничего не намотано." }, { cmd:"«Якорь нечист!»", en:"Foul anchor!", when:"На якоре чужой трос, водоросли или камни." }, ]; const CMD_EMERGENCY = [ { cmd:"«ЧЕЛОВЕК ЗА БОРТОМ!» + показать рукой", en:"MAN OVERBOARD! (MOB!)", when:"Немедленно. Самая приоритетная команда." }, { cmd:"«Не отводить взгляд!»", en:"Keep eyes on casualty!", when:"Наблюдатель ТОЛЬКО смотрит." }, { cmd:"«ПОЖАР!» + где именно", en:"FIRE!", when:"Указать место: двигатель, камбуз, каюта." }, { cmd:"«Спасательные жилеты ВСЕМ!»", en:"Life jackets everyone!", when:"При любой серьёзной угрозе." }, { cmd:"«Готовить плот!»", en:"Prepare life raft!", when:"Если яхта тонет и нет надежды." }, { cmd:"«MAYDAY! MAYDAY! MAYDAY!»", en:"MAYDAY!", when:"По радио на 16 канале при угрозе жизни." }, { cmd:"«Все к штурвалу!» / «Все наверх!»", en:"All hands on deck!", when:"Ситуация требует всех рук." }, ]; function renderCommands(){ const cmdTable = (data) => data.map(c => `
${c.cmd}
${c.en}
${c.when}
`).join(""); $("#cmdDock").innerHTML = cmdTable(CMD_DOCK); $("#cmdSail").innerHTML = cmdTable(CMD_SAIL); $("#cmdAnchor").innerHTML = cmdTable(CMD_ANCHOR_LIST); $("#cmdEmergency").innerHTML = cmdTable(CMD_EMERGENCY); autoTermify($("#mode-commands")); } /* === Initial renders для всех модулей === */ try { renderRig(); } catch(e){ console.warn('renderRig:', e); } try { renderRunningRig(); } catch(e){ console.warn('renderRunningRig:', e); } try { renderCourses(); } catch(e){ console.warn('renderCourses:', e); } try { renderNav(); } catch(e){ console.warn('renderNav:', e); } try { renderColregs(); } catch(e){ console.warn('renderColregs:', e); } try { renderRadio(); } catch(e){ console.warn('renderRadio:', e); } try { renderKnots(); } catch(e){ console.warn('renderKnots:', e); } try { renderSafety(); } catch(e){ console.warn('renderSafety:', e); } try { renderBuoyage(); } catch(e){ console.warn('renderBuoyage:', e); } try { renderYachtParts(); } catch(e){ console.warn('renderYachtParts:', e); } try { renderTides(); } catch(e){ console.warn('renderTides:', e); } try { renderWeather(); } catch(e){ console.warn('renderWeather:', e); } try { renderFlags(); } catch(e){ console.warn('renderFlags:', e); } try { renderAnchor(); } catch(e){ console.warn('renderAnchor:', e); } try { renderCommands(); } catch(e){ console.warn('renderCommands:', e); } window.AppModulesLoaded = true; if (typeof initTooltips === 'function') initTooltips();