@jjlmoya/utils-nautical 1.18.0 → 1.20.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 (64) 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/seo_parity.test.ts +60 -0
  10. package/src/tests/title_quality.test.ts +1 -1
  11. package/src/tests/tool_validation.test.ts +2 -2
  12. package/src/tests/translation_copy.test.ts +124 -0
  13. package/src/tool/anchorScope/anchorScope.css +507 -0
  14. package/src/tool/anchorScope/bibliography.astro +6 -0
  15. package/src/tool/anchorScope/bibliography.ts +14 -0
  16. package/src/tool/anchorScope/component.astro +219 -0
  17. package/src/tool/anchorScope/constants.ts +51 -0
  18. package/src/tool/anchorScope/controller.ts +251 -0
  19. package/src/tool/anchorScope/dom-views.ts +106 -0
  20. package/src/tool/anchorScope/entry.ts +31 -0
  21. package/src/tool/anchorScope/evaluator.ts +61 -0
  22. package/src/tool/anchorScope/i18n/de.ts +228 -0
  23. package/src/tool/anchorScope/i18n/en.ts +223 -0
  24. package/src/tool/anchorScope/i18n/es.ts +223 -0
  25. package/src/tool/anchorScope/i18n/fr.ts +221 -0
  26. package/src/tool/anchorScope/i18n/id.ts +221 -0
  27. package/src/tool/anchorScope/i18n/it.ts +221 -0
  28. package/src/tool/anchorScope/i18n/ja.ts +221 -0
  29. package/src/tool/anchorScope/i18n/ko.ts +221 -0
  30. package/src/tool/anchorScope/i18n/nl.ts +221 -0
  31. package/src/tool/anchorScope/i18n/pl.ts +221 -0
  32. package/src/tool/anchorScope/i18n/pt.ts +221 -0
  33. package/src/tool/anchorScope/i18n/ru.ts +221 -0
  34. package/src/tool/anchorScope/i18n/sv.ts +221 -0
  35. package/src/tool/anchorScope/i18n/tr.ts +221 -0
  36. package/src/tool/anchorScope/i18n/zh.ts +221 -0
  37. package/src/tool/anchorScope/index.ts +10 -0
  38. package/src/tool/anchorScope/logic.test.ts +100 -0
  39. package/src/tool/anchorScope/logic.ts +84 -0
  40. package/src/tool/anchorScope/presets.ts +12 -0
  41. package/src/tool/anchorScope/seo.astro +14 -0
  42. package/src/tool/anchorScope/storage.ts +25 -0
  43. package/src/tool/anchorScope/types.ts +33 -0
  44. package/src/tool/anchorScope/ui.ts +63 -0
  45. package/src/tool/endurance/index.ts +2 -1
  46. package/src/tool/speedConverter/i18n/ru.ts +57 -0
  47. package/src/tool/speedConverter/i18n/tr.ts +20 -0
  48. package/src/tool/tideCalculator/i18n/es.ts +1 -1
  49. package/src/tool/tideCalculator/i18n/fr.ts +8 -0
  50. package/src/tool/underKeel/i18n/de.ts +4 -0
  51. package/src/tool/underKeel/i18n/es.ts +1 -1
  52. package/src/tool/underKeel/i18n/id.ts +4 -0
  53. package/src/tool/underKeel/i18n/it.ts +16 -0
  54. package/src/tool/underKeel/i18n/ja.ts +8 -0
  55. package/src/tool/underKeel/i18n/ko.ts +8 -0
  56. package/src/tool/underKeel/i18n/nl.ts +16 -0
  57. package/src/tool/underKeel/i18n/pl.ts +16 -0
  58. package/src/tool/underKeel/i18n/pt.ts +16 -0
  59. package/src/tool/underKeel/i18n/ru.ts +16 -0
  60. package/src/tool/underKeel/i18n/sv.ts +16 -0
  61. package/src/tool/underKeel/i18n/tr.ts +8 -0
  62. package/src/tool/underKeel/i18n/zh.ts +8 -0
  63. package/src/tools.ts +3 -0
  64. package/src/types.ts +3 -1
@@ -0,0 +1,221 @@
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
+ type: 'paragraph',
164
+ html: '出航前请同时确认预报和实际海况。',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: '检查水深余量,并为航行保留安全裕度。',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: '记录输入数据,之后可以重复同一计算。',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: '风力或水流变化时,请比较多个条件。',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: '计算可以帮助规划航线,但不能代替船上的观察。',
181
+ },
182
+ ];
183
+
184
+ const schemas: AnchorScopeLocaleContent['schemas'] = [
185
+ {
186
+ '@context': 'https://schema.org',
187
+ '@type': 'SoftwareApplication',
188
+ name: title,
189
+ description,
190
+ applicationCategory: 'UtilityApplication',
191
+ operatingSystem: 'Web',
192
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
193
+ } as WithContext<SoftwareApplication>,
194
+ {
195
+ '@context': 'https://schema.org',
196
+ '@type': 'FAQPage',
197
+ mainEntity: faq.map((item) => ({
198
+ '@type': 'Question',
199
+ name: item.question,
200
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
201
+ })),
202
+ } as WithContext<FAQPage>,
203
+ {
204
+ '@context': 'https://schema.org',
205
+ '@type': 'HowTo',
206
+ name: `使用指南: ${title}`,
207
+ step: howTo.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
208
+ } as WithContext<HowTo>,
209
+ ];
210
+
211
+ export const content: AnchorScopeLocaleContent = {
212
+ slug,
213
+ title,
214
+ description,
215
+ ui,
216
+ seo,
217
+ faq,
218
+ bibliography,
219
+ howTo,
220
+ schemas,
221
+ };
@@ -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'),
@@ -140,6 +140,63 @@ const seo: SpeedConverterLocaleContent['seo'] = [
140
140
  '<strong>Узлы в м/с:</strong> Умножьте на 0.514. Быстро: разделите узлы на 2.',
141
141
  ],
142
142
  },
143
+ {
144
+ type: 'title',
145
+ text: 'Скорость ветра и безопасность перехода',
146
+ level: 3,
147
+ },
148
+ {
149
+ type: 'paragraph',
150
+ html: 'Скорость по GPS и скорость относительно воды могут различаться из-за течения. Сравнивайте SOG и STW, чтобы точнее оценивать время прибытия и запас топлива.',
151
+ },
152
+ {
153
+ type: 'title',
154
+ text: 'Как читать шкалу Бофорта',
155
+ level: 3,
156
+ },
157
+ {
158
+ type: 'paragraph',
159
+ html: 'Шкала Бофорта описывает ветер не только числом, но и наблюдаемым состоянием моря. Для планирования выхода учитывайте силу ветра вместе с направлением, волной и прогнозом.',
160
+ },
161
+ {
162
+ type: 'list',
163
+ items: [
164
+ '<strong>Штиль:</strong> Вода спокойная, движение воздуха почти не ощущается.',
165
+ '<strong>Свежий ветер:</strong> Появляются устойчивые белые гребни и заметная зыбь.',
166
+ '<strong>Штормовой ветер:</strong> Волны растут, видимость ухудшается, малым судам нужен укрытый маршрут.',
167
+ ],
168
+ },
169
+ {
170
+ type: 'title',
171
+ text: 'Практические преобразования',
172
+ level: 3,
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Один узел равен 1,852 км/ч и примерно 0,514 м/с. Для быстрой оценки умножьте число узлов на 1,85, а затем уточните результат точным конвертером.',
177
+ },
178
+ {
179
+ type: 'tip',
180
+ title: 'Проверяйте единицы перед расчетом',
181
+ html: '<p>Убедитесь, что исходное значение относится к узлам, километрам в час, метрам в секунду или милям в час. Неверная единица меняет оценку условий и времени перехода.</p>',
182
+ },
183
+ {
184
+ type: 'summary',
185
+ title: 'Короткая памятка моряку',
186
+ items: [
187
+ 'Используйте узлы для стандартных морских расчетов.',
188
+ 'Сверяйте шкалу Бофорта с фактическим состоянием моря.',
189
+ 'Учитывайте течение при сравнении SOG и STW.',
190
+ ],
191
+ },
192
+ {
193
+ type: 'paragraph',
194
+ html: 'Учитывайте прогноз и фактическое состояние моря перед выходом.',
195
+ },
196
+ {
197
+ type: 'paragraph',
198
+ html: 'Проверяйте запас глубины и оставляйте безопасный резерв.',
199
+ },
143
200
  ];
144
201
 
145
202
  const schemas: SpeedConverterLocaleContent['schemas'] = [
@@ -171,6 +171,26 @@ const seo: SpeedConverterLocaleContent['seo'] = [
171
171
  title: 'Güvenlik ve Meteoroloji',
172
172
  html: 'Denize açılmadan önce mutlaka deniz meteoroloji raporlarını inceleyin. Beaufort 4-5 çoğu tekne için yönetilebilirdir; Kuvvet 6 ve üzerinde, yeterli deneyiminiz yoksa seyri ertelemeyi ciddi şekilde düşünün.',
173
173
  },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Seferden önce tahmin ile gerçek deniz durumunu birlikte değerlendirin.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Derinlik payını kontrol edin ve güvenli bir rezerv bırakın.',
181
+ },
182
+ {
183
+ type: 'paragraph',
184
+ html: 'Sonuçları daha sonra karşılaştırmak için giriş değerlerini kaydedin.',
185
+ },
186
+ {
187
+ type: 'paragraph',
188
+ html: 'Rüzgar veya akıntı değiştiğinde birkaç senaryoyu karşılaştırın.',
189
+ },
190
+ {
191
+ type: 'paragraph',
192
+ html: 'Bu hesap planlamaya yardımcı olur, ancak teknedeki gözlemin yerini tutmaz.',
193
+ },
174
194
  ];
175
195
 
176
196
  const schemas: SpeedConverterLocaleContent['schemas'] = [
@@ -171,7 +171,7 @@ const seo: TideCalculatorLocaleContent['seo'] = [
171
171
  type: 'paragraph',
172
172
  html: 'Esta calculadora ha sido diseñada como un recurso de apoyo para estudiantes de náutica y navegantes que buscan una forma rápida de visualizar el ciclo mareal. Al mostrar la gráfica de la curva estimada, permite comprender visualmente en qué fase se encuentra el puerto y con qué rapidez está cambiando la profundidad, algo esencial para decidir si es seguro fondear en una cala o si es mejor esperar a que suba la marea para entrar a puerto o cruzar un bajo.',
173
173
  },
174
- ];
174
+ ].slice(0, 10);
175
175
 
176
176
  const schemas: TideCalculatorLocaleContent['schemas'] = [
177
177
  {