@jjlmoya/utils-nautical 1.22.0 → 1.23.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 +1 -1
- package/src/category/index.ts +2 -1
- package/src/data.ts +1 -0
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/i18n_coverage.test.ts +6 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +7 -4
- package/src/tool/hullSpeed/bibliography.astro +14 -0
- package/src/tool/hullSpeed/bibliography.ts +12 -0
- package/src/tool/hullSpeed/component.astro +116 -0
- package/src/tool/hullSpeed/controller.ts +204 -0
- package/src/tool/hullSpeed/dom-views.ts +141 -0
- package/src/tool/hullSpeed/entry.ts +32 -0
- package/src/tool/hullSpeed/evaluator.ts +20 -0
- package/src/tool/hullSpeed/hull-draw.ts +185 -0
- package/src/tool/hullSpeed/i18n/de.ts +199 -0
- package/src/tool/hullSpeed/i18n/en.ts +199 -0
- package/src/tool/hullSpeed/i18n/es.ts +199 -0
- package/src/tool/hullSpeed/i18n/fr.ts +199 -0
- package/src/tool/hullSpeed/i18n/id.ts +199 -0
- package/src/tool/hullSpeed/i18n/it.ts +199 -0
- package/src/tool/hullSpeed/i18n/ja.ts +199 -0
- package/src/tool/hullSpeed/i18n/ko.ts +199 -0
- package/src/tool/hullSpeed/i18n/nl.ts +199 -0
- package/src/tool/hullSpeed/i18n/pl.ts +199 -0
- package/src/tool/hullSpeed/i18n/pt.ts +199 -0
- package/src/tool/hullSpeed/i18n/ru.ts +199 -0
- package/src/tool/hullSpeed/i18n/sv.ts +199 -0
- package/src/tool/hullSpeed/i18n/tr.ts +199 -0
- package/src/tool/hullSpeed/i18n/zh.ts +199 -0
- package/src/tool/hullSpeed/index.ts +11 -0
- package/src/tool/hullSpeed/logic.test.ts +105 -0
- package/src/tool/hullSpeed/logic.ts +116 -0
- package/src/tool/hullSpeed/presets.ts +17 -0
- package/src/tool/hullSpeed/sailboat-hull-speed-calculator.css +504 -0
- package/src/tool/hullSpeed/seo.astro +15 -0
- package/src/tool/hullSpeed/storage.test.ts +23 -0
- package/src/tool/hullSpeed/storage.ts +65 -0
- package/src/tool/hullSpeed/ui.ts +43 -0
- package/src/tools.ts +3 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { HullSpeedLocaleContent, HullSpeedUI } from '../index';
|
|
3
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
4
|
+
|
|
5
|
+
const slug = 'sailboat-hull-speed-calculator';
|
|
6
|
+
const title = '帆船船体速度计算器';
|
|
7
|
+
const description =
|
|
8
|
+
'由水线长度求出理论船体速度,对照实测航程仪读数,并看排水型船体何时爬上自己的艏波。';
|
|
9
|
+
|
|
10
|
+
const ui: HullSpeedUI = {
|
|
11
|
+
metric: '公制',
|
|
12
|
+
imperial: '英制',
|
|
13
|
+
unitGroup: '单位',
|
|
14
|
+
sceneLabel: '水线与艏波',
|
|
15
|
+
hullSpeedLabel: '船体速度',
|
|
16
|
+
knotsUnit: 'kn',
|
|
17
|
+
kmhUnit: 'km/h',
|
|
18
|
+
mphUnit: 'mph',
|
|
19
|
+
ratioLabel: 'S/L',
|
|
20
|
+
froudeLabel: 'Fr',
|
|
21
|
+
lwlLabelMetric: '水线长度',
|
|
22
|
+
lwlLabelImperial: '水线长度',
|
|
23
|
+
lwlUnitM: 'm',
|
|
24
|
+
lwlUnitFt: 'ft',
|
|
25
|
+
observedLabel: '航程仪速度',
|
|
26
|
+
observedHint: '留 0 以绘制理论波。输入航程仪读数,看你是否仍坐在波谷里。',
|
|
27
|
+
stretchHint: '拖动船体以拉长水线',
|
|
28
|
+
hullKindLabel: '船体形状',
|
|
29
|
+
displacement: '排水型',
|
|
30
|
+
semi: '半排水型',
|
|
31
|
+
planing: '滑行型',
|
|
32
|
+
boatsLabel: '水线示例',
|
|
33
|
+
dinghy: '小艇',
|
|
34
|
+
daysailer: '日航帆船',
|
|
35
|
+
cruiser: '巡航船',
|
|
36
|
+
bluewater: '远海',
|
|
37
|
+
workboat: '工作船',
|
|
38
|
+
bandBelow: '仍是短波',
|
|
39
|
+
bandNear: '接近波浪屏障',
|
|
40
|
+
bandAt: '骑在艏波上',
|
|
41
|
+
bandAbove: '正试图爬上波浪',
|
|
42
|
+
bandPlane: '已滑行,超过船体速度',
|
|
43
|
+
planingNote:
|
|
44
|
+
'滑行型船体可以把这道波甩在后面。船体速度是参照,不是墙。',
|
|
45
|
+
displacementNote:
|
|
46
|
+
'排水型追上自己的伴流时会付出陡峭代价。把它当成实用上限,不是 GPS 目标。',
|
|
47
|
+
semiNote:
|
|
48
|
+
'半排水型在功率足够时可以略微超过经典比值,但阻力会急剧上升。',
|
|
49
|
+
waveLegend: '艏波',
|
|
50
|
+
waterLegend: '水线',
|
|
51
|
+
markLegend: '船体速度',
|
|
52
|
+
faqTitle: '船体速度问题',
|
|
53
|
+
bibliographyTitle: '参考文献',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const faq: HullSpeedLocaleContent['faq'] = [
|
|
57
|
+
{
|
|
58
|
+
question: '帆船的船体速度是什么?',
|
|
59
|
+
answer:
|
|
60
|
+
'排水型船体带着大约与自身水线等长的艏波前进时的速度。航海单位里通常用 1.34 乘以英尺水线长度的平方根来估计。这是临界速度,不是硬性的物理停点。',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
question: '如何计算船体速度?',
|
|
64
|
+
answer:
|
|
65
|
+
'把水线换成英尺,开平方,再乘 1.34 得到节。10 m 水线约 32.8 ft,所以大约 7.7 kn。同一点对应弗劳德数约 0.40。',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
question: '滑行艇也适用船体速度吗?',
|
|
69
|
+
answer:
|
|
70
|
+
'公式仍描述船体若停留在排水模式会造出的波。滑行型和许多半排水型就是为了爬过那道波而设计的。把数字当参照,再读船体形状说明。',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
question: '该用水线长还是总长?',
|
|
74
|
+
answer:
|
|
75
|
+
'用水线长 (LWL),也就是真正造波的浸水长度。总长、艏斜桅和悬伸并不决定波长。在水里量 LWL,不要用量册上的 LOA。',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const howTo: HullSpeedLocaleContent['howTo'] = [
|
|
80
|
+
{
|
|
81
|
+
name: '拉长水线',
|
|
82
|
+
text: '拖动船体、移动水线滑轨,或从小艇选到工作船。船体速度是该长度(英尺)平方根的 1.34 倍。',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: '选择船体形状',
|
|
86
|
+
text: '排水型、半排水型或滑行型会改变龙骨,以及船能否爬出自己的波。',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: '读波谷',
|
|
90
|
+
text: '在船体速度时,艏波和艉波相隔一条水线,船坐在谷里。那就是屏障。',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: '对照航程仪',
|
|
94
|
+
text: '输入对水速度。较短的波表示你仍推得便宜。较长的波表示你正在爬谷。',
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const seo: HullSpeedLocaleContent['seo'] = [
|
|
99
|
+
{
|
|
100
|
+
type: 'title',
|
|
101
|
+
text: '船体速度真正在说什么',
|
|
102
|
+
level: 2,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'paragraph',
|
|
106
|
+
html: '排水型船体从船首推一道波,从船尾再推一道。当这两道峰大约相隔一条水线时,船就带着自己的伴流走。多余功率这时主要把水堆起来,而不是买速度。水手把这种状态叫做船体速度。',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: 'paragraph',
|
|
110
|
+
html: '本计算器使用常见的业余比值:每英尺水线平方根 1.34 节。细长船体可以略高,驳船更低。把结果当教学记号,再拿真实航程仪对照。',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: 'title',
|
|
114
|
+
text: '算过的水线例子',
|
|
115
|
+
level: 2,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'table',
|
|
119
|
+
headers: ['水线', '船体速度', '典型船只'],
|
|
120
|
+
rows: [
|
|
121
|
+
['4.2 m / 13.8 ft', '5.0 kn', '小艇'],
|
|
122
|
+
['6.5 m / 21.3 ft', '6.2 kn', '日航帆船'],
|
|
123
|
+
['10 m / 32.8 ft', '7.7 kn', '家庭巡航船'],
|
|
124
|
+
['13.5 m / 44.3 ft', '8.9 kn', '远海游艇'],
|
|
125
|
+
['18 m / 59.1 ft', '10.3 kn', '小型工作船'],
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'title',
|
|
130
|
+
text: '如何读艏波',
|
|
131
|
+
level: 2,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'list',
|
|
135
|
+
items: [
|
|
136
|
+
'标记以下时波短,船仍便宜推动。',
|
|
137
|
+
'靠近标记时艏波变陡,燃油或帆买到的速度变少。',
|
|
138
|
+
'正好在标记上时波长等于 LWL。排水型处于经典屏障。',
|
|
139
|
+
'再往上,排水型爬自己的伴流。滑行型可能已经抬起来。',
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'tip',
|
|
144
|
+
title: '量 LWL,不要量 LOA',
|
|
145
|
+
html: '宣传册长度包含悬伸。波浪在乎浸入的水线。若只有 LOA,算出的船体速度会偏乐观。',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'title',
|
|
149
|
+
text: '一句话里的弗劳德数',
|
|
150
|
+
level: 2,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'paragraph',
|
|
154
|
+
html: '船体速度是把弗劳德数约 0.40 包成好用的形式:速度除以重力乘长度的平方根。所以即使船体形状不变,更长的水线也更快。抬高排水极限的干净办法是拉长 LWL,不是加马力。',
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const schemas: HullSpeedLocaleContent['schemas'] = [
|
|
159
|
+
{
|
|
160
|
+
'@context': 'https://schema.org',
|
|
161
|
+
'@type': 'SoftwareApplication',
|
|
162
|
+
name: title,
|
|
163
|
+
description,
|
|
164
|
+
applicationCategory: 'UtilityApplication',
|
|
165
|
+
operatingSystem: 'Web',
|
|
166
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
167
|
+
} as WithContext<SoftwareApplication>,
|
|
168
|
+
{
|
|
169
|
+
'@context': 'https://schema.org',
|
|
170
|
+
'@type': 'FAQPage',
|
|
171
|
+
mainEntity: faq.map((item) => ({
|
|
172
|
+
'@type': 'Question',
|
|
173
|
+
name: item.question,
|
|
174
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
175
|
+
})),
|
|
176
|
+
} as WithContext<FAQPage>,
|
|
177
|
+
{
|
|
178
|
+
'@context': 'https://schema.org',
|
|
179
|
+
'@type': 'HowTo',
|
|
180
|
+
name: `如何使用${title}`,
|
|
181
|
+
step: howTo.map((step) => ({
|
|
182
|
+
'@type': 'HowToStep',
|
|
183
|
+
name: step.name,
|
|
184
|
+
text: step.text,
|
|
185
|
+
})),
|
|
186
|
+
} as WithContext<HowTo>,
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
export const content: HullSpeedLocaleContent = {
|
|
190
|
+
slug,
|
|
191
|
+
title,
|
|
192
|
+
description,
|
|
193
|
+
ui,
|
|
194
|
+
seo,
|
|
195
|
+
faq,
|
|
196
|
+
bibliography,
|
|
197
|
+
howTo,
|
|
198
|
+
schemas,
|
|
199
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { hullSpeed } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const HULL_SPEED_TOOL: ToolDefinition = {
|
|
7
|
+
entry: hullSpeed,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
bandFromRatio,
|
|
4
|
+
clampLwlMetres,
|
|
5
|
+
clampObservedKnots,
|
|
6
|
+
computeHullSpeed,
|
|
7
|
+
displayLength,
|
|
8
|
+
feetToMetres,
|
|
9
|
+
froudeNumber,
|
|
10
|
+
HULL_SPEED_FACTOR,
|
|
11
|
+
hullSpeedKnotsFromLwl,
|
|
12
|
+
knotsToKmh,
|
|
13
|
+
knotsToMph,
|
|
14
|
+
LWL_MAX_M,
|
|
15
|
+
LWL_MIN_M,
|
|
16
|
+
metresToFeet,
|
|
17
|
+
speedLengthRatio,
|
|
18
|
+
storeLength,
|
|
19
|
+
} from './logic';
|
|
20
|
+
|
|
21
|
+
describe('hull speed geometry', () => {
|
|
22
|
+
it('converts metres and feet without drift on a round trip', () => {
|
|
23
|
+
const metres = 10;
|
|
24
|
+
expect(feetToMetres(metresToFeet(metres))).toBeCloseTo(metres, 10);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses 1.34 times sqrt of waterline in feet', () => {
|
|
28
|
+
const lwlM = 10;
|
|
29
|
+
const expected = HULL_SPEED_FACTOR * Math.sqrt(metresToFeet(lwlM));
|
|
30
|
+
expect(hullSpeedKnotsFromLwl(lwlM)).toBeCloseTo(expected, 8);
|
|
31
|
+
expect(hullSpeedKnotsFromLwl(lwlM)).toBeCloseTo(7.676, 2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps speed-length ratio equal to the factor at hull speed', () => {
|
|
35
|
+
const lwlM = 12;
|
|
36
|
+
const hs = hullSpeedKnotsFromLwl(lwlM);
|
|
37
|
+
expect(speedLengthRatio(hs, lwlM)).toBeCloseTo(HULL_SPEED_FACTOR, 8);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns a Froude number near 0.40 at hull speed', () => {
|
|
41
|
+
const lwlM = 10;
|
|
42
|
+
const hs = hullSpeedKnotsFromLwl(lwlM);
|
|
43
|
+
expect(froudeNumber(hs, lwlM)).toBeGreaterThan(0.39);
|
|
44
|
+
expect(froudeNumber(hs, lwlM)).toBeLessThan(0.41);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('units and clamps', () => {
|
|
49
|
+
it('displays feet in imperial and metres in metric', () => {
|
|
50
|
+
expect(displayLength(10, 'metric')).toBe(10);
|
|
51
|
+
expect(displayLength(10, 'imperial')).toBeCloseTo(metresToFeet(10), 8);
|
|
52
|
+
expect(storeLength(32.80839895013123, 'imperial')).toBeCloseTo(10, 6);
|
|
53
|
+
expect(storeLength(10, 'metric')).toBe(10);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('clamps waterline and observed speed', () => {
|
|
57
|
+
expect(clampLwlMetres(Number.NaN)).toBe(LWL_MIN_M);
|
|
58
|
+
expect(clampLwlMetres(0)).toBe(LWL_MIN_M);
|
|
59
|
+
expect(clampLwlMetres(99)).toBe(LWL_MAX_M);
|
|
60
|
+
expect(clampObservedKnots(-4)).toBe(0);
|
|
61
|
+
expect(clampObservedKnots(Number.NaN)).toBe(0);
|
|
62
|
+
expect(clampObservedKnots(80)).toBe(40);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('converts knots to km/h and mph', () => {
|
|
66
|
+
expect(knotsToKmh(10)).toBeCloseTo(18.52, 6);
|
|
67
|
+
expect(knotsToMph(10)).toBeCloseTo(11.5078, 3);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('bands', () => {
|
|
72
|
+
it('marks below, near, at and above the wave barrier', () => {
|
|
73
|
+
expect(bandFromRatio(0.9)).toBe('below');
|
|
74
|
+
expect(bandFromRatio(1.2)).toBe('near');
|
|
75
|
+
expect(bandFromRatio(1.34)).toBe('at');
|
|
76
|
+
expect(bandFromRatio(1.8)).toBe('above');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('computeHullSpeed', () => {
|
|
81
|
+
it('uses hull speed as the compared speed when observed is zero', () => {
|
|
82
|
+
const result = computeHullSpeed({
|
|
83
|
+
lwlMetres: 10,
|
|
84
|
+
hullKind: 'displacement',
|
|
85
|
+
observedKnots: 0,
|
|
86
|
+
});
|
|
87
|
+
expect(result.band).toBe('at');
|
|
88
|
+
expect(result.hullSpeedKnots).toBeCloseTo(7.676, 2);
|
|
89
|
+
expect(result.observedKnots).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('classifies an observed speed above hull speed', () => {
|
|
93
|
+
const result = computeHullSpeed({
|
|
94
|
+
lwlMetres: 10,
|
|
95
|
+
hullKind: 'planing',
|
|
96
|
+
observedKnots: 18,
|
|
97
|
+
});
|
|
98
|
+
expect(result.band).toBe('above');
|
|
99
|
+
expect(result.speedLengthRatio).toBeGreaterThan(HULL_SPEED_FACTOR);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('treats a ratio within 0.02 of 1.34 as at hull speed', () => {
|
|
103
|
+
expect(bandFromRatio(1.33)).toBe('at');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export type UnitSystem = 'metric' | 'imperial';
|
|
2
|
+
export type HullKind = 'displacement' | 'semi' | 'planing';
|
|
3
|
+
export type SpeedBand = 'below' | 'near' | 'at' | 'above';
|
|
4
|
+
|
|
5
|
+
export interface HullSpeedInput {
|
|
6
|
+
lwlMetres: number;
|
|
7
|
+
hullKind: HullKind;
|
|
8
|
+
observedKnots: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface HullSpeedResult {
|
|
12
|
+
lwlMetres: number;
|
|
13
|
+
lwlFeet: number;
|
|
14
|
+
hullKind: HullKind;
|
|
15
|
+
hullSpeedKnots: number;
|
|
16
|
+
hullSpeedKmh: number;
|
|
17
|
+
hullSpeedMph: number;
|
|
18
|
+
speedLengthRatio: number;
|
|
19
|
+
froudeNumber: number;
|
|
20
|
+
band: SpeedBand;
|
|
21
|
+
observedKnots: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const METRES_PER_FOOT = 0.3048;
|
|
25
|
+
export const HULL_SPEED_FACTOR = 1.34;
|
|
26
|
+
export const GRAVITY_MS2 = 9.80665;
|
|
27
|
+
export const METRES_PER_NAUTICAL_MILE = 1852;
|
|
28
|
+
export const LWL_MIN_M = 2.5;
|
|
29
|
+
export const LWL_MAX_M = 40;
|
|
30
|
+
export const OBSERVED_MAX_KN = 40;
|
|
31
|
+
export const NEAR_RATIO = 1.1;
|
|
32
|
+
|
|
33
|
+
export function metresToFeet(metres: number): number {
|
|
34
|
+
return metres / METRES_PER_FOOT;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function feetToMetres(feet: number): number {
|
|
38
|
+
return feet * METRES_PER_FOOT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function knotsToMetresPerSecond(knots: number): number {
|
|
42
|
+
return (knots * METRES_PER_NAUTICAL_MILE) / 3600;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function knotsToKmh(knots: number): number {
|
|
46
|
+
return knots * 1.852;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function knotsToMph(knots: number): number {
|
|
50
|
+
return knotsToKmh(knots) / 1.609344;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clampLwlMetres(metres: number): number {
|
|
54
|
+
if (Number.isNaN(metres)) return LWL_MIN_M;
|
|
55
|
+
return Math.min(LWL_MAX_M, Math.max(LWL_MIN_M, metres));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function clampObservedKnots(knots: number): number {
|
|
59
|
+
if (Number.isNaN(knots) || knots < 0) return 0;
|
|
60
|
+
return Math.min(OBSERVED_MAX_KN, knots);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function displayLength(metres: number, unit: UnitSystem): number {
|
|
64
|
+
if (unit === 'imperial') return metresToFeet(metres);
|
|
65
|
+
return metres;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function storeLength(displayed: number, unit: UnitSystem): number {
|
|
69
|
+
if (unit === 'imperial') return feetToMetres(displayed);
|
|
70
|
+
return displayed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function hullSpeedKnotsFromLwl(lwlMetres: number): number {
|
|
74
|
+
const feet = metresToFeet(clampLwlMetres(lwlMetres));
|
|
75
|
+
return HULL_SPEED_FACTOR * Math.sqrt(feet);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function speedLengthRatio(speedKnots: number, lwlMetres: number): number {
|
|
79
|
+
const feet = metresToFeet(clampLwlMetres(lwlMetres));
|
|
80
|
+
const root = Math.sqrt(feet);
|
|
81
|
+
if (root === 0) return 0;
|
|
82
|
+
return speedKnots / root;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function froudeNumber(speedKnots: number, lwlMetres: number): number {
|
|
86
|
+
const length = clampLwlMetres(lwlMetres);
|
|
87
|
+
const denom = Math.sqrt(GRAVITY_MS2 * length);
|
|
88
|
+
if (denom === 0) return 0;
|
|
89
|
+
return knotsToMetresPerSecond(speedKnots) / denom;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function bandFromRatio(ratio: number): SpeedBand {
|
|
93
|
+
if (Math.abs(ratio - HULL_SPEED_FACTOR) < 0.02) return 'at';
|
|
94
|
+
if (ratio < NEAR_RATIO) return 'below';
|
|
95
|
+
if (ratio < HULL_SPEED_FACTOR) return 'near';
|
|
96
|
+
return 'above';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function computeHullSpeed(input: HullSpeedInput): HullSpeedResult {
|
|
100
|
+
const lwlMetres = clampLwlMetres(input.lwlMetres);
|
|
101
|
+
const observedKnots = clampObservedKnots(input.observedKnots);
|
|
102
|
+
const hullSpeedKnots = hullSpeedKnotsFromLwl(lwlMetres);
|
|
103
|
+
const compareKnots = observedKnots > 0 ? observedKnots : hullSpeedKnots;
|
|
104
|
+
return {
|
|
105
|
+
lwlMetres,
|
|
106
|
+
lwlFeet: metresToFeet(lwlMetres),
|
|
107
|
+
hullKind: input.hullKind,
|
|
108
|
+
hullSpeedKnots,
|
|
109
|
+
hullSpeedKmh: knotsToKmh(hullSpeedKnots),
|
|
110
|
+
hullSpeedMph: knotsToMph(hullSpeedKnots),
|
|
111
|
+
speedLengthRatio: speedLengthRatio(compareKnots, lwlMetres),
|
|
112
|
+
froudeNumber: froudeNumber(compareKnots, lwlMetres),
|
|
113
|
+
band: bandFromRatio(speedLengthRatio(compareKnots, lwlMetres)),
|
|
114
|
+
observedKnots,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface BoatPreset {
|
|
2
|
+
id: string;
|
|
3
|
+
lwlMetres: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const BOAT_PRESETS: BoatPreset[] = [
|
|
7
|
+
{ id: 'dinghy', lwlMetres: 4.2 },
|
|
8
|
+
{ id: 'daysailer', lwlMetres: 6.5 },
|
|
9
|
+
{ id: 'cruiser', lwlMetres: 10 },
|
|
10
|
+
{ id: 'bluewater', lwlMetres: 13.5 },
|
|
11
|
+
{ id: 'workboat', lwlMetres: 18 },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function presetIdForLwl(lwlMetres: number): string | null {
|
|
15
|
+
const found = BOAT_PRESETS.find((item) => Math.abs(item.lwlMetres - lwlMetres) < 0.05);
|
|
16
|
+
return found ? found.id : null;
|
|
17
|
+
}
|