@jjlmoya/utils-nautical 1.22.0 → 1.24.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 (59) hide show
  1. package/package.json +2 -2
  2. package/src/category/i18n/en.ts +1 -1
  3. package/src/category/index.ts +2 -1
  4. package/src/data.ts +1 -0
  5. package/src/entries.ts +4 -1
  6. package/src/index.ts +1 -0
  7. package/src/layouts/PreviewLayout.astro +118 -118
  8. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  9. package/src/tests/category_seo_quality.test.ts +74 -0
  10. package/src/tests/faq_count.test.ts +30 -30
  11. package/src/tests/i18n_coverage.test.ts +6 -1
  12. package/src/tests/locale_completeness.test.ts +2 -2
  13. package/src/tests/no_h1_in_components.test.ts +1 -1
  14. package/src/tests/seo_length.test.ts +29 -29
  15. package/src/tests/seo_parity.test.ts +2 -2
  16. package/src/tests/seo_wellformed_export.test.ts +65 -0
  17. package/src/tests/spanish_leakage.test.ts +175 -175
  18. package/src/tests/tool_validation.test.ts +54 -51
  19. package/src/tool/endurance/i18n/fr.ts +205 -205
  20. package/src/tool/endurance/seo.astro +15 -15
  21. package/src/tool/hullSpeed/bibliography.astro +14 -0
  22. package/src/tool/hullSpeed/bibliography.ts +12 -0
  23. package/src/tool/hullSpeed/component.astro +116 -0
  24. package/src/tool/hullSpeed/controller.ts +204 -0
  25. package/src/tool/hullSpeed/dom-views.ts +141 -0
  26. package/src/tool/hullSpeed/entry.ts +32 -0
  27. package/src/tool/hullSpeed/evaluator.ts +20 -0
  28. package/src/tool/hullSpeed/hull-draw.ts +185 -0
  29. package/src/tool/hullSpeed/i18n/de.ts +199 -0
  30. package/src/tool/hullSpeed/i18n/en.ts +199 -0
  31. package/src/tool/hullSpeed/i18n/es.ts +199 -0
  32. package/src/tool/hullSpeed/i18n/fr.ts +199 -0
  33. package/src/tool/hullSpeed/i18n/id.ts +199 -0
  34. package/src/tool/hullSpeed/i18n/it.ts +199 -0
  35. package/src/tool/hullSpeed/i18n/ja.ts +199 -0
  36. package/src/tool/hullSpeed/i18n/ko.ts +199 -0
  37. package/src/tool/hullSpeed/i18n/nl.ts +199 -0
  38. package/src/tool/hullSpeed/i18n/pl.ts +199 -0
  39. package/src/tool/hullSpeed/i18n/pt.ts +199 -0
  40. package/src/tool/hullSpeed/i18n/ru.ts +199 -0
  41. package/src/tool/hullSpeed/i18n/sv.ts +199 -0
  42. package/src/tool/hullSpeed/i18n/tr.ts +199 -0
  43. package/src/tool/hullSpeed/i18n/zh.ts +199 -0
  44. package/src/tool/hullSpeed/index.ts +11 -0
  45. package/src/tool/hullSpeed/logic.test.ts +105 -0
  46. package/src/tool/hullSpeed/logic.ts +116 -0
  47. package/src/tool/hullSpeed/presets.ts +17 -0
  48. package/src/tool/hullSpeed/sailboat-hull-speed-calculator.css +504 -0
  49. package/src/tool/hullSpeed/seo.astro +15 -0
  50. package/src/tool/hullSpeed/storage.test.ts +23 -0
  51. package/src/tool/hullSpeed/storage.ts +65 -0
  52. package/src/tool/hullSpeed/ui.ts +43 -0
  53. package/src/tool/nauticalConverter/seo.astro +15 -15
  54. package/src/tool/sailArea/seo.astro +15 -15
  55. package/src/tool/speedConverter/i18n/fr.ts +259 -259
  56. package/src/tool/speedConverter/seo.astro +15 -15
  57. package/src/tool/tideCalculator/seo.astro +15 -15
  58. package/src/tool/underKeel/seo.astro +15 -15
  59. package/src/tools.ts +3 -0
@@ -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
+ }