@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.
Files changed (45) hide show
  1. package/package.json +2 -2
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +9 -7
  5. package/src/tests/diacritics_density.test.ts +1 -1
  6. package/src/tests/inverted_punctuation.test.ts +1 -1
  7. package/src/tests/locale_completeness.test.ts +2 -2
  8. package/src/tests/script_density.test.ts +1 -1
  9. package/src/tests/title_quality.test.ts +1 -1
  10. package/src/tests/tool_validation.test.ts +2 -2
  11. package/src/tool/anchorScope/anchorScope.css +507 -0
  12. package/src/tool/anchorScope/bibliography.astro +6 -0
  13. package/src/tool/anchorScope/bibliography.ts +14 -0
  14. package/src/tool/anchorScope/component.astro +219 -0
  15. package/src/tool/anchorScope/constants.ts +51 -0
  16. package/src/tool/anchorScope/controller.ts +251 -0
  17. package/src/tool/anchorScope/dom-views.ts +106 -0
  18. package/src/tool/anchorScope/entry.ts +31 -0
  19. package/src/tool/anchorScope/evaluator.ts +61 -0
  20. package/src/tool/anchorScope/i18n/de.ts +201 -0
  21. package/src/tool/anchorScope/i18n/en.ts +223 -0
  22. package/src/tool/anchorScope/i18n/es.ts +223 -0
  23. package/src/tool/anchorScope/i18n/fr.ts +201 -0
  24. package/src/tool/anchorScope/i18n/id.ts +201 -0
  25. package/src/tool/anchorScope/i18n/it.ts +201 -0
  26. package/src/tool/anchorScope/i18n/ja.ts +201 -0
  27. package/src/tool/anchorScope/i18n/ko.ts +201 -0
  28. package/src/tool/anchorScope/i18n/nl.ts +201 -0
  29. package/src/tool/anchorScope/i18n/pl.ts +201 -0
  30. package/src/tool/anchorScope/i18n/pt.ts +201 -0
  31. package/src/tool/anchorScope/i18n/ru.ts +201 -0
  32. package/src/tool/anchorScope/i18n/sv.ts +201 -0
  33. package/src/tool/anchorScope/i18n/tr.ts +201 -0
  34. package/src/tool/anchorScope/i18n/zh.ts +201 -0
  35. package/src/tool/anchorScope/index.ts +10 -0
  36. package/src/tool/anchorScope/logic.test.ts +100 -0
  37. package/src/tool/anchorScope/logic.ts +84 -0
  38. package/src/tool/anchorScope/presets.ts +12 -0
  39. package/src/tool/anchorScope/seo.astro +14 -0
  40. package/src/tool/anchorScope/storage.ts +25 -0
  41. package/src/tool/anchorScope/types.ts +33 -0
  42. package/src/tool/anchorScope/ui.ts +63 -0
  43. package/src/tool/endurance/index.ts +2 -1
  44. package/src/tools.ts +3 -0
  45. package/src/types.ts +3 -1
@@ -0,0 +1,201 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { AnchorScopeLocaleContent } from '../entry';
4
+ import type { AnchorScopeUI } from '../ui';
5
+
6
+ const slug = 'anchor-scope-calculator';
7
+ const title = '锚链长度及回旋半径计算器';
8
+ const description =
9
+ '根据海图水深、船首干舷、潮高差及海底底质,精确计算安全锚泊所需的最佳锚链长度、出链比和船体回旋半径。';
10
+
11
+ const ui: AnchorScopeUI = {
12
+ parametersTitle: '参数设定',
13
+ waterDepthLabel: '海图水深',
14
+ bowHeightLabel: '船首干舷高度',
15
+ tideRangeLabel: '潮位上涨幅度',
16
+ boatLengthLabel: '船体全长 (LOA)',
17
+ rodeTypeLabel: '锚链绳缆类型',
18
+ windConditionLabel: '风力与天气预报',
19
+ seabedTypeLabel: '海底底质类型',
20
+ presetProtected: '避风海湾',
21
+ presetOpen: '开阔海域',
22
+ presetStorm: '大风风暴预警',
23
+ seabedTag: '底质抓力系数',
24
+ catenaryTag: '悬链线缓冲效应',
25
+ unitM: 'm',
26
+ unitFt: 'ft',
27
+ unitMeters: '公制 (m)',
28
+ unitFeet: '英制 (ft)',
29
+ allChainOption: '全铁链 (重悬链线)',
30
+ ropeChainOption: '缆绳与铁链组合',
31
+ allRopeOption: '全尼龙缆绳',
32
+ calmWindOption: '平静 / 微风 (< 15 节)',
33
+ moderateWindOption: '和风 (15 - 25 节)',
34
+ strongWindOption: '强风 / 疾风 (25 - 35 节)',
35
+ stormWindOption: '暴风雨 (> 35 节)',
36
+ sandOption: '沙底 (最佳抓力)',
37
+ mudOption: '软泥 / 淤泥 (良好抓力)',
38
+ clayOption: '硬粘土 (极佳抓力)',
39
+ gravelOption: '砾石 / 碎石 (一般)',
40
+ rockOption: '礁石 (卡锚风险)',
41
+ weedOption: '海草 / 藻类 (抓力较差)',
42
+ resultsTitle: '锚泊分析与尺寸',
43
+ scopeRatioLabel: '有效出链比 (Scope)',
44
+ totalDepthLabel: '总垂直高度',
45
+ recommendedRodeLabel: '建议抛锚出链长度',
46
+ horizontalDistanceLabel: '水平延伸距离',
47
+ swingRadiusLabel: '预估回旋半径',
48
+ minSafeRodeLabel: '平稳时最小长度',
49
+ heavyWeatherRodeLabel: '恶劣天气出链长',
50
+ statusOptimalTitle: '锚泊安全稳固',
51
+ statusOptimalDesc: '计算得出的出链长度可形成良好的悬链线缓冲,确保对锚柄施加水平拉力。',
52
+ statusCautionTitle: '适度范围: 密切关注风力',
53
+ statusCautionDesc: '适用于中等风力。如果阵风或涌浪加剧,请及时放长锚链。',
54
+ statusDangerTitle: '出链不足: 走锚风险',
55
+ statusDangerDesc: '出链比过低。锚柄将被向上提起,极易从海底脱出发生走锚。',
56
+ adviceSand: '沙底能够让锚爪深度抓入。请挂倒挡缓慢倒车以紧实锚泊。',
57
+ adviceMud: '松软淤泥需要宽大锚爪和更长的锚链以接触底层硬土。',
58
+ adviceClay: '硬粘土抓力强劲,但需要锋利的锚尖以便刺入。',
59
+ adviceGravel: '碎石在波浪冲击下易滑动。建议将出链长度增加至少25%。',
60
+ adviceRock: '极易卡死锚具。请务必系结带浮标的解脱绳。',
61
+ adviceWeed: '茂密海草阻碍锚爪切入。请用倒车动力仔细核验抓力。',
62
+ catenaryAllChain: '锚链自重形成的悬链线可吸收波浪冲击,并保持水平拉拽方向。',
63
+ catenaryRopeChain: '混合缆绳需保持至少7比1的出链比以弥补自重不足。',
64
+ catenaryAllRope: '轻质尼龙绳需达到8比1以上,防止波浪将锚柄直接拔起。',
65
+ resetButton: '重置参数',
66
+ visualProfileTitle: '悬链线轮廓图',
67
+ waterlineLabel: '吃水线',
68
+ seabedLabel: '海底面',
69
+ anchorLabel: '船锚',
70
+ catenaryCurveLabel: '悬链线',
71
+ swingCircleLabel: '回旋圆周',
72
+ };
73
+
74
+ const faq: AnchorScopeLocaleContent['faq'] = [
75
+ {
76
+ question: '什么是锚泊中的出链比 (Scope)?',
77
+ answer: '出链比是指放入水中的锚链或缆绳总长度与海底到船首锚链滚轮总垂直高度之间的数学比例。',
78
+ },
79
+ {
80
+ question: '为何必须将船首干舷与潮差计入总深度?',
81
+ answer: '测深仪仅显示龙骨下深度。船首滚轮高出水面的距离以及夜间高潮水位会使实际垂直高度大幅增加。',
82
+ },
83
+ {
84
+ question: '全铁链与缆绳在出链比上有何区别?',
85
+ answer: '重质铁链依靠自重下垂形成悬链线,4:1至5:1即可;轻质缆绳绷直受力,需7:1至10:1。',
86
+ },
87
+ {
88
+ question: '如何计算船舶的锚泊回旋半径?',
89
+ answer: '利用勾股定理求出锚链水平跨度,加上船体全长以及3至5米的安全余量即可得出。',
90
+ },
91
+ {
92
+ question: '海底底质对抓力有何影响?',
93
+ answer: '沙底和粘土提供最高抓力;海草与碎石底质需要加长出链并用倒车动力测试。',
94
+ },
95
+ {
96
+ question: '锚链减震止动绳 (Snubber) 的作用是什么?',
97
+ answer: '吸收涌浪对电动起锚机的冲击负荷,并消除锚链传导到舱内的金属震动噪音。',
98
+ },
99
+ ];
100
+
101
+ const howTo: AnchorScopeLocaleContent['howTo'] = [
102
+ {
103
+ name: '测量海图水深与船首高度',
104
+ text: '确认锚泊点水深,并加上船首导缆轮距离水面的垂直高度。',
105
+ },
106
+ {
107
+ name: '计入潮位上涨幅度',
108
+ text: '查阅潮汐表,将停泊期间预计的最大高潮上涨值加入计算。',
109
+ },
110
+ {
111
+ name: '选择索具类型与预报风力',
112
+ text: '选择全链或组合缆绳,并根据预报风力调整安全系数。',
113
+ },
114
+ {
115
+ name: '抛锚出链并倒车紧锚',
116
+ text: '放出计算出的锚链长度,挂倒挡缓慢加油门使锚爪紧紧抓入海底。',
117
+ },
118
+ ];
119
+
120
+ const seo: AnchorScopeLocaleContent['seo'] = [
121
+ {
122
+ type: 'title',
123
+ text: '安全锚泊计算: 锚链长度与回旋半径',
124
+ level: 2,
125
+ },
126
+ {
127
+ type: 'paragraph',
128
+ html: '规范的锚泊是确保船舶与船员安全的基本技能。出链长度不足是走锚和夜间锚地碰撞的主要原因。通过计算精确的<strong>出链比</strong>,即使在大风天气下船舶也能稳固停泊。',
129
+ },
130
+ {
131
+ type: 'title',
132
+ text: '总垂直高度的构成要素',
133
+ level: 3,
134
+ },
135
+ {
136
+ type: 'list',
137
+ items: [
138
+ '<strong>锚地水深:</strong> 测深仪水深加上船舶实际吃水深度。',
139
+ '<strong>船首干舷高度:</strong> 水面至船首锚滚轮的垂直跨度。',
140
+ '<strong>最大潮汐落差:</strong> 滞泊期间满潮时的海面上升量。',
141
+ ],
142
+ },
143
+ {
144
+ type: 'title',
145
+ text: '推荐出链比基准表',
146
+ level: 3,
147
+ },
148
+ {
149
+ type: 'table',
150
+ headers: ['索具类型', '风平浪静 (<15 节)', '和风起浪 (15-25 节)', '狂风暴雨 (>30 节)'],
151
+ rows: [
152
+ ['<strong>全铁链配置</strong>', '4:1 比例', '5:1 比例', '7:1 比例'],
153
+ ['<strong>缆绳-铁链组合</strong>', '5:1 比例', '7:1 比例', '8:1 至 10:1 比例'],
154
+ ['<strong>全尼龙缆绳</strong>', '7:1 比例', '8:1 比例', '10:1 至 12:1 比例'],
155
+ ],
156
+ },
157
+ {
158
+ type: 'tip',
159
+ title: '保护电动起锚机',
160
+ html: '严禁将锚链拉力直接承受在起锚机的链轮上。请务必使用系于船首系缆桩的弹性减震绳。',
161
+ },
162
+ ];
163
+
164
+ const schemas: AnchorScopeLocaleContent['schemas'] = [
165
+ {
166
+ '@context': 'https://schema.org',
167
+ '@type': 'SoftwareApplication',
168
+ name: title,
169
+ description,
170
+ applicationCategory: 'UtilityApplication',
171
+ operatingSystem: 'Web',
172
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
173
+ } as WithContext<SoftwareApplication>,
174
+ {
175
+ '@context': 'https://schema.org',
176
+ '@type': 'FAQPage',
177
+ mainEntity: faq.map((item) => ({
178
+ '@type': 'Question',
179
+ name: item.question,
180
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
181
+ })),
182
+ } as WithContext<FAQPage>,
183
+ {
184
+ '@context': 'https://schema.org',
185
+ '@type': 'HowTo',
186
+ name: `使用指南: ${title}`,
187
+ step: howTo.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
188
+ } as WithContext<HowTo>,
189
+ ];
190
+
191
+ export const content: AnchorScopeLocaleContent = {
192
+ slug,
193
+ title,
194
+ description,
195
+ ui,
196
+ seo,
197
+ faq,
198
+ bibliography,
199
+ howTo,
200
+ schemas,
201
+ };
@@ -0,0 +1,10 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { anchorScope } from './entry';
3
+ export * from './entry';
4
+
5
+ export const ANCHOR_SCOPE_TOOL: ToolDefinition = {
6
+ entry: anchorScope,
7
+ Component: () => import('./component.astro'),
8
+ SEOComponent: () => import('./seo.astro'),
9
+ BibliographyComponent: () => import('./bibliography.astro'),
10
+ };
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ calculateTotalHeight,
4
+ getRecommendedScopeRatio,
5
+ calculateRodeLength,
6
+ calculateHorizontalDistance,
7
+ calculateSwingRadius,
8
+ evaluateSafety,
9
+ computeAnchorCalculations,
10
+ generateCatenaryCurve,
11
+ } from './logic';
12
+ import type { AnchorInputs } from './types';
13
+
14
+ describe('AnchorScope Logic Suite', () => {
15
+ const baseInputs: AnchorInputs = {
16
+ waterDepth: 5,
17
+ bowHeight: 1.5,
18
+ tideRange: 1.5,
19
+ boatLength: 10,
20
+ rodeType: 'all-chain',
21
+ windCondition: 'moderate',
22
+ seabedType: 'sand',
23
+ unitSystem: 'metric',
24
+ };
25
+
26
+ it('calculates total height correctly', () => {
27
+ expect(calculateTotalHeight(5, 1.5, 1.5)).toBe(8);
28
+ expect(calculateTotalHeight(0, 0, 0)).toBe(0);
29
+ expect(calculateTotalHeight(-2, 1, 1)).toBe(2);
30
+ });
31
+
32
+ it('computes recommended scope ratio for all-chain', () => {
33
+ const ratio = getRecommendedScopeRatio(baseInputs);
34
+ expect(ratio).toBe(5);
35
+ });
36
+
37
+ it('adjusts scope ratio for rocky seabed and storm conditions', () => {
38
+ const stormInputs: AnchorInputs = {
39
+ ...baseInputs,
40
+ rodeType: 'rope-chain',
41
+ windCondition: 'storm',
42
+ seabedType: 'rock',
43
+ };
44
+ const ratio = getRecommendedScopeRatio(stormInputs);
45
+ expect(ratio).toBe(15);
46
+ });
47
+
48
+ it('uses custom scope ratio when provided', () => {
49
+ const customInputs: AnchorInputs = {
50
+ ...baseInputs,
51
+ customScopeRatio: 6.5,
52
+ };
53
+ expect(getRecommendedScopeRatio(customInputs)).toBe(6.5);
54
+ });
55
+
56
+ it('calculates rode length correctly', () => {
57
+ expect(calculateRodeLength(8, 5)).toBe(40);
58
+ });
59
+
60
+ it('calculates horizontal distance correctly', () => {
61
+ const dist = calculateHorizontalDistance(10, 6);
62
+ expect(dist).toBe(8);
63
+ expect(calculateHorizontalDistance(5, 8)).toBe(0);
64
+ });
65
+
66
+ it('calculates swing radius with safety buffer', () => {
67
+ expect(calculateSwingRadius(20, 10)).toBe(33);
68
+ });
69
+
70
+ it('evaluates safety thresholds accurately', () => {
71
+ expect(evaluateSafety(7, 'all-chain')).toBe('optimal');
72
+ expect(evaluateSafety(4.5, 'all-chain')).toBe('caution');
73
+ expect(evaluateSafety(3, 'all-chain')).toBe('danger');
74
+ });
75
+
76
+ it('computes full calculation result object', () => {
77
+ const res = computeAnchorCalculations(baseInputs);
78
+ expect(res.totalDepth).toBe(8);
79
+ expect(res.effectiveScopeRatio).toBe(5);
80
+ expect(res.rodeLength).toBe(40);
81
+ expect(res.horizontalDistance).toBeCloseTo(39.2, 1);
82
+ expect(res.swingRadius).toBeCloseTo(52.2, 1);
83
+ expect(res.safetyLevel).toBe('caution');
84
+ expect(res.minRecommendedRode).toBe(32);
85
+ expect(res.heavyWeatherRode).toBe(64);
86
+ });
87
+
88
+ it('generates smooth catenary curve points', () => {
89
+ const points = generateCatenaryCurve({
90
+ start: { x: 0, y: 0 },
91
+ end: { x: 100, y: 100 },
92
+ sagFactor: 20,
93
+ pointsCount: 10,
94
+ });
95
+ expect(points.length).toBe(11);
96
+ expect(points[0]?.x).toBe(0);
97
+ expect(points[10]?.x).toBe(100);
98
+ expect(points[5]?.y).toBeGreaterThan(50);
99
+ });
100
+ });
@@ -0,0 +1,84 @@
1
+ import type { AnchorInputs, AnchorCalculationResult, CatenaryPoint, SafetyLevel } from './types';
2
+ import { BASE_SCOPE_RATIOS, SEABED_MODIFIERS, MIN_SAFE_SCOPE_RATIOS } from './constants';
3
+
4
+ export interface CatenaryParams {
5
+ start: CatenaryPoint;
6
+ end: CatenaryPoint;
7
+ sagFactor: number;
8
+ pointsCount?: number;
9
+ }
10
+
11
+ export function calculateTotalHeight(waterDepth: number, bowHeight: number, tideRange: number): number {
12
+ return Math.max(0, waterDepth) + Math.max(0, bowHeight) + Math.max(0, tideRange);
13
+ }
14
+
15
+ export function getRecommendedScopeRatio(inputs: AnchorInputs): number {
16
+ if (inputs.customScopeRatio && inputs.customScopeRatio > 0) {
17
+ return inputs.customScopeRatio;
18
+ }
19
+ const base = BASE_SCOPE_RATIOS[inputs.rodeType][inputs.windCondition];
20
+ const seabedFactor = SEABED_MODIFIERS[inputs.seabedType];
21
+ return Math.round(base * seabedFactor * 10) / 10;
22
+ }
23
+
24
+ export function calculateRodeLength(totalHeight: number, scopeRatio: number): number {
25
+ return Math.round(totalHeight * scopeRatio * 10) / 10;
26
+ }
27
+
28
+ export function calculateHorizontalDistance(rodeLength: number, totalHeight: number): number {
29
+ if (rodeLength <= totalHeight) {
30
+ return 0;
31
+ }
32
+ return Math.round(Math.sqrt(Math.max(0, rodeLength * rodeLength - totalHeight * totalHeight)) * 10) / 10;
33
+ }
34
+
35
+ export function calculateSwingRadius(horizontalDistance: number, boatLength: number): number {
36
+ const safetyBuffer = 3;
37
+ return Math.round((horizontalDistance + Math.max(0, boatLength) + safetyBuffer) * 10) / 10;
38
+ }
39
+
40
+ export function evaluateSafety(scopeRatio: number, rodeType: AnchorInputs['rodeType']): SafetyLevel {
41
+ const minSafe = MIN_SAFE_SCOPE_RATIOS[rodeType];
42
+ if (scopeRatio >= minSafe + 2) {
43
+ return 'optimal';
44
+ }
45
+ if (scopeRatio >= minSafe) {
46
+ return 'caution';
47
+ }
48
+ return 'danger';
49
+ }
50
+
51
+ export function computeAnchorCalculations(inputs: AnchorInputs): AnchorCalculationResult {
52
+ const totalHeight = calculateTotalHeight(inputs.waterDepth, inputs.bowHeight, inputs.tideRange);
53
+ const effectiveScopeRatio = getRecommendedScopeRatio(inputs);
54
+ const rodeLength = calculateRodeLength(totalHeight, effectiveScopeRatio);
55
+ const horizontalDistance = calculateHorizontalDistance(rodeLength, totalHeight);
56
+ const swingRadius = calculateSwingRadius(horizontalDistance, inputs.boatLength);
57
+ const safetyLevel = evaluateSafety(effectiveScopeRatio, inputs.rodeType);
58
+ const minRecommendedRode = calculateRodeLength(totalHeight, MIN_SAFE_SCOPE_RATIOS[inputs.rodeType]);
59
+ const heavyWeatherRode = calculateRodeLength(totalHeight, BASE_SCOPE_RATIOS[inputs.rodeType].storm);
60
+
61
+ return {
62
+ totalDepth: Math.round(totalHeight * 10) / 10,
63
+ effectiveScopeRatio,
64
+ rodeLength,
65
+ horizontalDistance,
66
+ swingRadius,
67
+ safetyLevel,
68
+ minRecommendedRode,
69
+ heavyWeatherRode,
70
+ };
71
+ }
72
+
73
+ export function generateCatenaryCurve(params: CatenaryParams): CatenaryPoint[] {
74
+ const count = params.pointsCount ?? 20;
75
+ const points: CatenaryPoint[] = [];
76
+ for (let i = 0; i <= count; i++) {
77
+ const t = i / count;
78
+ const x = params.start.x + (params.end.x - params.start.x) * t;
79
+ const linearY = params.start.y + (params.end.y - params.start.y) * t;
80
+ const sag = Math.sin(t * Math.PI) * params.sagFactor;
81
+ points.push({ x, y: linearY + sag });
82
+ }
83
+ return points;
84
+ }
@@ -0,0 +1,12 @@
1
+ import type { AnchorInputs } from './types';
2
+
3
+ const PRESET_MAP: Record<string, Partial<AnchorInputs>> = {
4
+ calm: { waterDepth: 4, bowHeight: 1.2, tideRange: 0.8, boatLength: 8, windCondition: 'calm', seabedType: 'sand' },
5
+ storm: { waterDepth: 8, bowHeight: 1.8, tideRange: 2.2, boatLength: 12, windCondition: 'storm', seabedType: 'mud' },
6
+ moderate: { waterDepth: 6, bowHeight: 1.5, tideRange: 1.5, boatLength: 10, windCondition: 'moderate', seabedType: 'sand' },
7
+ };
8
+
9
+ export function getPresetInputs(preset: string | null, current: AnchorInputs): AnchorInputs {
10
+ const overrides = (preset && PRESET_MAP[preset]) || PRESET_MAP.moderate;
11
+ return { ...current, ...overrides };
12
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { anchorScope } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await (anchorScope.i18n[locale] ?? anchorScope.i18n.en)?.();
12
+ ---
13
+
14
+ {content && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,25 @@
1
+ import type { AnchorInputs } from './types';
2
+ import { DEFAULT_INPUTS } from './constants';
3
+
4
+ const STORAGE_KEY = 'jjlmoya_anchor_scope_inputs';
5
+
6
+ export function saveAnchorInputs(inputs: AnchorInputs): void {
7
+ try {
8
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(inputs));
9
+ } catch {
10
+ return;
11
+ }
12
+ }
13
+
14
+ export function loadAnchorInputs(): AnchorInputs {
15
+ try {
16
+ const raw = localStorage.getItem(STORAGE_KEY);
17
+ if (!raw) {
18
+ return { ...DEFAULT_INPUTS };
19
+ }
20
+ const parsed = JSON.parse(raw) as Partial<AnchorInputs>;
21
+ return { ...DEFAULT_INPUTS, ...parsed };
22
+ } catch {
23
+ return { ...DEFAULT_INPUTS };
24
+ }
25
+ }
@@ -0,0 +1,33 @@
1
+ export type RodeType = 'all-chain' | 'rope-chain' | 'all-rope';
2
+ export type WindCondition = 'calm' | 'moderate' | 'strong' | 'storm';
3
+ export type SeabedType = 'sand' | 'mud' | 'clay' | 'gravel' | 'rock' | 'weed';
4
+ export type UnitSystem = 'metric' | 'imperial';
5
+ export type SafetyLevel = 'optimal' | 'caution' | 'danger';
6
+
7
+ export interface AnchorInputs {
8
+ waterDepth: number;
9
+ bowHeight: number;
10
+ tideRange: number;
11
+ boatLength: number;
12
+ rodeType: RodeType;
13
+ windCondition: WindCondition;
14
+ seabedType: SeabedType;
15
+ customScopeRatio?: number;
16
+ unitSystem: UnitSystem;
17
+ }
18
+
19
+ export interface AnchorCalculationResult {
20
+ totalDepth: number;
21
+ effectiveScopeRatio: number;
22
+ rodeLength: number;
23
+ horizontalDistance: number;
24
+ swingRadius: number;
25
+ safetyLevel: SafetyLevel;
26
+ minRecommendedRode: number;
27
+ heavyWeatherRode: number;
28
+ }
29
+
30
+ export interface CatenaryPoint {
31
+ x: number;
32
+ y: number;
33
+ }
@@ -0,0 +1,63 @@
1
+ export interface AnchorScopeUI {
2
+ [key: string]: string;
3
+ parametersTitle: string;
4
+ waterDepthLabel: string;
5
+ bowHeightLabel: string;
6
+ tideRangeLabel: string;
7
+ boatLengthLabel: string;
8
+ rodeTypeLabel: string;
9
+ windConditionLabel: string;
10
+ seabedTypeLabel: string;
11
+ presetProtected: string;
12
+ presetOpen: string;
13
+ presetStorm: string;
14
+ unitM: string;
15
+ unitFt: string;
16
+ unitMeters: string;
17
+ unitFeet: string;
18
+ allChainOption: string;
19
+ ropeChainOption: string;
20
+ allRopeOption: string;
21
+ calmWindOption: string;
22
+ moderateWindOption: string;
23
+ strongWindOption: string;
24
+ stormWindOption: string;
25
+ sandOption: string;
26
+ mudOption: string;
27
+ clayOption: string;
28
+ gravelOption: string;
29
+ rockOption: string;
30
+ weedOption: string;
31
+ resultsTitle: string;
32
+ scopeRatioLabel: string;
33
+ totalDepthLabel: string;
34
+ recommendedRodeLabel: string;
35
+ horizontalDistanceLabel: string;
36
+ swingRadiusLabel: string;
37
+ minSafeRodeLabel: string;
38
+ heavyWeatherRodeLabel: string;
39
+ statusOptimalTitle: string;
40
+ statusOptimalDesc: string;
41
+ statusCautionTitle: string;
42
+ statusCautionDesc: string;
43
+ statusDangerTitle: string;
44
+ statusDangerDesc: string;
45
+ adviceSand: string;
46
+ adviceMud: string;
47
+ adviceClay: string;
48
+ adviceGravel: string;
49
+ adviceRock: string;
50
+ adviceWeed: string;
51
+ catenaryAllChain: string;
52
+ catenaryRopeChain: string;
53
+ catenaryAllRope: string;
54
+ resetButton: string;
55
+ visualProfileTitle: string;
56
+ waterlineLabel: string;
57
+ seabedLabel: string;
58
+ anchorLabel: string;
59
+ catenaryCurveLabel: string;
60
+ swingCircleLabel: string;
61
+ seabedTag: string;
62
+ catenaryTag: string;
63
+ }
@@ -1,6 +1,7 @@
1
+ import type { NauticalToolDefinition } from '../../types';
1
2
  import { endurance } from './entry';
2
3
  export * from './entry';
3
- export const ENDURANCE_TOOL: ToolDefinition = {
4
+ export const ENDURANCE_TOOL: NauticalToolDefinition = {
4
5
  entry: endurance,
5
6
  Component: () => import('./component.astro'),
6
7
  SEOComponent: () => import('./seo.astro'),
package/src/tools.ts CHANGED
@@ -5,6 +5,7 @@ import { NAUTICAL_CONVERTER_TOOL } from './tool/nauticalConverter';
5
5
  import { SAIL_AREA_TOOL } from './tool/sailArea';
6
6
  import { SPEED_CONVERTER_TOOL } from './tool/speedConverter';
7
7
  import { ENDURANCE_TOOL } from './tool/endurance';
8
+ import { ANCHOR_SCOPE_TOOL } from './tool/anchorScope';
8
9
  import type { ToolDefinition } from './types';
9
10
 
10
11
  export const ALL_TOOLS: ToolDefinition[] = [
@@ -14,6 +15,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
14
15
  SAIL_AREA_TOOL,
15
16
  SPEED_CONVERTER_TOOL,
16
17
  ENDURANCE_TOOL,
18
+ ANCHOR_SCOPE_TOOL,
17
19
  ];
18
20
 
19
21
  export {
@@ -23,5 +25,6 @@ export {
23
25
  SAIL_AREA_TOOL,
24
26
  SPEED_CONVERTER_TOOL,
25
27
  ENDURANCE_TOOL,
28
+ ANCHOR_SCOPE_TOOL,
26
29
  };
27
30
 
package/src/types.ts CHANGED
@@ -5,7 +5,7 @@ export type { SEOSection };
5
5
 
6
6
  export type KnownLocale =
7
7
  | 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
8
- | 'fr' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
8
+ | 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
9
9
  | 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
10
10
 
11
11
  export interface FAQItem {
@@ -67,3 +67,5 @@ export interface ToolDefinition {
67
67
  SEOComponent: unknown;
68
68
  BibliographyComponent: unknown;
69
69
  }
70
+
71
+ export type NauticalToolDefinition = ToolDefinition;