@neaps/tide-predictor 0.0.4

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 (46) hide show
  1. package/.eslintrc.js +22 -0
  2. package/.github/workflows/test.yml +15 -0
  3. package/.prettierrc +4 -0
  4. package/Gruntfile.js +87 -0
  5. package/LICENSE +21 -0
  6. package/README.md +199 -0
  7. package/babel.config.js +9 -0
  8. package/dist/tide-predictor.js +1013 -0
  9. package/examples/browser/index.html +51 -0
  10. package/jest.config.js +14 -0
  11. package/lib/astronomy/coefficients.js +31 -0
  12. package/lib/astronomy/constants.js +10 -0
  13. package/lib/astronomy/index.js +199 -0
  14. package/lib/constituents/compound-constituent.js +67 -0
  15. package/lib/constituents/constituent.js +74 -0
  16. package/lib/constituents/index.js +140 -0
  17. package/lib/harmonics/index.js +113 -0
  18. package/lib/harmonics/prediction.js +195 -0
  19. package/lib/index.es6.js +1005 -0
  20. package/lib/index.js +53 -0
  21. package/lib/node-corrections/index.js +147 -0
  22. package/package.json +45 -0
  23. package/rollup.config.js +21 -0
  24. package/src/__mocks__/constituents.js +335 -0
  25. package/src/__mocks__/secondary-station.js +11 -0
  26. package/src/__tests__/index.js +81 -0
  27. package/src/__tests__/noaa.js +92 -0
  28. package/src/astronomy/__tests__/coefficients.js +12 -0
  29. package/src/astronomy/__tests__/index.js +96 -0
  30. package/src/astronomy/coefficients.js +72 -0
  31. package/src/astronomy/constants.js +4 -0
  32. package/src/astronomy/index.js +201 -0
  33. package/src/constituents/__tests__/compound-constituent.js +44 -0
  34. package/src/constituents/__tests__/constituent.js +65 -0
  35. package/src/constituents/__tests__/index.js +34 -0
  36. package/src/constituents/compound-constituent.js +55 -0
  37. package/src/constituents/constituent.js +74 -0
  38. package/src/constituents/index.js +119 -0
  39. package/src/harmonics/__mocks__/water-levels.js +0 -0
  40. package/src/harmonics/__tests__/index.js +123 -0
  41. package/src/harmonics/__tests__/prediction.js +148 -0
  42. package/src/harmonics/index.js +87 -0
  43. package/src/harmonics/prediction.js +175 -0
  44. package/src/index.js +45 -0
  45. package/src/node-corrections/__tests__/index.js +114 -0
  46. package/src/node-corrections/index.js +208 -0
@@ -0,0 +1,1005 @@
1
+ const d2r = Math.PI / 180.0;
2
+ const r2d = 180.0 / Math.PI;
3
+
4
+ // Convert a sexagesimal angle into decimal degrees
5
+ const sexagesimalToDecimal = (degrees, arcmins, arcsecs, mas, muas) => {
6
+ arcmins = typeof arcmins !== 'undefined' ? arcmins : 0;
7
+ arcsecs = typeof arcsecs !== 'undefined' ? arcsecs : 0;
8
+ mas = typeof mas !== 'undefined' ? mas : 0;
9
+ muas = typeof muas !== 'undefined' ? muas : 0;
10
+
11
+ return (
12
+ degrees +
13
+ arcmins / 60.0 +
14
+ arcsecs / (60.0 * 60.0) +
15
+ mas / (60.0 * 60.0 * 1e3) +
16
+ muas / (60.0 * 60.0 * 1e6)
17
+ )
18
+ };
19
+
20
+ const coefficients = {
21
+ // Meeus formula 21.3
22
+ terrestrialObliquity: [
23
+ sexagesimalToDecimal(23, 26, 21.448),
24
+ -sexagesimalToDecimal(0, 0, 4680.93),
25
+ -sexagesimalToDecimal(0, 0, 1.55),
26
+ sexagesimalToDecimal(0, 0, 1999.25),
27
+ -sexagesimalToDecimal(0, 0, 51.38),
28
+ -sexagesimalToDecimal(0, 0, 249.67),
29
+ -sexagesimalToDecimal(0, 0, 39.05),
30
+ sexagesimalToDecimal(0, 0, 7.12),
31
+ sexagesimalToDecimal(0, 0, 27.87),
32
+ sexagesimalToDecimal(0, 0, 5.79),
33
+ sexagesimalToDecimal(0, 0, 2.45)
34
+ ].map((number, index) => {
35
+ return number * Math.pow(1e-2, index)
36
+ }),
37
+
38
+ solarPerigee: [
39
+ 280.46645 - 357.5291,
40
+ 36000.76932 - 35999.0503,
41
+ 0.0003032 + 0.0001559,
42
+ 0.00000048
43
+ ],
44
+
45
+ solarLongitude: [280.46645, 36000.76983, 0.0003032],
46
+
47
+ lunarInclination: [5.145],
48
+
49
+ lunarLongitude: [
50
+ 218.3164591,
51
+ 481267.88134236,
52
+ -0.0013268,
53
+ 1 / 538841.0 - 1 / 65194000.0
54
+ ],
55
+
56
+ lunarNode: [
57
+ 125.044555,
58
+ -1934.1361849,
59
+ 0.0020762,
60
+ 1 / 467410.0,
61
+ -1 / 60616000.0
62
+ ],
63
+
64
+ lunarPerigee: [
65
+ 83.353243,
66
+ 4069.0137111,
67
+ -0.0103238,
68
+ -1 / 80053.0,
69
+ 1 / 18999000.0
70
+ ]
71
+ };
72
+
73
+ // Evaluates a polynomial at argument
74
+ const polynomial = (coefficients, argument) => {
75
+ const result = [];
76
+ coefficients.forEach((coefficient, index) => {
77
+ result.push(coefficient * Math.pow(argument, index));
78
+ });
79
+ return result.reduce((a, b) => {
80
+ return a + b
81
+ })
82
+ };
83
+
84
+ // Evaluates a derivative polynomial at argument
85
+ const derivativePolynomial = (coefficients, argument) => {
86
+ const result = [];
87
+ coefficients.forEach((coefficient, index) => {
88
+ result.push(coefficient * index * Math.pow(argument, index - 1));
89
+ });
90
+ return result.reduce((a, b) => {
91
+ return a + b
92
+ })
93
+ };
94
+
95
+ // Meeus formula 11.1
96
+ const T = t => {
97
+ return (JD(t) - 2451545.0) / 36525
98
+ };
99
+
100
+ // Meeus formula 7.1
101
+ const JD = t => {
102
+ let Y = t.getFullYear();
103
+ let M = t.getMonth() + 1;
104
+ const D =
105
+ t.getDate() +
106
+ t.getHours() / 24.0 +
107
+ t.getMinutes() / (24.0 * 60.0) +
108
+ t.getSeconds() / (24.0 * 60.0 * 60.0) +
109
+ t.getMilliseconds() / (24.0 * 60.0 * 60.0 * 1e6);
110
+ if (M <= 2) {
111
+ Y = Y - 1;
112
+ M = M + 12;
113
+ }
114
+ const A = Math.floor(Y / 100.0);
115
+ const B = 2 - A + Math.floor(A / 4.0);
116
+ return (
117
+ Math.floor(365.25 * (Y + 4716)) +
118
+ Math.floor(30.6001 * (M + 1)) +
119
+ D +
120
+ B -
121
+ 1524.5
122
+ )
123
+ };
124
+
125
+ /**
126
+ * @todo - What's with the array returned from the arccos?
127
+ * @param {*} N
128
+ * @param {*} i
129
+ * @param {*} omega
130
+ */
131
+ const _I = (N, i, omega) => {
132
+ N = d2r * N;
133
+ i = d2r * i;
134
+ omega = d2r * omega;
135
+ const cosI =
136
+ Math.cos(i) * Math.cos(omega) - Math.sin(i) * Math.sin(omega) * Math.cos(N);
137
+ return r2d * Math.acos(cosI)
138
+ };
139
+
140
+ const _xi = (N, i, omega) => {
141
+ N = d2r * N;
142
+ i = d2r * i;
143
+ omega = d2r * omega;
144
+ let e1 =
145
+ (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
146
+ Math.tan(0.5 * N);
147
+ let e2 =
148
+ (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
149
+ Math.tan(0.5 * N);
150
+ e1 = Math.atan(e1);
151
+ e2 = Math.atan(e2);
152
+ e1 = e1 - 0.5 * N;
153
+ e2 = e2 - 0.5 * N;
154
+ return -(e1 + e2) * r2d
155
+ };
156
+
157
+ const _nu = (N, i, omega) => {
158
+ N = d2r * N;
159
+ i = d2r * i;
160
+ omega = d2r * omega;
161
+ let e1 =
162
+ (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
163
+ Math.tan(0.5 * N);
164
+ let e2 =
165
+ (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
166
+ Math.tan(0.5 * N);
167
+ e1 = Math.atan(e1);
168
+ e2 = Math.atan(e2);
169
+ e1 = e1 - 0.5 * N;
170
+ e2 = e2 - 0.5 * N;
171
+ return (e1 - e2) * r2d
172
+ };
173
+
174
+ // Schureman equation 224
175
+ const _nup = (N, i, omega) => {
176
+ const I = d2r * _I(N, i, omega);
177
+ const nu = d2r * _nu(N, i, omega);
178
+ return (
179
+ r2d *
180
+ Math.atan(
181
+ (Math.sin(2 * I) * Math.sin(nu)) /
182
+ (Math.sin(2 * I) * Math.cos(nu) + 0.3347)
183
+ )
184
+ )
185
+ };
186
+
187
+ // Schureman equation 232
188
+ const _nupp = (N, i, omega) => {
189
+ const I = d2r * _I(N, i, omega);
190
+ const nu = d2r * _nu(N, i, omega);
191
+ const tan2nupp =
192
+ (Math.sin(I) ** 2 * Math.sin(2 * nu)) /
193
+ (Math.sin(I) ** 2 * Math.cos(2 * nu) + 0.0727);
194
+ return r2d * 0.5 * Math.atan(tan2nupp)
195
+ };
196
+
197
+ const modulus = (a, b) => {
198
+ return ((a % b) + b) % b
199
+ };
200
+
201
+ const astro = time => {
202
+ const result = {};
203
+ const polynomials = {
204
+ s: coefficients.lunarLongitude,
205
+ h: coefficients.solarLongitude,
206
+ p: coefficients.lunarPerigee,
207
+ N: coefficients.lunarNode,
208
+ pp: coefficients.solarPerigee,
209
+ 90: [90.0],
210
+ omega: coefficients.terrestrialObliquity,
211
+ i: coefficients.lunarInclination
212
+ };
213
+
214
+ // Polynomials are in T, that is Julian Centuries; we want our speeds to be
215
+ // in the more convenient unit of degrees per hour.
216
+ const dTdHour = 1 / (24 * 365.25 * 100);
217
+ Object.keys(polynomials).forEach(name => {
218
+ result[name] = {
219
+ value: modulus(polynomial(polynomials[name], T(time)), 360.0),
220
+ speed: derivativePolynomial(polynomials[name], T(time)) * dTdHour
221
+ };
222
+ });
223
+
224
+ // Some other parameters defined by Schureman which are dependent on the
225
+ // parameters N, i, omega for use in node factor calculations. We don't need
226
+ // their speeds.
227
+ const functions = {
228
+ I: _I,
229
+ xi: _xi,
230
+ nu: _nu,
231
+ nup: _nup,
232
+ nupp: _nupp
233
+ };
234
+ Object.keys(functions).forEach(name => {
235
+ const functionCall = functions[name];
236
+ result[name] = {
237
+ value: modulus(
238
+ functionCall(result.N.value, result.i.value, result.omega.value),
239
+ 360.0
240
+ ),
241
+ speed: null
242
+ };
243
+ });
244
+
245
+ // We don't work directly with the T (hours) parameter, instead our spanning
246
+ // set for equilibrium arguments #is given by T+h-s, s, h, p, N, pp, 90.
247
+ // This is in line with convention.
248
+ const hour = {
249
+ value: (JD(time) - Math.floor(JD(time))) * 360.0,
250
+ speed: 15.0
251
+ };
252
+
253
+ result['T+h-s'] = {
254
+ value: hour.value + result.h.value - result.s.value,
255
+ speed: hour.speed + result.h.speed - result.s.speed
256
+ };
257
+
258
+ // It is convenient to calculate Schureman's P here since several node
259
+ // factors need it, although it could be argued that these
260
+ // (along with I, xi, nu etc) belong somewhere else.
261
+ result.P = {
262
+ value: result.p.value - (result.xi.value % 360.0),
263
+ speed: null
264
+ };
265
+
266
+ return result
267
+ };
268
+
269
+ const modulus$1 = (a, b) => {
270
+ return ((a % b) + b) % b
271
+ };
272
+
273
+ const addExtremesOffsets = (extreme, offsets) => {
274
+ if (typeof offsets === 'undefined' || !offsets) {
275
+ return extreme
276
+ }
277
+ if (extreme.high && offsets.height_offset && offsets.height_offset.high) {
278
+ extreme.level *= offsets.height_offset.high;
279
+ }
280
+ if (extreme.low && offsets.height_offset && offsets.height_offset.low) {
281
+ extreme.level *= offsets.height_offset.low;
282
+ }
283
+ if (extreme.high && offsets.time_offset && offsets.time_offset.high) {
284
+ extreme.time = new Date(
285
+ extreme.time.getTime() + offsets.time_offset.high * 60 * 1000
286
+ );
287
+ }
288
+ if (extreme.low && offsets.time_offset && offsets.time_offset.low) {
289
+ extreme.time = new Date(
290
+ extreme.time.getTime() + offsets.time_offset.low * 60 * 1000
291
+ );
292
+ }
293
+ return extreme
294
+ };
295
+
296
+ const getExtremeLabel = (label, highLowLabels) => {
297
+ if (
298
+ typeof highLowLabels !== 'undefined' &&
299
+ typeof highLowLabels[label] !== 'undefined'
300
+ ) {
301
+ return highLowLabels[label]
302
+ }
303
+ const labels = {
304
+ high: 'High',
305
+ low: 'Low'
306
+ };
307
+ return labels[label]
308
+ };
309
+
310
+ const predictionFactory = ({ timeline, constituents, start }) => {
311
+ const getLevel = (hour, modelBaseSpeed, modelU, modelF, modelBaseValue) => {
312
+ const amplitudes = [];
313
+ let result = 0;
314
+
315
+ constituents.forEach(constituent => {
316
+ const amplitude = constituent.amplitude;
317
+ const phase = constituent._phase;
318
+ const f = modelF[constituent.name];
319
+ const speed = modelBaseSpeed[constituent.name];
320
+ const u = modelU[constituent.name];
321
+ const V0 = modelBaseValue[constituent.name];
322
+ amplitudes.push(amplitude * f * Math.cos(speed * hour + (V0 + u) - phase));
323
+ });
324
+ // sum up each row
325
+ amplitudes.forEach(item => {
326
+ result += item;
327
+ });
328
+ return result
329
+ };
330
+
331
+ const prediction = {};
332
+
333
+ prediction.getExtremesPrediction = options => {
334
+ const { labels, offsets } = typeof options !== 'undefined' ? options : {};
335
+ const results = [];
336
+ const { baseSpeed, u, f, baseValue } = prepare();
337
+ let goingUp = false;
338
+ let goingDown = false;
339
+ let lastLevel = getLevel(0, baseSpeed, u[0], f[0], baseValue);
340
+ timeline.items.forEach((time, index) => {
341
+ const hour = timeline.hours[index];
342
+ const level = getLevel(hour, baseSpeed, u[index], f[index], baseValue);
343
+ // Compare this level to the last one, if we
344
+ // are changing angle, then the last one was high or low
345
+ if (level > lastLevel && goingDown) {
346
+ results.push(
347
+ addExtremesOffsets(
348
+ {
349
+ time: timeline.items[index - 1],
350
+ level: lastLevel,
351
+ high: false,
352
+ low: true,
353
+ label: getExtremeLabel('low', labels)
354
+ },
355
+ offsets
356
+ )
357
+ );
358
+ }
359
+ if (level < lastLevel && goingUp) {
360
+ results.push(
361
+ addExtremesOffsets(
362
+ {
363
+ time: timeline.items[index - 1],
364
+ level: lastLevel,
365
+ high: true,
366
+ low: false,
367
+ label: getExtremeLabel('high', labels)
368
+ },
369
+ offsets
370
+ )
371
+ );
372
+ }
373
+ if (level > lastLevel) {
374
+ goingUp = true;
375
+ goingDown = false;
376
+ }
377
+ if (level < lastLevel) {
378
+ goingUp = false;
379
+ goingDown = true;
380
+ }
381
+ lastLevel = level;
382
+ });
383
+ return results
384
+ };
385
+
386
+ prediction.getTimelinePrediction = () => {
387
+ const results = [];
388
+ const { baseSpeed, u, f, baseValue } = prepare();
389
+ timeline.items.forEach((time, index) => {
390
+ const hour = timeline.hours[index];
391
+ const prediction = {
392
+ time: time,
393
+ hour: hour,
394
+ level: getLevel(hour, baseSpeed, u[index], f[index], baseValue)
395
+ };
396
+
397
+ results.push(prediction);
398
+ });
399
+ return results
400
+ };
401
+
402
+ const prepare = () => {
403
+ const baseAstro = astro(start);
404
+
405
+ const baseValue = {};
406
+ const baseSpeed = {};
407
+ const u = [];
408
+ const f = [];
409
+ constituents.forEach(constituent => {
410
+ const value = constituent._model.value(baseAstro);
411
+ const speed = constituent._model.speed(baseAstro);
412
+ baseValue[constituent.name] = d2r * value;
413
+ baseSpeed[constituent.name] = d2r * speed;
414
+ });
415
+ timeline.items.forEach(time => {
416
+ const uItem = {};
417
+ const fItem = {};
418
+ const itemAstro = astro(time);
419
+ constituents.forEach(constituent => {
420
+ const constituentU = modulus$1(constituent._model.u(itemAstro), 360);
421
+
422
+ uItem[constituent.name] = d2r * constituentU;
423
+ fItem[constituent.name] = modulus$1(constituent._model.f(itemAstro), 360);
424
+ });
425
+ u.push(uItem);
426
+ f.push(fItem);
427
+ });
428
+
429
+ return {
430
+ baseValue: baseValue,
431
+ baseSpeed: baseSpeed,
432
+ u: u,
433
+ f: f
434
+ }
435
+ };
436
+
437
+ return Object.freeze(prediction)
438
+ };
439
+
440
+ const corrections = {
441
+ fUnity() {
442
+ return 1
443
+ },
444
+
445
+ // Schureman equations 73, 65
446
+ fMm(a) {
447
+ const omega = d2r * a.omega.value;
448
+ const i = d2r * a.i.value;
449
+ const I = d2r * a.I.value;
450
+ const mean =
451
+ (2 / 3.0 - Math.pow(Math.sin(omega), 2)) *
452
+ (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
453
+ return (2 / 3.0 - Math.pow(Math.sin(I), 2)) / mean
454
+ },
455
+
456
+ // Schureman equations 74, 66
457
+ fMf(a) {
458
+ const omega = d2r * a.omega.value;
459
+ const i = d2r * a.i.value;
460
+ const I = d2r * a.I.value;
461
+ const mean = Math.pow(Math.sin(omega), 2) * Math.pow(Math.cos(0.5 * i), 4);
462
+ return Math.pow(Math.sin(I), 2) / mean
463
+ },
464
+
465
+ // Schureman equations 75, 67
466
+ fO1(a) {
467
+ const omega = d2r * a.omega.value;
468
+ const i = d2r * a.i.value;
469
+ const I = d2r * a.I.value;
470
+ const mean =
471
+ Math.sin(omega) *
472
+ Math.pow(Math.cos(0.5 * omega), 2) *
473
+ Math.pow(Math.cos(0.5 * i), 4);
474
+ return (Math.sin(I) * Math.pow(Math.cos(0.5 * I), 2)) / mean
475
+ },
476
+
477
+ // Schureman equations 76, 68
478
+ fJ1(a) {
479
+ const omega = d2r * a.omega.value;
480
+ const i = d2r * a.i.value;
481
+ const I = d2r * a.I.value;
482
+ const mean =
483
+ Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
484
+ return Math.sin(2 * I) / mean
485
+ },
486
+
487
+ // Schureman equations 77, 69
488
+ fOO1(a) {
489
+ const omega = d2r * a.omega.value;
490
+ const i = d2r * a.i.value;
491
+ const I = d2r * a.I.value;
492
+ const mean =
493
+ Math.sin(omega) *
494
+ Math.pow(Math.sin(0.5 * omega), 2) *
495
+ Math.pow(Math.cos(0.5 * i), 4);
496
+ return (Math.sin(I) * Math.pow(Math.sin(0.5 * I), 2)) / mean
497
+ },
498
+
499
+ // Schureman equations 78, 70
500
+ fM2(a) {
501
+ const omega = d2r * a.omega.value;
502
+ const i = d2r * a.i.value;
503
+ const I = d2r * a.I.value;
504
+ const mean =
505
+ Math.pow(Math.cos(0.5 * omega), 4) * Math.pow(Math.cos(0.5 * i), 4);
506
+ return Math.pow(Math.cos(0.5 * I), 4) / mean
507
+ },
508
+
509
+ // Schureman equations 227, 226, 68
510
+ // Should probably eventually include the derivations of the magic numbers (0.5023 etc).
511
+ fK1(a) {
512
+ const omega = d2r * a.omega.value;
513
+ const i = d2r * a.i.value;
514
+ const I = d2r * a.I.value;
515
+ const nu = d2r * a.nu.value;
516
+ const sin2IcosnuMean =
517
+ Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
518
+ const mean = 0.5023 * sin2IcosnuMean + 0.1681;
519
+ return (
520
+ Math.pow(
521
+ 0.2523 * Math.pow(Math.sin(2 * I), 2) +
522
+ 0.1689 * Math.sin(2 * I) * Math.cos(nu) +
523
+ 0.0283,
524
+ 0.5
525
+ ) / mean
526
+ )
527
+ },
528
+
529
+ // Schureman equations 215, 213, 204
530
+ // It can be (and has been) confirmed that the exponent for R_a reads 1/2 via Schureman Table 7
531
+ fL2(a) {
532
+ const P = d2r * a.P.value;
533
+ const I = d2r * a.I.value;
534
+ const rAInv = Math.pow(
535
+ 1 -
536
+ 12 * Math.pow(Math.tan(0.5 * I), 2) * Math.cos(2 * P) +
537
+ 36 * Math.pow(Math.tan(0.5 * I), 4),
538
+ 0.5
539
+ );
540
+ return corrections.fM2(a) * rAInv
541
+ },
542
+
543
+ // Schureman equations 235, 234, 71
544
+ // Again, magic numbers
545
+ fK2(a) {
546
+ const omega = d2r * a.omega.value;
547
+ const i = d2r * a.i.value;
548
+ const I = d2r * a.I.value;
549
+ const nu = d2r * a.nu.value;
550
+ const sinsqIcos2nuMean =
551
+ Math.sin(omega) ** 2 * (1 - (3 / 2.0) * Math.sin(i) ** 2);
552
+ const mean = 0.5023 * sinsqIcos2nuMean + 0.0365;
553
+ return (
554
+ Math.pow(
555
+ 0.2523 * Math.pow(Math.sin(I), 4) +
556
+ 0.0367 * Math.pow(Math.sin(I), 2) * Math.cos(2 * nu) +
557
+ 0.0013,
558
+ 0.5
559
+ ) / mean
560
+ )
561
+ },
562
+ // Schureman equations 206, 207, 195
563
+ fM1(a) {
564
+ const P = d2r * a.P.value;
565
+ const I = d2r * a.I.value;
566
+ const qAInv = Math.pow(
567
+ 0.25 +
568
+ 1.5 *
569
+ Math.cos(I) *
570
+ Math.cos(2 * P) *
571
+ Math.pow(Math.cos(0.5 * I), -0.5) +
572
+ 2.25 * Math.pow(Math.cos(I), 2) * Math.pow(Math.cos(0.5 * I), -4),
573
+ 0.5
574
+ );
575
+ return corrections.fO1(a) * qAInv
576
+ },
577
+
578
+ // See e.g. Schureman equation 149
579
+ fModd(a, n) {
580
+ return Math.pow(corrections.fM2(a), n / 2.0)
581
+ },
582
+
583
+ // Node factors u, see Table 2 of Schureman.
584
+
585
+ uZero(a) {
586
+ return 0.0
587
+ },
588
+
589
+ uMf(a) {
590
+ return -2.0 * a.xi.value
591
+ },
592
+
593
+ uO1(a) {
594
+ return 2.0 * a.xi.value - a.nu.value
595
+ },
596
+
597
+ uJ1(a) {
598
+ return -a.nu.value
599
+ },
600
+
601
+ uOO1(a) {
602
+ return -2.0 * a.xi.value - a.nu.value
603
+ },
604
+
605
+ uM2(a) {
606
+ return 2.0 * a.xi.value - 2.0 * a.nu.value
607
+ },
608
+
609
+ uK1(a) {
610
+ return -a.nup.value
611
+ },
612
+
613
+ // Schureman 214
614
+ uL2(a) {
615
+ const I = d2r * a.I.value;
616
+ const P = d2r * a.P.value;
617
+ const R =
618
+ r2d *
619
+ Math.atan(
620
+ Math.sin(2 * P) /
621
+ ((1 / 6.0) * Math.pow(Math.tan(0.5 * I), -2) - Math.cos(2 * P))
622
+ );
623
+ return 2.0 * a.xi.value - 2.0 * a.nu.value - R
624
+ },
625
+
626
+ uK2(a) {
627
+ return -2.0 * a.nupp.value
628
+ },
629
+
630
+ // Schureman 202
631
+ uM1(a) {
632
+ const I = d2r * a.I.value;
633
+ const P = d2r * a.P.value;
634
+ const Q =
635
+ r2d *
636
+ Math.atan(((5 * Math.cos(I) - 1) / (7 * Math.cos(I) + 1)) * Math.tan(P));
637
+ return a.xi.value - a.nu.value + Q
638
+ },
639
+
640
+ uModd(a, n) {
641
+ return (n / 2.0) * corrections.uM2(a)
642
+ }
643
+ };
644
+
645
+ /**
646
+ * Computes the dot notation of two arrays
647
+ * @param {*} a
648
+ * @param {*} b
649
+ */
650
+ const dotArray = (a, b) => {
651
+ const results = [];
652
+ a.forEach((value, index) => {
653
+ results.push(value * b[index]);
654
+ });
655
+ return results.reduce((total, value) => {
656
+ return total + value
657
+ })
658
+ };
659
+
660
+ const astronimicDoodsonNumber = astro => {
661
+ return [
662
+ astro['T+h-s'],
663
+ astro.s,
664
+ astro.h,
665
+ astro.p,
666
+ astro.N,
667
+ astro.pp,
668
+ astro['90']
669
+ ]
670
+ };
671
+
672
+ const astronomicSpeed = astro => {
673
+ const results = [];
674
+ astronimicDoodsonNumber(astro).forEach(number => {
675
+ results.push(number.speed);
676
+ });
677
+ return results
678
+ };
679
+
680
+ const astronomicValues = astro => {
681
+ const results = [];
682
+ astronimicDoodsonNumber(astro).forEach(number => {
683
+ results.push(number.value);
684
+ });
685
+ return results
686
+ };
687
+
688
+ const constituentFactory = (name, coefficients, u, f) => {
689
+ if (!coefficients) {
690
+ throw new Error('Coefficient must be defined for a constituent')
691
+ }
692
+
693
+ const constituent = {
694
+ name: name,
695
+
696
+ coefficients: coefficients,
697
+
698
+ value: astro => {
699
+ return dotArray(coefficients, astronomicValues(astro))
700
+ },
701
+
702
+ speed(astro) {
703
+ return dotArray(coefficients, astronomicSpeed(astro))
704
+ },
705
+
706
+ u: typeof u !== 'undefined' ? u : corrections.uZero,
707
+
708
+ f: typeof f !== 'undefined' ? f : corrections.fUnity
709
+ };
710
+
711
+ return Object.freeze(constituent)
712
+ };
713
+
714
+ const compoundConstituentFactory = (name, members) => {
715
+ const coefficients = [];
716
+ members.forEach(({ constituent, factor }) => {
717
+ constituent.coefficients.forEach((coefficient, index) => {
718
+ if (typeof coefficients[index] === 'undefined') {
719
+ coefficients[index] = 0;
720
+ }
721
+ coefficients[index] += coefficient * factor;
722
+ });
723
+ });
724
+
725
+ const compoundConstituent = {
726
+ name: name,
727
+
728
+ coefficients: coefficients,
729
+
730
+ speed: astro => {
731
+ let speed = 0;
732
+ members.forEach(({ constituent, factor }) => {
733
+ speed += constituent.speed(astro) * factor;
734
+ });
735
+ return speed
736
+ },
737
+
738
+ value: astro => {
739
+ let value = 0;
740
+ members.forEach(({ constituent, factor }) => {
741
+ value += constituent.value(astro) * factor;
742
+ });
743
+ return value
744
+ },
745
+
746
+ u: astro => {
747
+ let u = 0;
748
+ members.forEach(({ constituent, factor }) => {
749
+ u += constituent.u(astro) * factor;
750
+ });
751
+ return u
752
+ },
753
+
754
+ f: astro => {
755
+ const f = [];
756
+ members.forEach(({ constituent, factor }) => {
757
+ f.push(Math.pow(constituent.f(astro), Math.abs(factor)));
758
+ });
759
+ return f.reduce((previous, value) => {
760
+ return previous * value
761
+ })
762
+ }
763
+ };
764
+
765
+ return Object.freeze(compoundConstituent)
766
+ };
767
+
768
+ const constituents = {};
769
+ // Long Term
770
+ constituents.Z0 = constituentFactory('Z0', [0, 0, 0, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
771
+ constituents.SA = constituentFactory('Sa', [0, 0, 1, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
772
+ constituents.SSA = constituentFactory(
773
+ 'Ssa',
774
+ [0, 0, 2, 0, 0, 0, 0],
775
+ corrections.uZero,
776
+ corrections.fUnity
777
+ );
778
+ constituents.MM = constituentFactory('MM', [0, 1, 0, -1, 0, 0, 0], corrections.uZero, corrections.fMm);
779
+ constituents.MF = constituentFactory('MF', [0, 2, 0, 0, 0, 0, 0], corrections.uMf, corrections.fMf);
780
+ // Diurnals
781
+ constituents.Q1 = constituentFactory('Q1', [1, -2, 0, 1, 0, 0, 1], corrections.uO1, corrections.fO1);
782
+ constituents.O1 = constituentFactory('O1', [1, -1, 0, 0, 0, 0, 1], corrections.uO1, corrections.fO1);
783
+ constituents.K1 = constituentFactory('K1', [1, 1, 0, 0, 0, 0, -1], corrections.uK1, corrections.fK1);
784
+ constituents.J1 = constituentFactory('J1', [1, 2, 0, -1, 0, 0, -1], corrections.uJ1, corrections.fJ1);
785
+ constituents.M1 = constituentFactory('M1', [1, 0, 0, 0, 0, 0, 1], corrections.uM1, corrections.fM1);
786
+ constituents.P1 = constituentFactory('P1', [1, 1, -2, 0, 0, 0, 1], corrections.uZero, corrections.fUnity);
787
+ constituents.S1 = constituentFactory('S1', [1, 1, -1, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
788
+ constituents.OO1 = constituentFactory('OO1', [1, 3, 0, 0, 0, 0, -1], corrections.uOO1, corrections.fOO1);
789
+ // Semi diurnals
790
+ constituents['2N2'] = constituentFactory('2N2', [2, -2, 0, 2, 0, 0, 0], corrections.uM2, corrections.fM2);
791
+ constituents.N2 = constituentFactory('N2', [2, -1, 0, 1, 0, 0, 0], corrections.uM2, corrections.fM2);
792
+ constituents.NU2 = constituentFactory('NU2', [2, -1, 2, -1, 0, 0, 0], corrections.uM2, corrections.fM2);
793
+ constituents.M2 = constituentFactory('M2', [2, 0, 0, 0, 0, 0, 0], corrections.uM2, corrections.fM2);
794
+ constituents.LAM2 = constituentFactory('LAM2', [2, 1, -2, 1, 0, 0, 2], corrections.uM2, corrections.fM2);
795
+ constituents.L2 = constituentFactory('L2', [2, 1, 0, -1, 0, 0, 2], corrections.uL2, corrections.fL2);
796
+ constituents.T2 = constituentFactory('T2', [2, 2, -3, 0, 0, 1, 0], corrections.uZero, corrections.fUnity);
797
+ constituents.S2 = constituentFactory('S2', [2, 2, -2, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
798
+ constituents.R2 = constituentFactory(
799
+ 'R2',
800
+ [2, 2, -1, 0, 0, -1, 2],
801
+ corrections.uZero,
802
+ corrections.fUnity
803
+ );
804
+ constituents.K2 = constituentFactory('K2', [2, 2, 0, 0, 0, 0, 0], corrections.uK2, corrections.fK2);
805
+ // Third diurnal
806
+ constituents.M3 = constituentFactory(
807
+ 'M3',
808
+ [3, 0, 0, 0, 0, 0, 0],
809
+ a => {
810
+ return corrections.uModd(a, 3)
811
+ },
812
+ a => {
813
+ return corrections.fModd(a, 3)
814
+ }
815
+ );
816
+ // Compound
817
+ constituents.MSF = compoundConstituentFactory('MSF', [
818
+ { constituent: constituents.S2, factor: 1 },
819
+ { constituent: constituents.M2, factor: -1 }
820
+ ]);
821
+
822
+ // Diurnal
823
+ constituents['2Q1'] = compoundConstituentFactory('2Q1', [
824
+ { constituent: constituents.N2, factor: 1 },
825
+ { constituent: constituents.J1, factor: -1 }
826
+ ]);
827
+ constituents.RHO = compoundConstituentFactory('RHO', [
828
+ { constituent: constituents.NU2, factor: 1 },
829
+ { constituent: constituents.K1, factor: -1 }
830
+ ]);
831
+
832
+ // Semi-Diurnal
833
+
834
+ constituents.MU2 = compoundConstituentFactory('MU2', [
835
+ { constituent: constituents.M2, factor: 2 },
836
+ { constituent: constituents.S2, factor: -1 }
837
+ ]);
838
+ constituents['2SM2'] = compoundConstituentFactory('2SM2', [
839
+ { constituent: constituents.S2, factor: 2 },
840
+ { constituent: constituents.M2, factor: -1 }
841
+ ]);
842
+
843
+ // Third-Diurnal
844
+ constituents['2MK3'] = compoundConstituentFactory('2MK3', [
845
+ { constituent: constituents.M2, factor: 1 },
846
+ { constituent: constituents.O1, factor: 1 }
847
+ ]);
848
+ constituents.MK3 = compoundConstituentFactory('MK3', [
849
+ { constituent: constituents.M2, factor: 1 },
850
+ { constituent: constituents.K1, factor: 1 }
851
+ ]);
852
+
853
+ // Quarter-Diurnal
854
+ constituents.MN4 = compoundConstituentFactory('MN4', [
855
+ { constituent: constituents.M2, factor: 1 },
856
+ { constituent: constituents.N2, factor: 1 }
857
+ ]);
858
+ constituents.M4 = compoundConstituentFactory('M4', [
859
+ { constituent: constituents.M2, factor: 2 }
860
+ ]);
861
+ constituents.MS4 = compoundConstituentFactory('MS4', [
862
+ { constituent: constituents.M2, factor: 1 },
863
+ { constituent: constituents.S2, factor: 1 }
864
+ ]);
865
+ constituents.S4 = compoundConstituentFactory('S4', [
866
+ { constituent: constituents.S2, factor: 2 }
867
+ ]);
868
+
869
+ // Sixth-Diurnal
870
+ constituents.M6 = compoundConstituentFactory('M6', [
871
+ { constituent: constituents.M2, factor: 3 }
872
+ ]);
873
+ constituents.S6 = compoundConstituentFactory('S6', [
874
+ { constituent: constituents.S2, factor: 3 }
875
+ ]);
876
+
877
+ // Eighth-Diurnals
878
+ constituents.M8 = compoundConstituentFactory('M8', [
879
+ { constituent: constituents.M2, factor: 4 }
880
+ ]);
881
+
882
+ const getDate = time => {
883
+ if (time instanceof Date) {
884
+ return time
885
+ }
886
+ if (typeof time === 'number') {
887
+ return new Date(time * 1000)
888
+ }
889
+ throw new Error('Invalid date format, should be a Date object, or timestamp')
890
+ };
891
+
892
+ const getTimeline = (start, end, seconds) => {
893
+ seconds = typeof seconds !== 'undefined' ? seconds : 10 * 60;
894
+ const timeline = [];
895
+ const endTime = end.getTime() / 1000;
896
+ let lastTime = start.getTime() / 1000;
897
+ const startTime = lastTime;
898
+ const hours = [];
899
+ while (lastTime <= endTime) {
900
+ timeline.push(new Date(lastTime * 1000));
901
+ hours.push((lastTime - startTime) / (60 * 60));
902
+ lastTime += seconds;
903
+ }
904
+
905
+ return {
906
+ items: timeline,
907
+ hours: hours
908
+ }
909
+ };
910
+
911
+ const harmonicsFactory = ({ harmonicConstituents, phaseKey, offset }) => {
912
+ if (!Array.isArray(harmonicConstituents)) {
913
+ throw new Error('Harmonic constituents are not an array')
914
+ }
915
+ const constituents$1 = [];
916
+ harmonicConstituents.forEach((constituent, index) => {
917
+ if (typeof constituent.name === 'undefined') {
918
+ throw new Error('Harmonic constituents must have a name property')
919
+ }
920
+ if (typeof constituents[constituent.name] !== 'undefined') {
921
+ constituent._model = constituents[constituent.name];
922
+ constituent._phase = d2r * constituent[phaseKey];
923
+ constituents$1.push(constituent);
924
+ }
925
+ });
926
+
927
+ if (offset !== false) {
928
+ constituents$1.push({
929
+ name: 'Z0',
930
+ _model: constituents.Z0,
931
+ _phase: 0,
932
+ amplitude: offset
933
+ });
934
+ }
935
+
936
+ let start = new Date();
937
+ let end = new Date();
938
+
939
+ const harmonics = {};
940
+
941
+ harmonics.setTimeSpan = (startTime, endTime) => {
942
+ start = getDate(startTime);
943
+ end = getDate(endTime);
944
+ if (start.getTime() >= end.getTime()) {
945
+ throw new Error('Start time must be before end time')
946
+ }
947
+ return harmonics
948
+ };
949
+
950
+ harmonics.prediction = options => {
951
+ options =
952
+ typeof options !== 'undefined' ? options : { timeFidelity: 10 * 60 };
953
+ return predictionFactory({
954
+ timeline: getTimeline(start, end, options.timeFidelity),
955
+ constituents: constituents$1,
956
+ start: start
957
+ })
958
+ };
959
+
960
+ return Object.freeze(harmonics)
961
+ };
962
+
963
+ const tidePredictionFactory = (constituents, options) => {
964
+ const harmonicsOptions = {
965
+ harmonicConstituents: constituents,
966
+ phaseKey: 'phase_GMT',
967
+ offset: false
968
+ };
969
+
970
+ if (typeof options !== 'undefined') {
971
+ Object.keys(harmonicsOptions).forEach(key => {
972
+ if (typeof options[key] !== 'undefined') {
973
+ harmonicsOptions[key] = options[key];
974
+ }
975
+ });
976
+ }
977
+
978
+ const tidePrediction = {
979
+ getTimelinePrediction: ({ start, end }) => {
980
+ return harmonicsFactory(harmonicsOptions)
981
+ .setTimeSpan(start, end)
982
+ .prediction()
983
+ .getTimelinePrediction()
984
+ },
985
+
986
+ getExtremesPrediction: ({ start, end, labels, offsets, timeFidelity }) => {
987
+ return harmonicsFactory(harmonicsOptions)
988
+ .setTimeSpan(start, end)
989
+ .prediction({ timeFidelity: timeFidelity })
990
+ .getExtremesPrediction(labels, offsets)
991
+ },
992
+
993
+ getWaterLevelAtTime: ({ time }) => {
994
+ const endDate = new Date(time.getTime() + 10 * 60 * 1000);
995
+ return harmonicsFactory(harmonicsOptions)
996
+ .setTimeSpan(time, endDate)
997
+ .prediction()
998
+ .getTimelinePrediction()[0]
999
+ }
1000
+ };
1001
+
1002
+ return tidePrediction
1003
+ };
1004
+
1005
+ export default tidePredictionFactory;