@jjlmoya/utils-nautical 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/category/index.ts +2 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +9 -7
- package/src/tests/diacritics_density.test.ts +1 -1
- package/src/tests/inverted_punctuation.test.ts +1 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/script_density.test.ts +1 -1
- package/src/tests/title_quality.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/anchorScope/anchorScope.css +507 -0
- package/src/tool/anchorScope/bibliography.astro +6 -0
- package/src/tool/anchorScope/bibliography.ts +14 -0
- package/src/tool/anchorScope/component.astro +219 -0
- package/src/tool/anchorScope/constants.ts +51 -0
- package/src/tool/anchorScope/controller.ts +251 -0
- package/src/tool/anchorScope/dom-views.ts +106 -0
- package/src/tool/anchorScope/entry.ts +31 -0
- package/src/tool/anchorScope/evaluator.ts +61 -0
- package/src/tool/anchorScope/i18n/de.ts +201 -0
- package/src/tool/anchorScope/i18n/en.ts +223 -0
- package/src/tool/anchorScope/i18n/es.ts +223 -0
- package/src/tool/anchorScope/i18n/fr.ts +201 -0
- package/src/tool/anchorScope/i18n/id.ts +201 -0
- package/src/tool/anchorScope/i18n/it.ts +201 -0
- package/src/tool/anchorScope/i18n/ja.ts +201 -0
- package/src/tool/anchorScope/i18n/ko.ts +201 -0
- package/src/tool/anchorScope/i18n/nl.ts +201 -0
- package/src/tool/anchorScope/i18n/pl.ts +201 -0
- package/src/tool/anchorScope/i18n/pt.ts +201 -0
- package/src/tool/anchorScope/i18n/ru.ts +201 -0
- package/src/tool/anchorScope/i18n/sv.ts +201 -0
- package/src/tool/anchorScope/i18n/tr.ts +201 -0
- package/src/tool/anchorScope/i18n/zh.ts +201 -0
- package/src/tool/anchorScope/index.ts +10 -0
- package/src/tool/anchorScope/logic.test.ts +100 -0
- package/src/tool/anchorScope/logic.ts +84 -0
- package/src/tool/anchorScope/presets.ts +12 -0
- package/src/tool/anchorScope/seo.astro +14 -0
- package/src/tool/anchorScope/storage.ts +25 -0
- package/src/tool/anchorScope/types.ts +33 -0
- package/src/tool/anchorScope/ui.ts +63 -0
- package/src/tool/endurance/index.ts +2 -1
- package/src/tools.ts +3 -0
- package/src/types.ts +3 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AnchorScopeUI } from './ui';
|
|
3
|
+
import './anchorScope.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: AnchorScopeUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<section id="anchor-scope-calculator" class="asc-calculator" data-i18n={JSON.stringify(ui)}>
|
|
13
|
+
<div class="asc-main-card">
|
|
14
|
+
<div class="asc-sidebar">
|
|
15
|
+
<div class="asc-sidebar-top">
|
|
16
|
+
<span class="asc-sidebar-title">{ui.parametersTitle}</span>
|
|
17
|
+
<div class="asc-unit-toggle">
|
|
18
|
+
<button type="button" class="asc-unit-btn asc-unit-active" data-unit="metric">{ui.unitM}</button>
|
|
19
|
+
<button type="button" class="asc-unit-btn" data-unit="imperial">{ui.unitFt}</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="asc-presets-bar">
|
|
24
|
+
<button type="button" class="asc-preset-chip" data-preset="calm">{ui.presetProtected}</button>
|
|
25
|
+
<button type="button" class="asc-preset-chip asc-preset-active" data-preset="moderate">{ui.presetOpen}</button>
|
|
26
|
+
<button type="button" class="asc-preset-chip" data-preset="storm">{ui.presetStorm}</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="asc-inputs-group">
|
|
30
|
+
<div class="asc-input-row">
|
|
31
|
+
<div class="asc-input-block">
|
|
32
|
+
<label for="sc-depth">{ui.waterDepthLabel}</label>
|
|
33
|
+
<div class="asc-unit-field">
|
|
34
|
+
<input type="number" id="sc-depth" class="sc-num-input" min="0.5" step="0.5" />
|
|
35
|
+
<span class="asc-field-unit">{ui.unitM}</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="asc-input-block">
|
|
39
|
+
<label for="sc-bow">{ui.bowHeightLabel}</label>
|
|
40
|
+
<div class="asc-unit-field">
|
|
41
|
+
<input type="number" id="sc-bow" class="sc-num-input" min="0" step="0.1" />
|
|
42
|
+
<span class="asc-field-unit">{ui.unitM}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="asc-input-row">
|
|
48
|
+
<div class="asc-input-block">
|
|
49
|
+
<label for="sc-tide">{ui.tideRangeLabel}</label>
|
|
50
|
+
<div class="asc-unit-field">
|
|
51
|
+
<input type="number" id="sc-tide" class="sc-num-input" min="0" step="0.1" />
|
|
52
|
+
<span class="asc-field-unit">{ui.unitM}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="asc-input-block">
|
|
56
|
+
<label for="sc-boat-length">{ui.boatLengthLabel}</label>
|
|
57
|
+
<div class="asc-unit-field">
|
|
58
|
+
<input type="number" id="sc-boat-length" class="sc-num-input" min="1" step="0.5" />
|
|
59
|
+
<span class="asc-field-unit">{ui.unitM}</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="asc-input-block">
|
|
65
|
+
<label>{ui.rodeTypeLabel}</label>
|
|
66
|
+
<div class="asc-custom-select" id="sc-rode-type-select" data-id="sc-rode-type" data-value="all-chain">
|
|
67
|
+
<button type="button" class="asc-select-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
68
|
+
<span class="asc-select-label">{ui.allChainOption}</span>
|
|
69
|
+
<svg class="asc-select-chevron" viewBox="0 0 20 20" fill="currentColor">
|
|
70
|
+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
<div class="asc-select-dropdown" role="listbox">
|
|
74
|
+
<div class="asc-option asc-option-active" data-val="all-chain" role="option">{ui.allChainOption}</div>
|
|
75
|
+
<div class="asc-option" data-val="rope-chain" role="option">{ui.ropeChainOption}</div>
|
|
76
|
+
<div class="asc-option" data-val="all-rope" role="option">{ui.allRopeOption}</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="asc-input-block">
|
|
82
|
+
<label>{ui.windConditionLabel}</label>
|
|
83
|
+
<div class="asc-custom-select" id="sc-wind-select" data-id="sc-wind" data-value="moderate">
|
|
84
|
+
<button type="button" class="asc-select-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
85
|
+
<span class="asc-select-label">{ui.moderateWindOption}</span>
|
|
86
|
+
<svg class="asc-select-chevron" viewBox="0 0 20 20" fill="currentColor">
|
|
87
|
+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
88
|
+
</svg>
|
|
89
|
+
</button>
|
|
90
|
+
<div class="asc-select-dropdown" role="listbox">
|
|
91
|
+
<div class="asc-option" data-val="calm" role="option">{ui.calmWindOption}</div>
|
|
92
|
+
<div class="asc-option asc-option-active" data-val="moderate" role="option">{ui.moderateWindOption}</div>
|
|
93
|
+
<div class="asc-option" data-val="strong" role="option">{ui.strongWindOption}</div>
|
|
94
|
+
<div class="asc-option" data-val="storm" role="option">{ui.stormWindOption}</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="asc-input-block">
|
|
100
|
+
<label>{ui.seabedTypeLabel}</label>
|
|
101
|
+
<div class="asc-custom-select" id="sc-seabed-select" data-id="sc-seabed" data-value="sand">
|
|
102
|
+
<button type="button" class="asc-select-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
103
|
+
<span class="asc-select-label">{ui.sandOption}</span>
|
|
104
|
+
<svg class="asc-select-chevron" viewBox="0 0 20 20" fill="currentColor">
|
|
105
|
+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
106
|
+
</svg>
|
|
107
|
+
</button>
|
|
108
|
+
<div class="asc-select-dropdown" role="listbox">
|
|
109
|
+
<div class="asc-option asc-option-active" data-val="sand" role="option">{ui.sandOption}</div>
|
|
110
|
+
<div class="asc-option" data-val="mud" role="option">{ui.mudOption}</div>
|
|
111
|
+
<div class="asc-option" data-val="clay" role="option">{ui.clayOption}</div>
|
|
112
|
+
<div class="asc-option" data-val="gravel" role="option">{ui.gravelOption}</div>
|
|
113
|
+
<div class="asc-option" data-val="rock" role="option">{ui.rockOption}</div>
|
|
114
|
+
<div class="asc-option" data-val="weed" role="option">{ui.weedOption}</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<button type="button" id="sc-reset-btn" class="asc-reset-btn">{ui.resetButton}</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="asc-content">
|
|
124
|
+
<div class="asc-metrics-grid">
|
|
125
|
+
<div class="asc-metric-card asc-metric-highlight">
|
|
126
|
+
<span class="asc-metric-lbl">{ui.recommendedRodeLabel}</span>
|
|
127
|
+
<span class="asc-metric-val" id="sc-rode-length-val">-</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="asc-metric-card">
|
|
130
|
+
<span class="asc-metric-lbl">{ui.scopeRatioLabel}</span>
|
|
131
|
+
<span class="asc-metric-val" id="sc-scope-val">-</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="asc-metric-card">
|
|
134
|
+
<span class="asc-metric-lbl">{ui.swingRadiusLabel}</span>
|
|
135
|
+
<span class="asc-metric-val" id="sc-swing-radius-val">-</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="asc-metric-card">
|
|
138
|
+
<span class="asc-metric-lbl">{ui.totalDepthLabel}</span>
|
|
139
|
+
<span class="asc-metric-val" id="sc-total-depth-val">-</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="asc-metric-card">
|
|
142
|
+
<span class="asc-metric-lbl">{ui.minSafeRodeLabel}</span>
|
|
143
|
+
<span class="asc-metric-val" id="sc-min-rode-val">-</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="asc-metric-card">
|
|
146
|
+
<span class="asc-metric-lbl">{ui.heavyWeatherRodeLabel}</span>
|
|
147
|
+
<span class="asc-metric-val" id="sc-storm-rode-val">-</span>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="asc-status-banner">
|
|
152
|
+
<div class="asc-status-tag" id="sc-status-badge">OPTIMAL</div>
|
|
153
|
+
<div class="asc-status-info">
|
|
154
|
+
<span class="asc-status-headline" id="sc-status-title"></span>
|
|
155
|
+
<span class="asc-status-subline" id="sc-status-desc"></span>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="asc-visual-stage">
|
|
160
|
+
<svg id="sc-anchor-svg" class="asc-diagram-svg" viewBox="0 0 600 240">
|
|
161
|
+
<defs>
|
|
162
|
+
<linearGradient id="asc-water-grad" x1="0" y1="0" x2="0" y2="1">
|
|
163
|
+
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.35" />
|
|
164
|
+
<stop offset="100%" stop-color="#0c4a6e" stop-opacity="0.95" />
|
|
165
|
+
</linearGradient>
|
|
166
|
+
<linearGradient id="asc-seabed-grad" x1="0" y1="0" x2="0" y2="1">
|
|
167
|
+
<stop offset="0%" stop-color="#475569" />
|
|
168
|
+
<stop offset="100%" stop-color="#1e293b" />
|
|
169
|
+
</linearGradient>
|
|
170
|
+
</defs>
|
|
171
|
+
|
|
172
|
+
<rect id="sc-tide-water" x="0" y="55" width="600" height="25" fill="#38bdf8" fill-opacity="0.25" />
|
|
173
|
+
<line x1="0" y1="80" x2="600" y2="80" stroke="#38bdf8" stroke-dasharray="6,4" stroke-width="1.5" />
|
|
174
|
+
<rect x="0" y="80" width="600" height="135" fill="url(#asc-water-grad)" />
|
|
175
|
+
|
|
176
|
+
<path d="M 0 215 Q 200 210 400 218 T 600 215 L 600 240 L 0 240 Z" fill="url(#asc-seabed-grad)" />
|
|
177
|
+
|
|
178
|
+
<line x1="550" y1="55" x2="550" y2="215" stroke="#94a3b8" stroke-dasharray="3,3" stroke-width="1.2" />
|
|
179
|
+
<text x="540" y="140" fill="#94a3b8" font-size="11" font-weight="700" text-anchor="end" id="sc-svg-depth-text">9.0m</text>
|
|
180
|
+
|
|
181
|
+
<path id="sc-catenary-line" d="M 485 75 Q 300 200 120 215 L 75 215" fill="none" stroke="#f59e0b" stroke-width="4" stroke-linecap="round" />
|
|
182
|
+
|
|
183
|
+
<g id="sc-anchor-group" transform="translate(60, 202)">
|
|
184
|
+
<circle cx="15" cy="8" r="4" fill="#f8fafc" />
|
|
185
|
+
<path d="M 15 8 L 15 24 M 5 18 Q 15 30 25 18" fill="none" stroke="#f8fafc" stroke-width="3" stroke-linecap="round" />
|
|
186
|
+
<path d="M 0 24 Q 15 18 30 24" fill="none" stroke="#64748b" stroke-width="2" />
|
|
187
|
+
</g>
|
|
188
|
+
|
|
189
|
+
<g id="sc-boat-group" transform="translate(425, 48)">
|
|
190
|
+
<path d="M 15 32 L 75 32 L 88 18 L 0 18 Z" fill="#f8fafc" />
|
|
191
|
+
<rect x="25" y="8" width="28" height="11" rx="2" fill="#0284c7" />
|
|
192
|
+
<line x1="45" y1="8" x2="45" y2="0" stroke="#f8fafc" stroke-width="2" />
|
|
193
|
+
<circle cx="85" cy="20" r="3.5" fill="#f59e0b" />
|
|
194
|
+
</g>
|
|
195
|
+
</svg>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="asc-advice-row">
|
|
199
|
+
<div class="asc-advice-card">
|
|
200
|
+
<span class="asc-advice-tag">{ui.seabedTag}</span>
|
|
201
|
+
<p class="asc-advice-text" id="sc-seabed-advice"></p>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="asc-advice-card">
|
|
204
|
+
<span class="asc-advice-tag">{ui.catenaryTag}</span>
|
|
205
|
+
<p class="asc-advice-text" id="sc-catenary-advice"></p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
<script>
|
|
213
|
+
import { AnchorScopeController } from './controller';
|
|
214
|
+
|
|
215
|
+
const root = document.getElementById('anchor-scope-calculator');
|
|
216
|
+
if (root) {
|
|
217
|
+
new AnchorScopeController(root);
|
|
218
|
+
}
|
|
219
|
+
</script>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RodeType, WindCondition, SeabedType, AnchorInputs } from './types';
|
|
2
|
+
|
|
3
|
+
export const BASE_SCOPE_RATIOS: Record<RodeType, Record<WindCondition, number>> = {
|
|
4
|
+
'all-chain': {
|
|
5
|
+
calm: 4,
|
|
6
|
+
moderate: 5,
|
|
7
|
+
strong: 7,
|
|
8
|
+
storm: 8,
|
|
9
|
+
},
|
|
10
|
+
'rope-chain': {
|
|
11
|
+
calm: 5,
|
|
12
|
+
moderate: 7,
|
|
13
|
+
strong: 8,
|
|
14
|
+
storm: 10,
|
|
15
|
+
},
|
|
16
|
+
'all-rope': {
|
|
17
|
+
calm: 7,
|
|
18
|
+
moderate: 8,
|
|
19
|
+
strong: 10,
|
|
20
|
+
storm: 12,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const SEABED_MODIFIERS: Record<SeabedType, number> = {
|
|
25
|
+
sand: 1.0,
|
|
26
|
+
mud: 1.1,
|
|
27
|
+
clay: 1.0,
|
|
28
|
+
gravel: 1.25,
|
|
29
|
+
rock: 1.5,
|
|
30
|
+
weed: 1.4,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const MIN_SAFE_SCOPE_RATIOS: Record<RodeType, number> = {
|
|
34
|
+
'all-chain': 4,
|
|
35
|
+
'rope-chain': 6,
|
|
36
|
+
'all-rope': 7,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_INPUTS: AnchorInputs = {
|
|
40
|
+
waterDepth: 6,
|
|
41
|
+
bowHeight: 1.5,
|
|
42
|
+
tideRange: 1.5,
|
|
43
|
+
boatLength: 10,
|
|
44
|
+
rodeType: 'all-chain',
|
|
45
|
+
windCondition: 'moderate',
|
|
46
|
+
seabedType: 'sand',
|
|
47
|
+
unitSystem: 'metric',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const FEET_TO_METERS = 0.3048;
|
|
51
|
+
export const METERS_TO_FEET = 3.28084;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { AnchorInputs, UnitSystem, RodeType, WindCondition, SeabedType } from './types';
|
|
2
|
+
import type { AnchorScopeUI } from './ui';
|
|
3
|
+
import { DEFAULT_INPUTS, FEET_TO_METERS, METERS_TO_FEET } from './constants';
|
|
4
|
+
import { computeAnchorCalculations } from './logic';
|
|
5
|
+
import { evaluateAnchorSecurity } from './evaluator';
|
|
6
|
+
import { loadAnchorInputs, saveAnchorInputs } from './storage';
|
|
7
|
+
import { getPresetInputs } from './presets';
|
|
8
|
+
import {
|
|
9
|
+
updateMetricDisplay,
|
|
10
|
+
updateStatusBanner,
|
|
11
|
+
updateAdviceCards,
|
|
12
|
+
renderVisualProfile,
|
|
13
|
+
} from './dom-views';
|
|
14
|
+
|
|
15
|
+
export class AnchorScopeController {
|
|
16
|
+
private inputs: AnchorInputs;
|
|
17
|
+
private ui: AnchorScopeUI;
|
|
18
|
+
private root: HTMLElement;
|
|
19
|
+
|
|
20
|
+
constructor(root: HTMLElement) {
|
|
21
|
+
this.root = root;
|
|
22
|
+
this.ui = this.parseUiData();
|
|
23
|
+
this.inputs = loadAnchorInputs();
|
|
24
|
+
this.initElements();
|
|
25
|
+
this.attachEvents();
|
|
26
|
+
this.recalculate();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private parseUiData(): AnchorScopeUI {
|
|
30
|
+
try {
|
|
31
|
+
const dataStr = this.root.getAttribute('data-i18n') ?? '{}';
|
|
32
|
+
return JSON.parse(dataStr) as AnchorScopeUI;
|
|
33
|
+
} catch {
|
|
34
|
+
return {} as AnchorScopeUI;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private initElements(): void {
|
|
39
|
+
this.setInputValue('#sc-depth', this.getDisplayValue(this.inputs.waterDepth));
|
|
40
|
+
this.setInputValue('#sc-bow', this.getDisplayValue(this.inputs.bowHeight));
|
|
41
|
+
this.setInputValue('#sc-tide', this.getDisplayValue(this.inputs.tideRange));
|
|
42
|
+
this.setInputValue('#sc-boat-length', this.getDisplayValue(this.inputs.boatLength));
|
|
43
|
+
this.setCustomSelectValue('#sc-rode-type-select', this.inputs.rodeType);
|
|
44
|
+
this.setCustomSelectValue('#sc-wind-select', this.inputs.windCondition);
|
|
45
|
+
this.setCustomSelectValue('#sc-seabed-select', this.inputs.seabedType);
|
|
46
|
+
this.updateUnitButtons();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private attachEvents(): void {
|
|
50
|
+
this.root.querySelectorAll<HTMLInputElement>('.sc-num-input').forEach((input) => {
|
|
51
|
+
input.addEventListener('input', () => this.handleNumericInput());
|
|
52
|
+
});
|
|
53
|
+
this.attachCustomSelectEvents();
|
|
54
|
+
this.root.querySelectorAll<HTMLButtonElement>('.asc-preset-chip').forEach((chip) => {
|
|
55
|
+
chip.addEventListener('click', () => this.applyPreset(chip.getAttribute('data-preset')));
|
|
56
|
+
});
|
|
57
|
+
this.root.querySelectorAll<HTMLButtonElement>('.asc-unit-btn, .sc-unit-btn').forEach((btn) => {
|
|
58
|
+
btn.addEventListener('click', (e) => this.handleUnitSwitch(e));
|
|
59
|
+
});
|
|
60
|
+
const resetBtn = this.root.querySelector<HTMLButtonElement>('#sc-reset-btn');
|
|
61
|
+
if (resetBtn) {
|
|
62
|
+
resetBtn.addEventListener('click', () => this.handleReset());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private applyPreset(preset: string | null): void {
|
|
67
|
+
this.inputs = getPresetInputs(preset, this.inputs);
|
|
68
|
+
this.updateActivePresetChip(preset);
|
|
69
|
+
this.initElements();
|
|
70
|
+
saveAnchorInputs(this.inputs);
|
|
71
|
+
this.recalculate();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private updateActivePresetChip(preset: string | null): void {
|
|
75
|
+
this.root.querySelectorAll<HTMLButtonElement>('.asc-preset-chip').forEach((chip) => {
|
|
76
|
+
const isActive = chip.getAttribute('data-preset') === preset;
|
|
77
|
+
chip.classList.toggle('asc-preset-active', isActive);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private attachCustomSelectEvents(): void {
|
|
82
|
+
this.root.querySelectorAll<HTMLElement>('.asc-custom-select').forEach((sel) => {
|
|
83
|
+
const trigger = sel.querySelector<HTMLButtonElement>('.asc-select-trigger');
|
|
84
|
+
trigger?.addEventListener('click', (e) => {
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
this.toggleCustomSelect(sel);
|
|
87
|
+
});
|
|
88
|
+
sel.querySelectorAll<HTMLElement>('.asc-option').forEach((opt) => {
|
|
89
|
+
opt.addEventListener('click', (e) => {
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
this.handleOptionSelect(sel, opt);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
document.addEventListener('click', () => this.closeAllCustomSelects());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private toggleCustomSelect(sel: HTMLElement): void {
|
|
99
|
+
const wasOpen = sel.classList.contains('asc-select-open');
|
|
100
|
+
this.closeAllCustomSelects();
|
|
101
|
+
if (!wasOpen) {
|
|
102
|
+
sel.classList.add('asc-select-open');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private closeAllCustomSelects(): void {
|
|
107
|
+
this.root.querySelectorAll<HTMLElement>('.asc-custom-select').forEach((s) => {
|
|
108
|
+
s.classList.remove('asc-select-open');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private handleOptionSelect(sel: HTMLElement, opt: HTMLElement): void {
|
|
113
|
+
const val = opt.getAttribute('data-val') ?? '';
|
|
114
|
+
const id = sel.getAttribute('data-id') ?? '';
|
|
115
|
+
this.setCustomSelectValue(`#${sel.id}`, val);
|
|
116
|
+
sel.classList.remove('asc-select-open');
|
|
117
|
+
this.applySelectValue(id, val);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private applySelectValue(id: string, val: string): void {
|
|
121
|
+
if (id === 'sc-rode-type') {
|
|
122
|
+
this.inputs.rodeType = val as RodeType;
|
|
123
|
+
} else if (id === 'sc-wind') {
|
|
124
|
+
this.inputs.windCondition = val as WindCondition;
|
|
125
|
+
} else if (id === 'sc-seabed') {
|
|
126
|
+
this.inputs.seabedType = val as SeabedType;
|
|
127
|
+
}
|
|
128
|
+
saveAnchorInputs(this.inputs);
|
|
129
|
+
this.recalculate();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private handleNumericInput(): void {
|
|
133
|
+
this.inputs.waterDepth = this.getStoredValue('#sc-depth');
|
|
134
|
+
this.inputs.bowHeight = this.getStoredValue('#sc-bow');
|
|
135
|
+
this.inputs.tideRange = this.getStoredValue('#sc-tide');
|
|
136
|
+
this.inputs.boatLength = this.getStoredValue('#sc-boat-length');
|
|
137
|
+
saveAnchorInputs(this.inputs);
|
|
138
|
+
this.recalculate();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private handleUnitSwitch(e: Event): void {
|
|
142
|
+
const target = e.currentTarget as HTMLButtonElement;
|
|
143
|
+
const unit = target.getAttribute('data-unit') as UnitSystem;
|
|
144
|
+
if (unit && unit !== this.inputs.unitSystem) {
|
|
145
|
+
this.inputs.unitSystem = unit;
|
|
146
|
+
this.updateUnitButtons();
|
|
147
|
+
this.initElements();
|
|
148
|
+
saveAnchorInputs(this.inputs);
|
|
149
|
+
this.recalculate();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private handleReset(): void {
|
|
154
|
+
this.inputs = { ...DEFAULT_INPUTS };
|
|
155
|
+
this.initElements();
|
|
156
|
+
this.updateActivePresetChip('moderate');
|
|
157
|
+
saveAnchorInputs(this.inputs);
|
|
158
|
+
this.recalculate();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private recalculate(): void {
|
|
162
|
+
const result = computeAnchorCalculations(this.inputs);
|
|
163
|
+
const report = evaluateAnchorSecurity(this.inputs, result);
|
|
164
|
+
const unitLabel = this.inputs.unitSystem === 'metric' ? this.ui.unitM : this.ui.unitFt;
|
|
165
|
+
const mult = this.inputs.unitSystem === 'metric' ? 1 : METERS_TO_FEET;
|
|
166
|
+
|
|
167
|
+
this.updateMetrics(result, unitLabel, mult);
|
|
168
|
+
this.updateStatusAndAdvice(report, result);
|
|
169
|
+
renderVisualProfile(this.root.querySelector<SVGSVGElement>('#sc-anchor-svg'), this.inputs, result);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private updateMetrics(result: ReturnType<typeof computeAnchorCalculations>, unit: string, mult: number): void {
|
|
173
|
+
const format = (v: number) => (v * mult).toFixed(1);
|
|
174
|
+
updateMetricDisplay(this.root.querySelector('#sc-scope-val'), `${result.effectiveScopeRatio}:1`);
|
|
175
|
+
updateMetricDisplay(this.root.querySelector('#sc-total-depth-val'), format(result.totalDepth), unit);
|
|
176
|
+
updateMetricDisplay(this.root.querySelector('#sc-rode-length-val'), format(result.rodeLength), unit);
|
|
177
|
+
updateMetricDisplay(this.root.querySelector('#sc-swing-radius-val'), format(result.swingRadius), unit);
|
|
178
|
+
updateMetricDisplay(this.root.querySelector('#sc-min-rode-val'), format(result.minRecommendedRode), unit);
|
|
179
|
+
updateMetricDisplay(this.root.querySelector('#sc-storm-rode-val'), format(result.heavyWeatherRode), unit);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private updateStatusAndAdvice(report: ReturnType<typeof evaluateAnchorSecurity>, result: ReturnType<typeof computeAnchorCalculations>): void {
|
|
183
|
+
updateStatusBanner(
|
|
184
|
+
{
|
|
185
|
+
badge: this.root.querySelector('#sc-status-badge'),
|
|
186
|
+
title: this.root.querySelector('#sc-status-title'),
|
|
187
|
+
desc: this.root.querySelector('#sc-status-desc'),
|
|
188
|
+
},
|
|
189
|
+
report,
|
|
190
|
+
result,
|
|
191
|
+
this.ui,
|
|
192
|
+
);
|
|
193
|
+
updateAdviceCards(
|
|
194
|
+
this.root.querySelector('#sc-seabed-advice'),
|
|
195
|
+
this.root.querySelector('#sc-catenary-advice'),
|
|
196
|
+
report,
|
|
197
|
+
this.ui,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private getDisplayValue(meters: number): string {
|
|
202
|
+
if (this.inputs.unitSystem === 'imperial') {
|
|
203
|
+
return (meters * METERS_TO_FEET).toFixed(1);
|
|
204
|
+
}
|
|
205
|
+
return meters.toString();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private getStoredValue(selector: string): number {
|
|
209
|
+
const el = this.root.querySelector<HTMLInputElement>(selector);
|
|
210
|
+
const val = parseFloat(el?.value || '0');
|
|
211
|
+
if (this.inputs.unitSystem === 'imperial') {
|
|
212
|
+
return val * FEET_TO_METERS;
|
|
213
|
+
}
|
|
214
|
+
return val;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private setInputValue(selector: string, val: string): void {
|
|
218
|
+
const el = this.root.querySelector<HTMLInputElement>(selector);
|
|
219
|
+
if (el) {
|
|
220
|
+
el.value = val;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private setCustomSelectValue(selector: string, val: string): void {
|
|
225
|
+
const sel = this.root.querySelector<HTMLElement>(selector);
|
|
226
|
+
if (!sel) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
sel.setAttribute('data-value', val);
|
|
230
|
+
const labelEl = sel.querySelector<HTMLElement>('.asc-select-label');
|
|
231
|
+
sel.querySelectorAll<HTMLElement>('.asc-option').forEach((opt) => {
|
|
232
|
+
const isSelected = opt.getAttribute('data-val') === val;
|
|
233
|
+
opt.classList.toggle('asc-option-active', isSelected);
|
|
234
|
+
if (isSelected && labelEl) {
|
|
235
|
+
labelEl.textContent = opt.textContent;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private updateUnitButtons(): void {
|
|
241
|
+
const unitLabel = this.inputs.unitSystem === 'metric' ? this.ui.unitM : this.ui.unitFt;
|
|
242
|
+
this.root.querySelectorAll<HTMLButtonElement>('.asc-unit-btn, .sc-unit-btn').forEach((btn) => {
|
|
243
|
+
const isActive = btn.getAttribute('data-unit') === this.inputs.unitSystem;
|
|
244
|
+
btn.classList.toggle('asc-unit-active', isActive);
|
|
245
|
+
btn.classList.toggle('sc-unit-active', isActive);
|
|
246
|
+
});
|
|
247
|
+
this.root.querySelectorAll<HTMLElement>('.asc-field-unit').forEach((el) => {
|
|
248
|
+
el.textContent = unitLabel;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { AnchorCalculationResult, AnchorInputs, CatenaryPoint, RodeType } from './types';
|
|
2
|
+
import type { EvaluationReport } from './evaluator';
|
|
3
|
+
import type { AnchorScopeUI } from './ui';
|
|
4
|
+
import { generateCatenaryCurve } from './logic';
|
|
5
|
+
|
|
6
|
+
export interface StatusBannerElements {
|
|
7
|
+
badge: HTMLElement | null;
|
|
8
|
+
title: HTMLElement | null;
|
|
9
|
+
desc: HTMLElement | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function updateMetricDisplay(
|
|
13
|
+
el: HTMLElement | null,
|
|
14
|
+
value: string | number,
|
|
15
|
+
unit = '',
|
|
16
|
+
): void {
|
|
17
|
+
if (!el) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
el.textContent = `${value}${unit ? ` ${unit}` : ''}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function updateStatusBanner(
|
|
24
|
+
elements: StatusBannerElements,
|
|
25
|
+
report: EvaluationReport,
|
|
26
|
+
result: AnchorCalculationResult,
|
|
27
|
+
ui: AnchorScopeUI,
|
|
28
|
+
): void {
|
|
29
|
+
if (elements.badge) {
|
|
30
|
+
elements.badge.setAttribute('class', `asc-status-tag sc-status-${result.safetyLevel}`);
|
|
31
|
+
elements.badge.textContent = result.safetyLevel.toUpperCase();
|
|
32
|
+
}
|
|
33
|
+
if (elements.title) {
|
|
34
|
+
elements.title.textContent = ui[report.statusTitleKey] ?? '';
|
|
35
|
+
}
|
|
36
|
+
if (elements.desc) {
|
|
37
|
+
elements.desc.textContent = ui[report.statusDescKey] ?? '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function updateAdviceCards(
|
|
42
|
+
seabedCardEl: HTMLElement | null,
|
|
43
|
+
catenaryCardEl: HTMLElement | null,
|
|
44
|
+
report: EvaluationReport,
|
|
45
|
+
ui: AnchorScopeUI,
|
|
46
|
+
): void {
|
|
47
|
+
if (seabedCardEl) {
|
|
48
|
+
seabedCardEl.textContent = ui[report.holdingAdviceKey] ?? '';
|
|
49
|
+
}
|
|
50
|
+
if (catenaryCardEl) {
|
|
51
|
+
catenaryCardEl.textContent = ui[report.catenaryEffectKey] ?? '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildCatenarySvgPath(start: CatenaryPoint, end: CatenaryPoint, sag: number): string {
|
|
56
|
+
const seabedTouch = { x: 120, y: 215 };
|
|
57
|
+
const points = generateCatenaryCurve({ start, end: seabedTouch, sagFactor: sag, pointsCount: 16 });
|
|
58
|
+
if (points.length === 0) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
let path = `M ${points[0]?.x.toFixed(1)} ${points[0]?.y.toFixed(1)}`;
|
|
62
|
+
for (let i = 1; i < points.length; i++) {
|
|
63
|
+
path += ` L ${points[i]?.x.toFixed(1)} ${points[i]?.y.toFixed(1)}`;
|
|
64
|
+
}
|
|
65
|
+
path += ` L ${end.x.toFixed(1)} ${end.y.toFixed(1)}`;
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getSagForRode(rode: RodeType): number {
|
|
70
|
+
if (rode === 'all-chain') {
|
|
71
|
+
return 38;
|
|
72
|
+
}
|
|
73
|
+
if (rode === 'rope-chain') {
|
|
74
|
+
return 20;
|
|
75
|
+
}
|
|
76
|
+
return 6;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderVisualProfile(
|
|
80
|
+
svgEl: SVGSVGElement | null,
|
|
81
|
+
inputs: AnchorInputs,
|
|
82
|
+
result: AnchorCalculationResult,
|
|
83
|
+
): void {
|
|
84
|
+
if (!svgEl) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const start = { x: 485, y: 75 };
|
|
88
|
+
const end = { x: 75, y: 215 };
|
|
89
|
+
const sag = getSagForRode(inputs.rodeType);
|
|
90
|
+
|
|
91
|
+
const rodePathEl = svgEl.querySelector<SVGPathElement>('#sc-catenary-line');
|
|
92
|
+
const depthTextEl = svgEl.querySelector<SVGTextElement>('#sc-svg-depth-text');
|
|
93
|
+
const tideRectEl = svgEl.querySelector<SVGRectElement>('#sc-tide-water');
|
|
94
|
+
|
|
95
|
+
if (rodePathEl) {
|
|
96
|
+
rodePathEl.setAttribute('d', buildCatenarySvgPath(start, end, sag));
|
|
97
|
+
}
|
|
98
|
+
if (depthTextEl) {
|
|
99
|
+
depthTextEl.textContent = `${result.totalDepth}m`;
|
|
100
|
+
}
|
|
101
|
+
if (tideRectEl) {
|
|
102
|
+
const tideH = Math.min(25, Math.max(5, inputs.tideRange * 6));
|
|
103
|
+
tideRectEl.setAttribute('height', tideH.toString());
|
|
104
|
+
tideRectEl.setAttribute('y', (80 - tideH).toString());
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NauticalToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { AnchorScopeUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type AnchorScopeLocaleContent = ToolLocaleContent<AnchorScopeUI>;
|
|
5
|
+
|
|
6
|
+
export const anchorScope: NauticalToolEntry<AnchorScopeUI> = {
|
|
7
|
+
id: 'anchor-scope',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:compass-outline',
|
|
10
|
+
fg: 'mdi:anchor',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
14
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
15
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
16
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
17
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
18
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
19
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
20
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
21
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
22
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
23
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
24
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
25
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
26
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
27
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { bibliography } from './bibliography';
|