@jjlmoya/utils-nautical 1.2.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 +75 -0
- package/src/category/i18n/en.ts +79 -0
- package/src/category/i18n/es.ts +79 -0
- package/src/category/i18n/fr.ts +79 -0
- package/src/category/index.ts +13 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +12 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +22 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +249 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +30 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/seo_length.test.ts +29 -0
- package/src/tests/tool_validation.test.ts +51 -0
- package/src/tool/endurance/bibliography.astro +14 -0
- package/src/tool/endurance/component.astro +310 -0
- package/src/tool/endurance/i18n/en.ts +243 -0
- package/src/tool/endurance/i18n/es.ts +249 -0
- package/src/tool/endurance/i18n/fr.ts +217 -0
- package/src/tool/endurance/index.ts +56 -0
- package/src/tool/endurance/logic.ts +36 -0
- package/src/tool/endurance/seo.astro +55 -0
- package/src/tool/endurance/style.css +333 -0
- package/src/tool/nauticalConverter/bibliography.astro +14 -0
- package/src/tool/nauticalConverter/component.astro +481 -0
- package/src/tool/nauticalConverter/i18n/en.ts +239 -0
- package/src/tool/nauticalConverter/i18n/es.ts +239 -0
- package/src/tool/nauticalConverter/i18n/fr.ts +239 -0
- package/src/tool/nauticalConverter/index.ts +57 -0
- package/src/tool/nauticalConverter/logic.ts +52 -0
- package/src/tool/nauticalConverter/seo.astro +52 -0
- package/src/tool/nauticalConverter/style.css +205 -0
- package/src/tool/sailArea/bibliography.astro +14 -0
- package/src/tool/sailArea/component.astro +418 -0
- package/src/tool/sailArea/i18n/en.ts +275 -0
- package/src/tool/sailArea/i18n/es.ts +275 -0
- package/src/tool/sailArea/i18n/fr.ts +275 -0
- package/src/tool/sailArea/index.ts +53 -0
- package/src/tool/sailArea/logic.ts +55 -0
- package/src/tool/sailArea/seo.astro +52 -0
- package/src/tool/sailArea/style.css +351 -0
- package/src/tool/speedConverter/bibliography.astro +14 -0
- package/src/tool/speedConverter/component.astro +125 -0
- package/src/tool/speedConverter/i18n/en.ts +271 -0
- package/src/tool/speedConverter/i18n/es.ts +271 -0
- package/src/tool/speedConverter/i18n/fr.ts +271 -0
- package/src/tool/speedConverter/index.ts +46 -0
- package/src/tool/speedConverter/logic.ts +48 -0
- package/src/tool/speedConverter/seo.astro +43 -0
- package/src/tool/speedConverter/style.css +239 -0
- package/src/tool/tideCalculator/bibliography.astro +14 -0
- package/src/tool/tideCalculator/component.astro +314 -0
- package/src/tool/tideCalculator/i18n/en.ts +203 -0
- package/src/tool/tideCalculator/i18n/es.ts +225 -0
- package/src/tool/tideCalculator/i18n/fr.ts +190 -0
- package/src/tool/tideCalculator/index.ts +50 -0
- package/src/tool/tideCalculator/logic.ts +73 -0
- package/src/tool/tideCalculator/seo.astro +61 -0
- package/src/tool/tideCalculator/style.css +310 -0
- package/src/tool/underKeel/bibliography.astro +14 -0
- package/src/tool/underKeel/component.astro +240 -0
- package/src/tool/underKeel/i18n/en.ts +193 -0
- package/src/tool/underKeel/i18n/es.ts +206 -0
- package/src/tool/underKeel/i18n/fr.ts +193 -0
- package/src/tool/underKeel/index.ts +49 -0
- package/src/tool/underKeel/logic.ts +83 -0
- package/src/tool/underKeel/seo.astro +46 -0
- package/src/tool/underKeel/style.css +312 -0
- package/src/tools.ts +25 -0
- package/src/types.ts +69 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './style.css';
|
|
3
|
+
import type { TideCalculatorUI } from './index';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: TideCalculatorUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<section id="tide-calculator" class="tide-calculator" data-ui={JSON.stringify(ui)}>
|
|
13
|
+
<div class="tc-main-card">
|
|
14
|
+
<div class="tc-calc-sidebar">
|
|
15
|
+
<div class="tc-sidebar-header">
|
|
16
|
+
<h3>{ui.parametersLabel}</h3>
|
|
17
|
+
<button id="invert-btn" title="Invertir ciclo">
|
|
18
|
+
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M18 18V22H16V18H13L17 14L21 18H18M8 6V2H10V6H13L9 10L5 6H8M3 15V13H5V15H3M3 11V9H5V11H3M3 7V5H5V7H3M19 11V9H21V11H19M19 15V13H21V15H19M19 19V17H21V19H19M3 19V17H5V19H3Z"/></svg>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="tc-input-set">
|
|
23
|
+
<div class="tc-input-block">
|
|
24
|
+
<label>{ui.highTideLabel}</label>
|
|
25
|
+
<div class="row">
|
|
26
|
+
<input type="time" id="high-tide-time" value="12:00" />
|
|
27
|
+
<div class="tc-unit-input">
|
|
28
|
+
<input type="number" id="high-tide-height" value="3.5" step="0.1" />
|
|
29
|
+
<span>m</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="tc-input-block">
|
|
35
|
+
<label>{ui.lowTideLabel}</label>
|
|
36
|
+
<div class="row">
|
|
37
|
+
<input type="time" id="low-tide-time" value="06:00" />
|
|
38
|
+
<div class="tc-unit-input">
|
|
39
|
+
<input type="number" id="low-tide-height" value="0.5" step="0.1" />
|
|
40
|
+
<span>m</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="tc-input-block tc-featured">
|
|
46
|
+
<label>{ui.targetTimeLabel}</label>
|
|
47
|
+
<input type="time" id="target-time" value="09:00" class="large-time" />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="tc-calc-content">
|
|
53
|
+
<div class="tc-top-row">
|
|
54
|
+
<div class="tc-result-display">
|
|
55
|
+
<span class="label">{ui.estimatedHeightLabel}</span>
|
|
56
|
+
<div class="tc-value-box">
|
|
57
|
+
<span id="target-value">--</span>
|
|
58
|
+
<small>{ui.metersLabel}</small>
|
|
59
|
+
</div>
|
|
60
|
+
<span id="tide-status" class="tc-status-indicator">--</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="tc-summary-info">
|
|
63
|
+
<div class="tc-stat">
|
|
64
|
+
<span class="tc-s-label">{ui.amplitudeLabel}</span>
|
|
65
|
+
<span id="amplitude-val">--</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="tc-stat">
|
|
68
|
+
<span class="tc-s-label">{ui.durationLabel}</span>
|
|
69
|
+
<span id="duration-val">--</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="tc-chart-area">
|
|
75
|
+
<canvas id="tide-chart"></canvas>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="tc-table-card">
|
|
81
|
+
<h4>{ui.tableBreakdownLabel}</h4>
|
|
82
|
+
<div class="tc-table-wrapper">
|
|
83
|
+
<table id="twelfths-table">
|
|
84
|
+
<thead>
|
|
85
|
+
<tr>
|
|
86
|
+
<th>{ui.tableHourLabel}</th>
|
|
87
|
+
<th>{ui.tableStateLabel}</th>
|
|
88
|
+
<th>%</th>
|
|
89
|
+
<th>{ui.metersLabel}</th>
|
|
90
|
+
</tr>
|
|
91
|
+
</thead>
|
|
92
|
+
<tbody></tbody>
|
|
93
|
+
</table>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<script>
|
|
99
|
+
import { Chart, registerables } from 'chart.js';
|
|
100
|
+
Chart.register(...registerables);
|
|
101
|
+
|
|
102
|
+
const section = document.getElementById('tide-calculator') as HTMLElement;
|
|
103
|
+
const ui = JSON.parse(section.dataset.ui as string);
|
|
104
|
+
|
|
105
|
+
interface CycleState { sT: number; dur: number; amp: number; sH: number; mult: number; }
|
|
106
|
+
type Pt = { t: number; h: number; p: string };
|
|
107
|
+
|
|
108
|
+
class TideCalculator {
|
|
109
|
+
private highTime: HTMLInputElement;
|
|
110
|
+
private highHeight: HTMLInputElement;
|
|
111
|
+
private lowTime: HTMLInputElement;
|
|
112
|
+
private lowHeight: HTMLInputElement;
|
|
113
|
+
private targetTime: HTMLInputElement;
|
|
114
|
+
private invertBtn: HTMLElement;
|
|
115
|
+
private displayValue: HTMLElement;
|
|
116
|
+
private displayStatus: HTMLElement;
|
|
117
|
+
private displayAmp: HTMLElement;
|
|
118
|
+
private displayDur: HTMLElement;
|
|
119
|
+
private tableBody: HTMLElement;
|
|
120
|
+
private chart: InstanceType<typeof Chart>;
|
|
121
|
+
|
|
122
|
+
constructor() {
|
|
123
|
+
this.highTime = document.getElementById('high-tide-time') as HTMLInputElement;
|
|
124
|
+
this.highHeight = document.getElementById('high-tide-height') as HTMLInputElement;
|
|
125
|
+
this.lowTime = document.getElementById('low-tide-time') as HTMLInputElement;
|
|
126
|
+
this.lowHeight = document.getElementById('low-tide-height') as HTMLInputElement;
|
|
127
|
+
this.targetTime = document.getElementById('target-time') as HTMLInputElement;
|
|
128
|
+
this.invertBtn = document.getElementById('invert-btn') as HTMLElement;
|
|
129
|
+
this.displayValue = document.getElementById('target-value') as HTMLElement;
|
|
130
|
+
this.displayStatus = document.getElementById('tide-status') as HTMLElement;
|
|
131
|
+
this.displayAmp = document.getElementById('amplitude-val') as HTMLElement;
|
|
132
|
+
this.displayDur = document.getElementById('duration-val') as HTMLElement;
|
|
133
|
+
this.tableBody = document.querySelector('#twelfths-table tbody') as HTMLElement;
|
|
134
|
+
this.chart = this.initChart();
|
|
135
|
+
this.init();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private init() {
|
|
139
|
+
[this.highTime, this.highHeight, this.lowTime, this.lowHeight, this.targetTime].forEach(
|
|
140
|
+
(i) => i.addEventListener('input', () => this.calculate()),
|
|
141
|
+
);
|
|
142
|
+
this.invertBtn.addEventListener('click', () => this.invert());
|
|
143
|
+
this.calculate();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private invert() {
|
|
147
|
+
const hT = this.highTime.value;
|
|
148
|
+
const hH = this.highHeight.value;
|
|
149
|
+
this.highTime.value = this.lowTime.value;
|
|
150
|
+
this.highHeight.value = this.lowHeight.value;
|
|
151
|
+
this.lowTime.value = hT;
|
|
152
|
+
this.lowHeight.value = hH;
|
|
153
|
+
this.calculate();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private timeToMins(s: string) {
|
|
157
|
+
const [h, m] = s.split(':').map(Number);
|
|
158
|
+
return h * 60 + m;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private minsToTime(m: number) {
|
|
162
|
+
let mins = Math.round(m) % 1440;
|
|
163
|
+
if (mins < 0) mins += 1440;
|
|
164
|
+
return `${String(Math.floor(mins / 60)).padStart(2, '0')}:${String(mins % 60).padStart(2, '0')}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private calculate() {
|
|
168
|
+
const tH = this.timeToMins(this.highTime.value);
|
|
169
|
+
const hH = parseFloat(this.highHeight.value);
|
|
170
|
+
const tL = this.timeToMins(this.lowTime.value);
|
|
171
|
+
const hL = parseFloat(this.lowHeight.value);
|
|
172
|
+
const tT = this.timeToMins(this.targetTime.value);
|
|
173
|
+
if (isNaN(hH) || isNaN(hL)) return;
|
|
174
|
+
const isFlooding = tH > tL;
|
|
175
|
+
const state: CycleState = {
|
|
176
|
+
sT: isFlooding ? tL : tH,
|
|
177
|
+
dur: Math.abs(tH - tL),
|
|
178
|
+
amp: Math.abs(hH - hL),
|
|
179
|
+
sH: isFlooding ? hL : hH,
|
|
180
|
+
mult: isFlooding ? 1 : -1,
|
|
181
|
+
};
|
|
182
|
+
this.displayAmp.textContent = `${state.amp.toFixed(2)} m`;
|
|
183
|
+
this.displayDur.textContent = `${Math.floor(state.dur / 60)}h ${state.dur % 60}m`;
|
|
184
|
+
const pts = this.generateCycle(state);
|
|
185
|
+
this.updateUI(pts, tT, state);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private generateCycle(state: CycleState): Pt[] {
|
|
189
|
+
const { sT, dur, amp, sH, mult } = state;
|
|
190
|
+
const step = dur / 6;
|
|
191
|
+
const doz = [1 / 12, 2 / 12, 3 / 12, 3 / 12, 2 / 12, 1 / 12];
|
|
192
|
+
const pts: Pt[] = [{ t: sT, h: sH, p: '0%' }];
|
|
193
|
+
let cum = 0;
|
|
194
|
+
for (let i = 0; i < 6; i++) {
|
|
195
|
+
cum += doz[i];
|
|
196
|
+
pts.push({ t: sT + step * (i + 1), h: sH + cum * amp * mult, p: `${Math.round(cum * 100)}%` });
|
|
197
|
+
}
|
|
198
|
+
return pts;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private getRowLabel(i: number): string {
|
|
202
|
+
if (i === 0) return ui.tableStartLabel;
|
|
203
|
+
if (i === 6) return ui.tableEndLabel;
|
|
204
|
+
return '---';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private updateStatus(tT: number, state: CycleState) {
|
|
208
|
+
const { sT, dur, amp, sH, mult } = state;
|
|
209
|
+
const rel = tT - sT;
|
|
210
|
+
if (rel >= 0 && rel <= dur) {
|
|
211
|
+
const step = dur / 6;
|
|
212
|
+
const idx = Math.floor(rel / step);
|
|
213
|
+
const doz = [1 / 12, 2 / 12, 3 / 12, 3 / 12, 2 / 12, 1 / 12];
|
|
214
|
+
let cum = 0;
|
|
215
|
+
for (let i = 0; i < idx; i++) cum += doz[i];
|
|
216
|
+
const rem = (rel % step) / step;
|
|
217
|
+
const h = sH + (cum + (idx < 6 ? (doz[idx] ?? 0) : 0) * rem) * amp * mult;
|
|
218
|
+
this.displayValue.textContent = h.toFixed(2);
|
|
219
|
+
this.displayStatus.textContent = mult > 0 ? ui.statusUpLabel : ui.statusDownLabel;
|
|
220
|
+
this.displayStatus.className = `tc-status-indicator ${mult > 0 ? 'up' : 'down'}`;
|
|
221
|
+
} else {
|
|
222
|
+
this.displayValue.textContent = '--';
|
|
223
|
+
this.displayStatus.textContent = ui.statusOutOfRange;
|
|
224
|
+
this.displayStatus.className = 'tc-status-indicator';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private updateTable(pts: Pt[]) {
|
|
229
|
+
this.tableBody.innerHTML = pts
|
|
230
|
+
.map((p, i) => `<tr>
|
|
231
|
+
<td>${this.minsToTime(p.t)}</td>
|
|
232
|
+
<td>${this.getRowLabel(i)}</td>
|
|
233
|
+
<td>${p.p}</td>
|
|
234
|
+
<td>${p.h.toFixed(2)}m</td>
|
|
235
|
+
</tr>`)
|
|
236
|
+
.join('');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private updateUI(pts: Pt[], tT: number, state: CycleState) {
|
|
240
|
+
this.updateStatus(tT, state);
|
|
241
|
+
this.updateTable(pts);
|
|
242
|
+
this.updateChart(pts, tT, state);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private getChartDatasets() {
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
data: [] as number[],
|
|
249
|
+
borderColor: '#0074B7',
|
|
250
|
+
borderWidth: 3,
|
|
251
|
+
pointRadius: 0,
|
|
252
|
+
tension: 0.4,
|
|
253
|
+
fill: true,
|
|
254
|
+
backgroundColor: (context: { chart: { ctx: CanvasRenderingContext2D; chartArea: { top: number; bottom: number } } }) => {
|
|
255
|
+
const { ctx: c, chartArea } = context.chart;
|
|
256
|
+
if (!chartArea) return undefined;
|
|
257
|
+
const g = c.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
|
|
258
|
+
g.addColorStop(0, 'rgba(0, 116, 183, 0.2)');
|
|
259
|
+
g.addColorStop(1, 'rgba(0, 116, 183, 0)');
|
|
260
|
+
return g;
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
data: [] as object[],
|
|
265
|
+
pointRadius: 6,
|
|
266
|
+
pointBackgroundColor: '#FF6B35',
|
|
267
|
+
pointBorderColor: '#ffffff',
|
|
268
|
+
pointBorderWidth: 2,
|
|
269
|
+
showLine: false,
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private initChart() {
|
|
275
|
+
const ctx = document.getElementById('tide-chart') as HTMLCanvasElement;
|
|
276
|
+
return new Chart(ctx, {
|
|
277
|
+
type: 'line',
|
|
278
|
+
data: { labels: [], datasets: this.getChartDatasets() },
|
|
279
|
+
options: {
|
|
280
|
+
responsive: true,
|
|
281
|
+
maintainAspectRatio: false,
|
|
282
|
+
plugins: { legend: { display: false } },
|
|
283
|
+
scales: {
|
|
284
|
+
x: { display: false },
|
|
285
|
+
y: { display: true, grid: { color: 'rgba(0,0,0,0.05)' } },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private updateChart(pts: Pt[], tT: number, state: CycleState) {
|
|
292
|
+
const { sT, dur, amp, sH, mult } = state;
|
|
293
|
+
const labels = pts.map((p) => this.minsToTime(p.t));
|
|
294
|
+
this.chart.data.labels = labels;
|
|
295
|
+
this.chart.data.datasets[0].data = pts.map((p) => p.h);
|
|
296
|
+
const rel = tT - sT;
|
|
297
|
+
if (rel >= 0 && rel <= dur) {
|
|
298
|
+
const step = dur / 6;
|
|
299
|
+
const idx = Math.floor(rel / step);
|
|
300
|
+
const doz = [1 / 12, 2 / 12, 3 / 12, 3 / 12, 2 / 12, 1 / 12];
|
|
301
|
+
let cum = 0;
|
|
302
|
+
for (let i = 0; i < idx; i++) cum += doz[i];
|
|
303
|
+
const rem = (rel % step) / step;
|
|
304
|
+
const h = sH + (cum + (idx < 6 ? (doz[idx] ?? 0) : 0) * rem) * amp * mult;
|
|
305
|
+
this.chart.data.datasets[1].data = [{ x: labels[Math.round(rel / step)], y: h }];
|
|
306
|
+
} else {
|
|
307
|
+
this.chart.data.datasets[1].data = [];
|
|
308
|
+
}
|
|
309
|
+
this.chart.update();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
new TideCalculator();
|
|
314
|
+
</script>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { TideCalculatorUI, TideCalculatorLocaleContent } from '../index';
|
|
3
|
+
|
|
4
|
+
const slug = 'tide-height-calculator';
|
|
5
|
+
const title = 'Tide Height Calculator';
|
|
6
|
+
const description =
|
|
7
|
+
'Estimate water depth at any point in the tidal cycle using the nautical Rule of Twelfths. Ideal for coastal planning and nautical licence study.';
|
|
8
|
+
|
|
9
|
+
const ui: TideCalculatorUI = {
|
|
10
|
+
parametersLabel: 'Parameters',
|
|
11
|
+
highTideLabel: 'High Tide',
|
|
12
|
+
lowTideLabel: 'Low Tide',
|
|
13
|
+
targetTimeLabel: 'Query time',
|
|
14
|
+
estimatedHeightLabel: 'Estimated Height',
|
|
15
|
+
metersLabel: 'metres',
|
|
16
|
+
amplitudeLabel: 'Amplitude',
|
|
17
|
+
durationLabel: 'Duration',
|
|
18
|
+
tableBreakdownLabel: 'Twelfths Breakdown',
|
|
19
|
+
tableHourLabel: 'Time',
|
|
20
|
+
tableStateLabel: 'State',
|
|
21
|
+
tableStartLabel: 'Start',
|
|
22
|
+
tableEndLabel: 'End',
|
|
23
|
+
statusUpLabel: 'Rising',
|
|
24
|
+
statusDownLabel: 'Falling',
|
|
25
|
+
statusOutOfRange: 'Out of range',
|
|
26
|
+
faqTitle: 'Frequently Asked Questions',
|
|
27
|
+
bibliographyTitle: 'Bibliography',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const faq: TideCalculatorLocaleContent['faq'] = [
|
|
31
|
+
{
|
|
32
|
+
question: 'What is the Rule of Twelfths and what is it used for?',
|
|
33
|
+
answer:
|
|
34
|
+
'The Rule of Twelfths is a simplified mathematical method used in navigation to estimate the tide height at any point between high and low water. It is based on the observation that tidal flow follows an approximate sinusoidal curve, dividing the total range into 12 parts over 6 hours (1/12, 2/12, 3/12, 3/12, 2/12, 1/12).',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
question: 'Is this method reliable for real navigation?',
|
|
38
|
+
answer:
|
|
39
|
+
'It is a very useful theoretical estimation method for quick planning and educational purposes. However, it does not account for meteorological factors such as atmospheric pressure or wind, which can alter sea level by several decimetres. For real navigation, always consult official tide tables.',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
question: 'What data do I need to calculate tide height?',
|
|
43
|
+
answer:
|
|
44
|
+
'You need the time and height (in metres) of the high tide and low tide closest to the moment you wish to query. This data is found in tide almanacs or marine weather applications.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
question: 'Why does the tide not always rise at the same speed?',
|
|
48
|
+
answer:
|
|
49
|
+
'Due to the inertia of large water masses and the configuration of ocean basins. The tide begins to rise slowly after low water, reaches maximum speed in the middle hours (the 3/12 hours) and slows again as it approaches high water.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
question: 'How does atmospheric pressure affect the tide?',
|
|
53
|
+
answer:
|
|
54
|
+
'High pressure (anticyclone) pushes water down, reducing the actual height. Low pressure (depression) allows water to rise higher. As a general rule, a variation of 1 hPa from the mean corresponds to approximately 1 cm change in sea level.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
question: 'What is tidal range?',
|
|
58
|
+
answer:
|
|
59
|
+
'It is the vertical difference in height between a high tide and the immediately preceding or following low tide. This measurement varies according to lunar phase (spring and neap tides) and geographical location.',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const howTo: TideCalculatorLocaleContent['howTo'] = [
|
|
64
|
+
{
|
|
65
|
+
name: 'Identify high and low tide',
|
|
66
|
+
text: 'Consult your tide almanac to obtain the times and heights for the tidal cycle you are interested in.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Enter the data in the calculator',
|
|
70
|
+
text: 'Input the time and height of high tide and low tide. The tool will determine duration and amplitude automatically.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Set the query time',
|
|
74
|
+
text: 'Adjust the time for which you want to know the water level. You will see the exact point highlighted on the chart.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Analyse the twelfths table',
|
|
78
|
+
text: 'Review the hour-by-hour breakdown to understand how the water level will fluctuate throughout the entire period.',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const bibliography: TideCalculatorLocaleContent['bibliography'] = [
|
|
83
|
+
{
|
|
84
|
+
name: 'Puertos del Estado - Tide Prediction',
|
|
85
|
+
url: 'https://portus.puertos.es/#/',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Instituto Hidrografico de la Marina',
|
|
89
|
+
url: 'https://armada.defensa.gob.es/ihm/',
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const seo: TideCalculatorLocaleContent['seo'] = [
|
|
94
|
+
{
|
|
95
|
+
type: 'title',
|
|
96
|
+
text: 'Mastering the Tide: The Rule of Twelfths in Navigation',
|
|
97
|
+
level: 2,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'paragraph',
|
|
101
|
+
html: 'Coastal navigation and entering harbours with restricted depth require precise knowledge of water depth at all times. Although digital applications and GPS providing real-time data are available today, the <strong>Rule of Twelfths</strong> remains a fundamental tool in the training of any recreational craft skipper or yacht captain. This rule is a mathematical estimation method that allows you to calculate, approximately and without complex calculators, the tide height at any point in a tidal cycle.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'title',
|
|
105
|
+
text: 'What exactly is the Rule of Twelfths?',
|
|
106
|
+
level: 3,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: 'paragraph',
|
|
110
|
+
html: 'The Rule of Twelfths is based on the observation that tidal flow is not constant. Water does not rise or fall at the same speed during the approximately six hours between high water and low water. Instead, the movement follows a sinusoidal curve. During the first hour the tide moves slowly; it accelerates in the middle hours and slows again as it approaches the next extreme.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: 'table',
|
|
114
|
+
headers: ['Hour of Cycle', 'Proportion', 'Flow State'],
|
|
115
|
+
rows: [
|
|
116
|
+
['<strong>1st Hour</strong>', '1/12 of the range', 'Slow start (Slack water)'],
|
|
117
|
+
['<strong>2nd Hour</strong>', '2/12 of the range', 'Accelerating flow'],
|
|
118
|
+
['<strong>3rd Hour</strong>', '3/12 of the range', 'Maximum flow (Strong current)'],
|
|
119
|
+
['<strong>4th Hour</strong>', '3/12 of the range', 'Sustained flow'],
|
|
120
|
+
['<strong>5th Hour</strong>', '2/12 of the range', 'Notable deceleration'],
|
|
121
|
+
['<strong>6th Hour</strong>', '1/12 of the range', 'Final slowing (Towards slack water)'],
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'title',
|
|
126
|
+
text: 'Technical limitations: Factors affecting the real tide',
|
|
127
|
+
level: 3,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'glossary',
|
|
131
|
+
items: [
|
|
132
|
+
{ term: 'Atmospheric pressure', definition: 'A depression raises sea level; an anticyclone lowers it. As a general rule, ~1 cm variation per hPa difference from the mean.' },
|
|
133
|
+
{ term: 'Coastal wind', definition: 'Sustained onshore winds can pile water above predicted levels in the tide tables.' },
|
|
134
|
+
{ term: 'Local topography', definition: 'Coastal shape, seabed depth and the presence of estuaries can significantly delay or accelerate tidal flow.' },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: 'tip',
|
|
139
|
+
title: 'Safety Note',
|
|
140
|
+
html: 'This tool uses a mathematical estimation method. It does not account for meteorological factors or local topography. For real navigation, always consult the official tide tables.',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'title',
|
|
144
|
+
text: 'Key concepts for new sailors',
|
|
145
|
+
level: 3,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'glossary',
|
|
149
|
+
items: [
|
|
150
|
+
{
|
|
151
|
+
term: 'Tidal range',
|
|
152
|
+
definition: 'Vertical difference between high water and low water. In the Bay of Biscay it can be several metres; in the Mediterranean it is almost negligible.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
term: 'Tidal duration',
|
|
156
|
+
definition: 'Time interval between high water and the next low water. Typically around 6 hours and 12 minutes (semi-diurnal cycle).',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
term: 'Chart datum',
|
|
160
|
+
definition: 'Reference level from which all depths on nautical charts are measured. Actual depths are almost never less than those indicated.',
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const schemas: TideCalculatorLocaleContent['schemas'] = [
|
|
167
|
+
{
|
|
168
|
+
'@context': 'https://schema.org',
|
|
169
|
+
'@type': 'SoftwareApplication',
|
|
170
|
+
name: title,
|
|
171
|
+
description,
|
|
172
|
+
applicationCategory: 'UtilityApplication',
|
|
173
|
+
operatingSystem: 'Web',
|
|
174
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
175
|
+
} as WithContext<SoftwareApplication>,
|
|
176
|
+
{
|
|
177
|
+
'@context': 'https://schema.org',
|
|
178
|
+
'@type': 'FAQPage',
|
|
179
|
+
mainEntity: faq.map((item) => ({
|
|
180
|
+
'@type': 'Question',
|
|
181
|
+
name: item.question,
|
|
182
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
183
|
+
})),
|
|
184
|
+
} as WithContext<FAQPage>,
|
|
185
|
+
{
|
|
186
|
+
'@context': 'https://schema.org',
|
|
187
|
+
'@type': 'HowTo',
|
|
188
|
+
name: `How to use: ${title}`,
|
|
189
|
+
step: howTo.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
|
|
190
|
+
} as WithContext<HowTo>,
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
export const content: TideCalculatorLocaleContent = {
|
|
194
|
+
slug,
|
|
195
|
+
title,
|
|
196
|
+
description,
|
|
197
|
+
ui,
|
|
198
|
+
seo,
|
|
199
|
+
faq,
|
|
200
|
+
bibliography,
|
|
201
|
+
howTo,
|
|
202
|
+
schemas,
|
|
203
|
+
};
|