@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
package/.eslintrc.js ADDED
@@ -0,0 +1,22 @@
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ es6: true,
5
+ node: true,
6
+ 'jest/globals': true,
7
+ },
8
+ extends: ['standard'],
9
+ globals: {
10
+ Atomics: 'readonly',
11
+ SharedArrayBuffer: 'readonly',
12
+ },
13
+ plugins: ['jest'],
14
+ parserOptions: {
15
+ ecmaVersion: 2018,
16
+ sourceType: 'module',
17
+ },
18
+ rules: {
19
+ 'space-before-function-paren': 0,
20
+ 'comma-dangle': 0,
21
+ },
22
+ }
@@ -0,0 +1,15 @@
1
+ name: Test
2
+ on:
3
+ - push
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+
11
+ - name: Install modules
12
+ run: yarn
13
+
14
+ - name: Test
15
+ run: yarn ci
package/.prettierrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true
4
+ }
package/Gruntfile.js ADDED
@@ -0,0 +1,87 @@
1
+ /* eslint-env node */
2
+ /* eslint-disable no-process-env, camelcase */
3
+
4
+ module.exports = function(grunt) {
5
+ grunt.initConfig({
6
+ pkg: grunt.file.readJSON('package.json'),
7
+ eslint: {
8
+ options: {},
9
+ files: ['src/**/*.js', 'test/**/*.js']
10
+ },
11
+ clean: ['lib', 'dist'],
12
+ exec: {
13
+ rollup: {
14
+ command: 'BABEL_ENV=build rollup -c'
15
+ },
16
+ test: {
17
+ command: 'yarn run test'
18
+ }
19
+ },
20
+ uglify: {
21
+ options: {
22
+ mangle: true,
23
+ compress: true,
24
+ preserveComments: 'some'
25
+ },
26
+ dist: {
27
+ files: [
28
+ {
29
+ cwd: 'dist/',
30
+ expand: true,
31
+ src: ['*.js', '!*.min.js'],
32
+ dest: 'dist/',
33
+ rename: function(dest, src) {
34
+ return dest + src.replace(/\.js$/, '.min.js')
35
+ }
36
+ }
37
+ ]
38
+ }
39
+ },
40
+ babel: {
41
+ cjs: {
42
+ files: [
43
+ {
44
+ cwd: 'src/',
45
+ expand: true,
46
+ src: ['**/*.js', '!**/__tests__/**', '!**/__mocks__/**'],
47
+ dest: 'lib/'
48
+ }
49
+ ]
50
+ }
51
+ },
52
+ watch: {
53
+ scripts: {
54
+ options: {
55
+ atBegin: true
56
+ },
57
+
58
+ files: ['src/**/*.js', 'test/**/*.js'],
59
+ tasks: ['eslint', 'exec:test']
60
+ }
61
+ },
62
+ coveralls: {
63
+ options: {
64
+ force: false
65
+ },
66
+
67
+ coverage: {
68
+ src: 'coverage/lcov.info'
69
+ }
70
+ },
71
+ eslint: {
72
+ target: ['src/**/*.js']
73
+ }
74
+ })
75
+ this.registerTask(
76
+ 'build',
77
+ 'Builds a distributable version of the current project',
78
+ ['clean', 'eslint', 'exec:test', 'babel', 'exec:rollup']
79
+ )
80
+ grunt.loadNpmTasks('grunt-eslint')
81
+ grunt.loadNpmTasks('grunt-contrib-clean')
82
+ grunt.loadNpmTasks('grunt-contrib-uglify')
83
+ grunt.loadNpmTasks('grunt-contrib-watch')
84
+ grunt.loadNpmTasks('grunt-babel')
85
+ grunt.loadNpmTasks('grunt-exec')
86
+ grunt.loadNpmTasks('grunt-coveralls')
87
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Kevin Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ [![CircleCI](https://circleci.com/gh/neaps/tide-predictor.svg?style=svg)](https://circleci.com/gh/neaps/tide-predictor) [![Coverage Status](https://coveralls.io/repos/github/neaps/tide-predictor/badge.svg?branch=master)](https://coveralls.io/github/neaps/tide-predictor?branch=master) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fneaps%2Ftide-predictor.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fneaps%2Ftide-predictor?ref=badge_shield)
2
+
3
+ # Tide predictor
4
+
5
+ A Javascript tide harmonic calculator.
6
+
7
+ <!-- START DOCS -->
8
+
9
+ ## 🚨Warning🚨
10
+
11
+ **Do not use calculations from this project for navigation, or depend on them in any situation where inaccuracies could result in harm to a person or property.**
12
+
13
+ Tide predictions are only as good as the harmonics data available, and these can be inconsistent and vary widely based on the accuracy of the source data and local conditions.
14
+
15
+ The tide predictions do not factor events such as storm surge, wind waves, uplift, tsunamis, or sadly, climate change. 😢
16
+
17
+ # Installation
18
+
19
+ ```
20
+ # yarn
21
+ yarn install @neaps/tide-prediction
22
+
23
+ #npm
24
+ npm install --save @neaps/tide-prediction
25
+ ```
26
+
27
+ # Usage
28
+
29
+ Neaps requires that you [provide your own tidal harmonics information](#constituent-object) to generate a prediction.
30
+
31
+ Because many constituent datum come with multiple phases (in the case of NOAA's data, they are `phase_local` and `phase_GMT`), there is a `phaseKey` option for choosing which to use.
32
+
33
+ Note that, for now, Neaps **will not** do any timezone corrections. This means you need to pass date objects that align with whatever timezone the constituents are in.
34
+
35
+ ```javascript
36
+ import TidePrediction from '@neaps/tide-prediction'
37
+ const constituents = [
38
+ {
39
+ phase_GMT: 98.7,
40
+ phase_local: 313.7,
41
+ amplitude: 2.687,
42
+ name: 'M2',
43
+ speed: 28.984104
44
+ }
45
+ //....there are usually many, read the docs
46
+ ]
47
+
48
+ const highLowTides = tidePrediction(constituents, {
49
+ phaseKey: 'phase_GMT'
50
+ }).getExtremesPrediction(new Date('2019-01-01'), new Date('2019-01-10'))
51
+ ```
52
+
53
+ ## Tide prediction object
54
+
55
+ Calling `tidePrediction` will generate a new tide prediction object. It accepts the following arguments:
56
+
57
+ - `constituents` - An array of [constituent objects](#constituent-object)
58
+ - `options` - An object with one of:
59
+ - `phaseKey` - The name of the parameter within constituents that is considered the "phase"
60
+ - `offset` - A value to add to **all** values predicted. This is useful if you want to, for example, offset tides by mean high water, etc.
61
+
62
+ ### Tide prediction methods
63
+
64
+ The returned tide prediction object has various methods. All of these return regular JavaScript objects.
65
+
66
+ #### High and low tide - `getExtremesPrediction`
67
+
68
+ Returns the predicted high and low tides between a start and end date.
69
+
70
+ ```javascript
71
+ const startDate = new Date()
72
+ const endDate = new Date(startDate + 3 * 24 * 60 * 60 * 1000)
73
+ const tides = tidePrediction(constituents).getExtremesPrediction({
74
+ start: startDate,
75
+ end: endDate,
76
+ labels: {
77
+ //optional human-readable labels
78
+ high: 'High tide',
79
+ low: 'Low tide'
80
+ }
81
+ })
82
+ ```
83
+
84
+ If you want predictions for a subservient station, first set the reference station in the prediction, and pass the [subservient station offests](#subservient-station) to the `getExtremesPrediction` method:
85
+
86
+ ```javascript
87
+ const tides = tidePrediction(constituents).getExtremesPrediction({
88
+ start: startDate,
89
+ end: endDate,
90
+ offset: {
91
+ height_offset: {
92
+ high: 1,
93
+ low: 2
94
+ },
95
+ time_offset: {
96
+ high: 1,
97
+ low: 2
98
+ }
99
+ }
100
+ })
101
+ ```
102
+
103
+ ##### Options
104
+
105
+ The `getExtremesPrediction` accepts a single object with options:
106
+
107
+ - `start` - **Required ** - The date & time to start looking for high and low tides
108
+ - `end` - **Required ** - The date & time to stop looking for high and low tides
109
+ - `timeFidelity` - Number of seconds accurate the time should be, defaults to 10 minutes.
110
+ - `labels` - An object to define the human-readable labels for the tides
111
+ - `high` - The human-readable label for high tides
112
+ - `low` - The human-readable label for low tides
113
+ - `offset` - The offset values if these predictions are for a [subservient station](#subservient-station)
114
+
115
+ ##### Return values
116
+
117
+ High and low tides are returned as arrays of objects:
118
+
119
+ - `time` - A Javascript Date object of the time
120
+ - `level` - The water level
121
+ - `high` - **true** if this is a high tide, **false** if not
122
+ - `low` - **true** if this is a low tide, **false** if not
123
+ - `label` - The human-readable label (by default, 'High' or 'Low')
124
+
125
+ #### Water level at time - `getWaterLevelAtTime`
126
+
127
+ Gives you the predicted water level at a specific time.
128
+
129
+ ```javascript
130
+ const waterLevel = tidePrediction(constituents).getWaterLevelAtTime({
131
+ time: new Date()
132
+ })
133
+ ```
134
+
135
+ ##### Options
136
+
137
+ The `getWaterLevelAtTime` accepts a single object of options:
138
+
139
+ - `time` - A Javascript date object of the time for the prediction
140
+
141
+ ##### Return values
142
+
143
+ A single object is returned with:
144
+
145
+ - `time` - A Javascript date object
146
+ - `level` - The predicted water level
147
+
148
+ ## Data definitions
149
+
150
+ ### <a name="constituent-object"></a>Constituent definition
151
+
152
+ Tidal constituents should be an array of objects with at least:
153
+
154
+ - `name` - **string** - The NOAA constituent name, all upper-case.
155
+ - `amplitude` - **float** - The constituent amplitude
156
+ - `[phase]` - **float** - The phase of the constituent. Because several services provide different phase values, you can choose which one to use when building your tide prediction.
157
+
158
+ ```
159
+ [
160
+ {
161
+ name: '[constituent name]',
162
+ amplitude: 1.3,
163
+ phase: 1.33
164
+ },
165
+ {
166
+ name: '[constituent name 2]',
167
+ amplitude: 1.3,
168
+ phase: 1.33
169
+ }
170
+ ]
171
+ ```
172
+
173
+ ### <a name="subservient-station"></a>Subservient station definitions
174
+
175
+ Some stations do not have defined harmonic data, but do have published offets and a reference station. These include the offsets in time or amplitude of the high and low tides. Subservient station definitions are objects that include:
176
+
177
+ - `height_offset` - **object** - An object of height offets, in the same units as the reference station.
178
+ - `high` - **float** - The offset to be added to high tide (can be negative)
179
+ - `low` - **float** - The offset to be added to low tide (can be negative)
180
+ - `time_offset` - **object** - An object of time offets, in number of minutes
181
+ - `high` - **float** - The number of minutes to add to high tide times (can be negative)
182
+ - `low` - **float** - The number of minutes to add to low tide times (can be negative)
183
+
184
+ ```
185
+ {
186
+ height_offset: {
187
+ high: 1,
188
+ low: 2
189
+ },
190
+ time_offset: {
191
+ high: 1,
192
+ low: 2
193
+ }
194
+ }
195
+ ```
196
+
197
+ # Shout out
198
+
199
+ All the really hard math is based on the excellent [Xtide](https://flaterco.com/xtide) and [pytides](https://github.com/sam-cox/pytides).
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ presets: ['@babel/preset-env'],
3
+ env: {
4
+ test: {},
5
+ build: {
6
+ ignore: ['**/__tests__', '**/__mocks__']
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,1013 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
+ typeof define === 'function' && define.amd ? define(factory) :
4
+ (global = global || self, global.tidePredictor = factory());
5
+ }(this, function () { 'use strict';
6
+
7
+ const d2r = Math.PI / 180.0;
8
+ const r2d = 180.0 / Math.PI;
9
+
10
+ // Convert a sexagesimal angle into decimal degrees
11
+ const sexagesimalToDecimal = (degrees, arcmins, arcsecs, mas, muas) => {
12
+ arcmins = typeof arcmins !== 'undefined' ? arcmins : 0;
13
+ arcsecs = typeof arcsecs !== 'undefined' ? arcsecs : 0;
14
+ mas = typeof mas !== 'undefined' ? mas : 0;
15
+ muas = typeof muas !== 'undefined' ? muas : 0;
16
+
17
+ return (
18
+ degrees +
19
+ arcmins / 60.0 +
20
+ arcsecs / (60.0 * 60.0) +
21
+ mas / (60.0 * 60.0 * 1e3) +
22
+ muas / (60.0 * 60.0 * 1e6)
23
+ )
24
+ };
25
+
26
+ const coefficients = {
27
+ // Meeus formula 21.3
28
+ terrestrialObliquity: [
29
+ sexagesimalToDecimal(23, 26, 21.448),
30
+ -sexagesimalToDecimal(0, 0, 4680.93),
31
+ -sexagesimalToDecimal(0, 0, 1.55),
32
+ sexagesimalToDecimal(0, 0, 1999.25),
33
+ -sexagesimalToDecimal(0, 0, 51.38),
34
+ -sexagesimalToDecimal(0, 0, 249.67),
35
+ -sexagesimalToDecimal(0, 0, 39.05),
36
+ sexagesimalToDecimal(0, 0, 7.12),
37
+ sexagesimalToDecimal(0, 0, 27.87),
38
+ sexagesimalToDecimal(0, 0, 5.79),
39
+ sexagesimalToDecimal(0, 0, 2.45)
40
+ ].map((number, index) => {
41
+ return number * Math.pow(1e-2, index)
42
+ }),
43
+
44
+ solarPerigee: [
45
+ 280.46645 - 357.5291,
46
+ 36000.76932 - 35999.0503,
47
+ 0.0003032 + 0.0001559,
48
+ 0.00000048
49
+ ],
50
+
51
+ solarLongitude: [280.46645, 36000.76983, 0.0003032],
52
+
53
+ lunarInclination: [5.145],
54
+
55
+ lunarLongitude: [
56
+ 218.3164591,
57
+ 481267.88134236,
58
+ -0.0013268,
59
+ 1 / 538841.0 - 1 / 65194000.0
60
+ ],
61
+
62
+ lunarNode: [
63
+ 125.044555,
64
+ -1934.1361849,
65
+ 0.0020762,
66
+ 1 / 467410.0,
67
+ -1 / 60616000.0
68
+ ],
69
+
70
+ lunarPerigee: [
71
+ 83.353243,
72
+ 4069.0137111,
73
+ -0.0103238,
74
+ -1 / 80053.0,
75
+ 1 / 18999000.0
76
+ ]
77
+ };
78
+
79
+ // Evaluates a polynomial at argument
80
+ const polynomial = (coefficients, argument) => {
81
+ const result = [];
82
+ coefficients.forEach((coefficient, index) => {
83
+ result.push(coefficient * Math.pow(argument, index));
84
+ });
85
+ return result.reduce((a, b) => {
86
+ return a + b
87
+ })
88
+ };
89
+
90
+ // Evaluates a derivative polynomial at argument
91
+ const derivativePolynomial = (coefficients, argument) => {
92
+ const result = [];
93
+ coefficients.forEach((coefficient, index) => {
94
+ result.push(coefficient * index * Math.pow(argument, index - 1));
95
+ });
96
+ return result.reduce((a, b) => {
97
+ return a + b
98
+ })
99
+ };
100
+
101
+ // Meeus formula 11.1
102
+ const T = t => {
103
+ return (JD(t) - 2451545.0) / 36525
104
+ };
105
+
106
+ // Meeus formula 7.1
107
+ const JD = t => {
108
+ let Y = t.getFullYear();
109
+ let M = t.getMonth() + 1;
110
+ const D =
111
+ t.getDate() +
112
+ t.getHours() / 24.0 +
113
+ t.getMinutes() / (24.0 * 60.0) +
114
+ t.getSeconds() / (24.0 * 60.0 * 60.0) +
115
+ t.getMilliseconds() / (24.0 * 60.0 * 60.0 * 1e6);
116
+ if (M <= 2) {
117
+ Y = Y - 1;
118
+ M = M + 12;
119
+ }
120
+ const A = Math.floor(Y / 100.0);
121
+ const B = 2 - A + Math.floor(A / 4.0);
122
+ return (
123
+ Math.floor(365.25 * (Y + 4716)) +
124
+ Math.floor(30.6001 * (M + 1)) +
125
+ D +
126
+ B -
127
+ 1524.5
128
+ )
129
+ };
130
+
131
+ /**
132
+ * @todo - What's with the array returned from the arccos?
133
+ * @param {*} N
134
+ * @param {*} i
135
+ * @param {*} omega
136
+ */
137
+ const _I = (N, i, omega) => {
138
+ N = d2r * N;
139
+ i = d2r * i;
140
+ omega = d2r * omega;
141
+ const cosI =
142
+ Math.cos(i) * Math.cos(omega) - Math.sin(i) * Math.sin(omega) * Math.cos(N);
143
+ return r2d * Math.acos(cosI)
144
+ };
145
+
146
+ const _xi = (N, i, omega) => {
147
+ N = d2r * N;
148
+ i = d2r * i;
149
+ omega = d2r * omega;
150
+ let e1 =
151
+ (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
152
+ Math.tan(0.5 * N);
153
+ let e2 =
154
+ (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
155
+ Math.tan(0.5 * N);
156
+ e1 = Math.atan(e1);
157
+ e2 = Math.atan(e2);
158
+ e1 = e1 - 0.5 * N;
159
+ e2 = e2 - 0.5 * N;
160
+ return -(e1 + e2) * r2d
161
+ };
162
+
163
+ const _nu = (N, i, omega) => {
164
+ N = d2r * N;
165
+ i = d2r * i;
166
+ omega = d2r * omega;
167
+ let e1 =
168
+ (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
169
+ Math.tan(0.5 * N);
170
+ let e2 =
171
+ (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
172
+ Math.tan(0.5 * N);
173
+ e1 = Math.atan(e1);
174
+ e2 = Math.atan(e2);
175
+ e1 = e1 - 0.5 * N;
176
+ e2 = e2 - 0.5 * N;
177
+ return (e1 - e2) * r2d
178
+ };
179
+
180
+ // Schureman equation 224
181
+ const _nup = (N, i, omega) => {
182
+ const I = d2r * _I(N, i, omega);
183
+ const nu = d2r * _nu(N, i, omega);
184
+ return (
185
+ r2d *
186
+ Math.atan(
187
+ (Math.sin(2 * I) * Math.sin(nu)) /
188
+ (Math.sin(2 * I) * Math.cos(nu) + 0.3347)
189
+ )
190
+ )
191
+ };
192
+
193
+ // Schureman equation 232
194
+ const _nupp = (N, i, omega) => {
195
+ const I = d2r * _I(N, i, omega);
196
+ const nu = d2r * _nu(N, i, omega);
197
+ const tan2nupp =
198
+ (Math.sin(I) ** 2 * Math.sin(2 * nu)) /
199
+ (Math.sin(I) ** 2 * Math.cos(2 * nu) + 0.0727);
200
+ return r2d * 0.5 * Math.atan(tan2nupp)
201
+ };
202
+
203
+ const modulus = (a, b) => {
204
+ return ((a % b) + b) % b
205
+ };
206
+
207
+ const astro = time => {
208
+ const result = {};
209
+ const polynomials = {
210
+ s: coefficients.lunarLongitude,
211
+ h: coefficients.solarLongitude,
212
+ p: coefficients.lunarPerigee,
213
+ N: coefficients.lunarNode,
214
+ pp: coefficients.solarPerigee,
215
+ 90: [90.0],
216
+ omega: coefficients.terrestrialObliquity,
217
+ i: coefficients.lunarInclination
218
+ };
219
+
220
+ // Polynomials are in T, that is Julian Centuries; we want our speeds to be
221
+ // in the more convenient unit of degrees per hour.
222
+ const dTdHour = 1 / (24 * 365.25 * 100);
223
+ Object.keys(polynomials).forEach(name => {
224
+ result[name] = {
225
+ value: modulus(polynomial(polynomials[name], T(time)), 360.0),
226
+ speed: derivativePolynomial(polynomials[name], T(time)) * dTdHour
227
+ };
228
+ });
229
+
230
+ // Some other parameters defined by Schureman which are dependent on the
231
+ // parameters N, i, omega for use in node factor calculations. We don't need
232
+ // their speeds.
233
+ const functions = {
234
+ I: _I,
235
+ xi: _xi,
236
+ nu: _nu,
237
+ nup: _nup,
238
+ nupp: _nupp
239
+ };
240
+ Object.keys(functions).forEach(name => {
241
+ const functionCall = functions[name];
242
+ result[name] = {
243
+ value: modulus(
244
+ functionCall(result.N.value, result.i.value, result.omega.value),
245
+ 360.0
246
+ ),
247
+ speed: null
248
+ };
249
+ });
250
+
251
+ // We don't work directly with the T (hours) parameter, instead our spanning
252
+ // set for equilibrium arguments #is given by T+h-s, s, h, p, N, pp, 90.
253
+ // This is in line with convention.
254
+ const hour = {
255
+ value: (JD(time) - Math.floor(JD(time))) * 360.0,
256
+ speed: 15.0
257
+ };
258
+
259
+ result['T+h-s'] = {
260
+ value: hour.value + result.h.value - result.s.value,
261
+ speed: hour.speed + result.h.speed - result.s.speed
262
+ };
263
+
264
+ // It is convenient to calculate Schureman's P here since several node
265
+ // factors need it, although it could be argued that these
266
+ // (along with I, xi, nu etc) belong somewhere else.
267
+ result.P = {
268
+ value: result.p.value - (result.xi.value % 360.0),
269
+ speed: null
270
+ };
271
+
272
+ return result
273
+ };
274
+
275
+ const modulus$1 = (a, b) => {
276
+ return ((a % b) + b) % b
277
+ };
278
+
279
+ const addExtremesOffsets = (extreme, offsets) => {
280
+ if (typeof offsets === 'undefined' || !offsets) {
281
+ return extreme
282
+ }
283
+ if (extreme.high && offsets.height_offset && offsets.height_offset.high) {
284
+ extreme.level *= offsets.height_offset.high;
285
+ }
286
+ if (extreme.low && offsets.height_offset && offsets.height_offset.low) {
287
+ extreme.level *= offsets.height_offset.low;
288
+ }
289
+ if (extreme.high && offsets.time_offset && offsets.time_offset.high) {
290
+ extreme.time = new Date(
291
+ extreme.time.getTime() + offsets.time_offset.high * 60 * 1000
292
+ );
293
+ }
294
+ if (extreme.low && offsets.time_offset && offsets.time_offset.low) {
295
+ extreme.time = new Date(
296
+ extreme.time.getTime() + offsets.time_offset.low * 60 * 1000
297
+ );
298
+ }
299
+ return extreme
300
+ };
301
+
302
+ const getExtremeLabel = (label, highLowLabels) => {
303
+ if (
304
+ typeof highLowLabels !== 'undefined' &&
305
+ typeof highLowLabels[label] !== 'undefined'
306
+ ) {
307
+ return highLowLabels[label]
308
+ }
309
+ const labels = {
310
+ high: 'High',
311
+ low: 'Low'
312
+ };
313
+ return labels[label]
314
+ };
315
+
316
+ const predictionFactory = ({ timeline, constituents, start }) => {
317
+ const getLevel = (hour, modelBaseSpeed, modelU, modelF, modelBaseValue) => {
318
+ const amplitudes = [];
319
+ let result = 0;
320
+
321
+ constituents.forEach(constituent => {
322
+ const amplitude = constituent.amplitude;
323
+ const phase = constituent._phase;
324
+ const f = modelF[constituent.name];
325
+ const speed = modelBaseSpeed[constituent.name];
326
+ const u = modelU[constituent.name];
327
+ const V0 = modelBaseValue[constituent.name];
328
+ amplitudes.push(amplitude * f * Math.cos(speed * hour + (V0 + u) - phase));
329
+ });
330
+ // sum up each row
331
+ amplitudes.forEach(item => {
332
+ result += item;
333
+ });
334
+ return result
335
+ };
336
+
337
+ const prediction = {};
338
+
339
+ prediction.getExtremesPrediction = options => {
340
+ const { labels, offsets } = typeof options !== 'undefined' ? options : {};
341
+ const results = [];
342
+ const { baseSpeed, u, f, baseValue } = prepare();
343
+ let goingUp = false;
344
+ let goingDown = false;
345
+ let lastLevel = getLevel(0, baseSpeed, u[0], f[0], baseValue);
346
+ timeline.items.forEach((time, index) => {
347
+ const hour = timeline.hours[index];
348
+ const level = getLevel(hour, baseSpeed, u[index], f[index], baseValue);
349
+ // Compare this level to the last one, if we
350
+ // are changing angle, then the last one was high or low
351
+ if (level > lastLevel && goingDown) {
352
+ results.push(
353
+ addExtremesOffsets(
354
+ {
355
+ time: timeline.items[index - 1],
356
+ level: lastLevel,
357
+ high: false,
358
+ low: true,
359
+ label: getExtremeLabel('low', labels)
360
+ },
361
+ offsets
362
+ )
363
+ );
364
+ }
365
+ if (level < lastLevel && goingUp) {
366
+ results.push(
367
+ addExtremesOffsets(
368
+ {
369
+ time: timeline.items[index - 1],
370
+ level: lastLevel,
371
+ high: true,
372
+ low: false,
373
+ label: getExtremeLabel('high', labels)
374
+ },
375
+ offsets
376
+ )
377
+ );
378
+ }
379
+ if (level > lastLevel) {
380
+ goingUp = true;
381
+ goingDown = false;
382
+ }
383
+ if (level < lastLevel) {
384
+ goingUp = false;
385
+ goingDown = true;
386
+ }
387
+ lastLevel = level;
388
+ });
389
+ return results
390
+ };
391
+
392
+ prediction.getTimelinePrediction = () => {
393
+ const results = [];
394
+ const { baseSpeed, u, f, baseValue } = prepare();
395
+ timeline.items.forEach((time, index) => {
396
+ const hour = timeline.hours[index];
397
+ const prediction = {
398
+ time: time,
399
+ hour: hour,
400
+ level: getLevel(hour, baseSpeed, u[index], f[index], baseValue)
401
+ };
402
+
403
+ results.push(prediction);
404
+ });
405
+ return results
406
+ };
407
+
408
+ const prepare = () => {
409
+ const baseAstro = astro(start);
410
+
411
+ const baseValue = {};
412
+ const baseSpeed = {};
413
+ const u = [];
414
+ const f = [];
415
+ constituents.forEach(constituent => {
416
+ const value = constituent._model.value(baseAstro);
417
+ const speed = constituent._model.speed(baseAstro);
418
+ baseValue[constituent.name] = d2r * value;
419
+ baseSpeed[constituent.name] = d2r * speed;
420
+ });
421
+ timeline.items.forEach(time => {
422
+ const uItem = {};
423
+ const fItem = {};
424
+ const itemAstro = astro(time);
425
+ constituents.forEach(constituent => {
426
+ const constituentU = modulus$1(constituent._model.u(itemAstro), 360);
427
+
428
+ uItem[constituent.name] = d2r * constituentU;
429
+ fItem[constituent.name] = modulus$1(constituent._model.f(itemAstro), 360);
430
+ });
431
+ u.push(uItem);
432
+ f.push(fItem);
433
+ });
434
+
435
+ return {
436
+ baseValue: baseValue,
437
+ baseSpeed: baseSpeed,
438
+ u: u,
439
+ f: f
440
+ }
441
+ };
442
+
443
+ return Object.freeze(prediction)
444
+ };
445
+
446
+ const corrections = {
447
+ fUnity() {
448
+ return 1
449
+ },
450
+
451
+ // Schureman equations 73, 65
452
+ fMm(a) {
453
+ const omega = d2r * a.omega.value;
454
+ const i = d2r * a.i.value;
455
+ const I = d2r * a.I.value;
456
+ const mean =
457
+ (2 / 3.0 - Math.pow(Math.sin(omega), 2)) *
458
+ (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
459
+ return (2 / 3.0 - Math.pow(Math.sin(I), 2)) / mean
460
+ },
461
+
462
+ // Schureman equations 74, 66
463
+ fMf(a) {
464
+ const omega = d2r * a.omega.value;
465
+ const i = d2r * a.i.value;
466
+ const I = d2r * a.I.value;
467
+ const mean = Math.pow(Math.sin(omega), 2) * Math.pow(Math.cos(0.5 * i), 4);
468
+ return Math.pow(Math.sin(I), 2) / mean
469
+ },
470
+
471
+ // Schureman equations 75, 67
472
+ fO1(a) {
473
+ const omega = d2r * a.omega.value;
474
+ const i = d2r * a.i.value;
475
+ const I = d2r * a.I.value;
476
+ const mean =
477
+ Math.sin(omega) *
478
+ Math.pow(Math.cos(0.5 * omega), 2) *
479
+ Math.pow(Math.cos(0.5 * i), 4);
480
+ return (Math.sin(I) * Math.pow(Math.cos(0.5 * I), 2)) / mean
481
+ },
482
+
483
+ // Schureman equations 76, 68
484
+ fJ1(a) {
485
+ const omega = d2r * a.omega.value;
486
+ const i = d2r * a.i.value;
487
+ const I = d2r * a.I.value;
488
+ const mean =
489
+ Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
490
+ return Math.sin(2 * I) / mean
491
+ },
492
+
493
+ // Schureman equations 77, 69
494
+ fOO1(a) {
495
+ const omega = d2r * a.omega.value;
496
+ const i = d2r * a.i.value;
497
+ const I = d2r * a.I.value;
498
+ const mean =
499
+ Math.sin(omega) *
500
+ Math.pow(Math.sin(0.5 * omega), 2) *
501
+ Math.pow(Math.cos(0.5 * i), 4);
502
+ return (Math.sin(I) * Math.pow(Math.sin(0.5 * I), 2)) / mean
503
+ },
504
+
505
+ // Schureman equations 78, 70
506
+ fM2(a) {
507
+ const omega = d2r * a.omega.value;
508
+ const i = d2r * a.i.value;
509
+ const I = d2r * a.I.value;
510
+ const mean =
511
+ Math.pow(Math.cos(0.5 * omega), 4) * Math.pow(Math.cos(0.5 * i), 4);
512
+ return Math.pow(Math.cos(0.5 * I), 4) / mean
513
+ },
514
+
515
+ // Schureman equations 227, 226, 68
516
+ // Should probably eventually include the derivations of the magic numbers (0.5023 etc).
517
+ fK1(a) {
518
+ const omega = d2r * a.omega.value;
519
+ const i = d2r * a.i.value;
520
+ const I = d2r * a.I.value;
521
+ const nu = d2r * a.nu.value;
522
+ const sin2IcosnuMean =
523
+ Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
524
+ const mean = 0.5023 * sin2IcosnuMean + 0.1681;
525
+ return (
526
+ Math.pow(
527
+ 0.2523 * Math.pow(Math.sin(2 * I), 2) +
528
+ 0.1689 * Math.sin(2 * I) * Math.cos(nu) +
529
+ 0.0283,
530
+ 0.5
531
+ ) / mean
532
+ )
533
+ },
534
+
535
+ // Schureman equations 215, 213, 204
536
+ // It can be (and has been) confirmed that the exponent for R_a reads 1/2 via Schureman Table 7
537
+ fL2(a) {
538
+ const P = d2r * a.P.value;
539
+ const I = d2r * a.I.value;
540
+ const rAInv = Math.pow(
541
+ 1 -
542
+ 12 * Math.pow(Math.tan(0.5 * I), 2) * Math.cos(2 * P) +
543
+ 36 * Math.pow(Math.tan(0.5 * I), 4),
544
+ 0.5
545
+ );
546
+ return corrections.fM2(a) * rAInv
547
+ },
548
+
549
+ // Schureman equations 235, 234, 71
550
+ // Again, magic numbers
551
+ fK2(a) {
552
+ const omega = d2r * a.omega.value;
553
+ const i = d2r * a.i.value;
554
+ const I = d2r * a.I.value;
555
+ const nu = d2r * a.nu.value;
556
+ const sinsqIcos2nuMean =
557
+ Math.sin(omega) ** 2 * (1 - (3 / 2.0) * Math.sin(i) ** 2);
558
+ const mean = 0.5023 * sinsqIcos2nuMean + 0.0365;
559
+ return (
560
+ Math.pow(
561
+ 0.2523 * Math.pow(Math.sin(I), 4) +
562
+ 0.0367 * Math.pow(Math.sin(I), 2) * Math.cos(2 * nu) +
563
+ 0.0013,
564
+ 0.5
565
+ ) / mean
566
+ )
567
+ },
568
+ // Schureman equations 206, 207, 195
569
+ fM1(a) {
570
+ const P = d2r * a.P.value;
571
+ const I = d2r * a.I.value;
572
+ const qAInv = Math.pow(
573
+ 0.25 +
574
+ 1.5 *
575
+ Math.cos(I) *
576
+ Math.cos(2 * P) *
577
+ Math.pow(Math.cos(0.5 * I), -0.5) +
578
+ 2.25 * Math.pow(Math.cos(I), 2) * Math.pow(Math.cos(0.5 * I), -4),
579
+ 0.5
580
+ );
581
+ return corrections.fO1(a) * qAInv
582
+ },
583
+
584
+ // See e.g. Schureman equation 149
585
+ fModd(a, n) {
586
+ return Math.pow(corrections.fM2(a), n / 2.0)
587
+ },
588
+
589
+ // Node factors u, see Table 2 of Schureman.
590
+
591
+ uZero(a) {
592
+ return 0.0
593
+ },
594
+
595
+ uMf(a) {
596
+ return -2.0 * a.xi.value
597
+ },
598
+
599
+ uO1(a) {
600
+ return 2.0 * a.xi.value - a.nu.value
601
+ },
602
+
603
+ uJ1(a) {
604
+ return -a.nu.value
605
+ },
606
+
607
+ uOO1(a) {
608
+ return -2.0 * a.xi.value - a.nu.value
609
+ },
610
+
611
+ uM2(a) {
612
+ return 2.0 * a.xi.value - 2.0 * a.nu.value
613
+ },
614
+
615
+ uK1(a) {
616
+ return -a.nup.value
617
+ },
618
+
619
+ // Schureman 214
620
+ uL2(a) {
621
+ const I = d2r * a.I.value;
622
+ const P = d2r * a.P.value;
623
+ const R =
624
+ r2d *
625
+ Math.atan(
626
+ Math.sin(2 * P) /
627
+ ((1 / 6.0) * Math.pow(Math.tan(0.5 * I), -2) - Math.cos(2 * P))
628
+ );
629
+ return 2.0 * a.xi.value - 2.0 * a.nu.value - R
630
+ },
631
+
632
+ uK2(a) {
633
+ return -2.0 * a.nupp.value
634
+ },
635
+
636
+ // Schureman 202
637
+ uM1(a) {
638
+ const I = d2r * a.I.value;
639
+ const P = d2r * a.P.value;
640
+ const Q =
641
+ r2d *
642
+ Math.atan(((5 * Math.cos(I) - 1) / (7 * Math.cos(I) + 1)) * Math.tan(P));
643
+ return a.xi.value - a.nu.value + Q
644
+ },
645
+
646
+ uModd(a, n) {
647
+ return (n / 2.0) * corrections.uM2(a)
648
+ }
649
+ };
650
+
651
+ /**
652
+ * Computes the dot notation of two arrays
653
+ * @param {*} a
654
+ * @param {*} b
655
+ */
656
+ const dotArray = (a, b) => {
657
+ const results = [];
658
+ a.forEach((value, index) => {
659
+ results.push(value * b[index]);
660
+ });
661
+ return results.reduce((total, value) => {
662
+ return total + value
663
+ })
664
+ };
665
+
666
+ const astronimicDoodsonNumber = astro => {
667
+ return [
668
+ astro['T+h-s'],
669
+ astro.s,
670
+ astro.h,
671
+ astro.p,
672
+ astro.N,
673
+ astro.pp,
674
+ astro['90']
675
+ ]
676
+ };
677
+
678
+ const astronomicSpeed = astro => {
679
+ const results = [];
680
+ astronimicDoodsonNumber(astro).forEach(number => {
681
+ results.push(number.speed);
682
+ });
683
+ return results
684
+ };
685
+
686
+ const astronomicValues = astro => {
687
+ const results = [];
688
+ astronimicDoodsonNumber(astro).forEach(number => {
689
+ results.push(number.value);
690
+ });
691
+ return results
692
+ };
693
+
694
+ const constituentFactory = (name, coefficients, u, f) => {
695
+ if (!coefficients) {
696
+ throw new Error('Coefficient must be defined for a constituent')
697
+ }
698
+
699
+ const constituent = {
700
+ name: name,
701
+
702
+ coefficients: coefficients,
703
+
704
+ value: astro => {
705
+ return dotArray(coefficients, astronomicValues(astro))
706
+ },
707
+
708
+ speed(astro) {
709
+ return dotArray(coefficients, astronomicSpeed(astro))
710
+ },
711
+
712
+ u: typeof u !== 'undefined' ? u : corrections.uZero,
713
+
714
+ f: typeof f !== 'undefined' ? f : corrections.fUnity
715
+ };
716
+
717
+ return Object.freeze(constituent)
718
+ };
719
+
720
+ const compoundConstituentFactory = (name, members) => {
721
+ const coefficients = [];
722
+ members.forEach(({ constituent, factor }) => {
723
+ constituent.coefficients.forEach((coefficient, index) => {
724
+ if (typeof coefficients[index] === 'undefined') {
725
+ coefficients[index] = 0;
726
+ }
727
+ coefficients[index] += coefficient * factor;
728
+ });
729
+ });
730
+
731
+ const compoundConstituent = {
732
+ name: name,
733
+
734
+ coefficients: coefficients,
735
+
736
+ speed: astro => {
737
+ let speed = 0;
738
+ members.forEach(({ constituent, factor }) => {
739
+ speed += constituent.speed(astro) * factor;
740
+ });
741
+ return speed
742
+ },
743
+
744
+ value: astro => {
745
+ let value = 0;
746
+ members.forEach(({ constituent, factor }) => {
747
+ value += constituent.value(astro) * factor;
748
+ });
749
+ return value
750
+ },
751
+
752
+ u: astro => {
753
+ let u = 0;
754
+ members.forEach(({ constituent, factor }) => {
755
+ u += constituent.u(astro) * factor;
756
+ });
757
+ return u
758
+ },
759
+
760
+ f: astro => {
761
+ const f = [];
762
+ members.forEach(({ constituent, factor }) => {
763
+ f.push(Math.pow(constituent.f(astro), Math.abs(factor)));
764
+ });
765
+ return f.reduce((previous, value) => {
766
+ return previous * value
767
+ })
768
+ }
769
+ };
770
+
771
+ return Object.freeze(compoundConstituent)
772
+ };
773
+
774
+ const constituents = {};
775
+ // Long Term
776
+ constituents.Z0 = constituentFactory('Z0', [0, 0, 0, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
777
+ constituents.SA = constituentFactory('Sa', [0, 0, 1, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
778
+ constituents.SSA = constituentFactory(
779
+ 'Ssa',
780
+ [0, 0, 2, 0, 0, 0, 0],
781
+ corrections.uZero,
782
+ corrections.fUnity
783
+ );
784
+ constituents.MM = constituentFactory('MM', [0, 1, 0, -1, 0, 0, 0], corrections.uZero, corrections.fMm);
785
+ constituents.MF = constituentFactory('MF', [0, 2, 0, 0, 0, 0, 0], corrections.uMf, corrections.fMf);
786
+ // Diurnals
787
+ constituents.Q1 = constituentFactory('Q1', [1, -2, 0, 1, 0, 0, 1], corrections.uO1, corrections.fO1);
788
+ constituents.O1 = constituentFactory('O1', [1, -1, 0, 0, 0, 0, 1], corrections.uO1, corrections.fO1);
789
+ constituents.K1 = constituentFactory('K1', [1, 1, 0, 0, 0, 0, -1], corrections.uK1, corrections.fK1);
790
+ constituents.J1 = constituentFactory('J1', [1, 2, 0, -1, 0, 0, -1], corrections.uJ1, corrections.fJ1);
791
+ constituents.M1 = constituentFactory('M1', [1, 0, 0, 0, 0, 0, 1], corrections.uM1, corrections.fM1);
792
+ constituents.P1 = constituentFactory('P1', [1, 1, -2, 0, 0, 0, 1], corrections.uZero, corrections.fUnity);
793
+ constituents.S1 = constituentFactory('S1', [1, 1, -1, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
794
+ constituents.OO1 = constituentFactory('OO1', [1, 3, 0, 0, 0, 0, -1], corrections.uOO1, corrections.fOO1);
795
+ // Semi diurnals
796
+ constituents['2N2'] = constituentFactory('2N2', [2, -2, 0, 2, 0, 0, 0], corrections.uM2, corrections.fM2);
797
+ constituents.N2 = constituentFactory('N2', [2, -1, 0, 1, 0, 0, 0], corrections.uM2, corrections.fM2);
798
+ constituents.NU2 = constituentFactory('NU2', [2, -1, 2, -1, 0, 0, 0], corrections.uM2, corrections.fM2);
799
+ constituents.M2 = constituentFactory('M2', [2, 0, 0, 0, 0, 0, 0], corrections.uM2, corrections.fM2);
800
+ constituents.LAM2 = constituentFactory('LAM2', [2, 1, -2, 1, 0, 0, 2], corrections.uM2, corrections.fM2);
801
+ constituents.L2 = constituentFactory('L2', [2, 1, 0, -1, 0, 0, 2], corrections.uL2, corrections.fL2);
802
+ constituents.T2 = constituentFactory('T2', [2, 2, -3, 0, 0, 1, 0], corrections.uZero, corrections.fUnity);
803
+ constituents.S2 = constituentFactory('S2', [2, 2, -2, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
804
+ constituents.R2 = constituentFactory(
805
+ 'R2',
806
+ [2, 2, -1, 0, 0, -1, 2],
807
+ corrections.uZero,
808
+ corrections.fUnity
809
+ );
810
+ constituents.K2 = constituentFactory('K2', [2, 2, 0, 0, 0, 0, 0], corrections.uK2, corrections.fK2);
811
+ // Third diurnal
812
+ constituents.M3 = constituentFactory(
813
+ 'M3',
814
+ [3, 0, 0, 0, 0, 0, 0],
815
+ a => {
816
+ return corrections.uModd(a, 3)
817
+ },
818
+ a => {
819
+ return corrections.fModd(a, 3)
820
+ }
821
+ );
822
+ // Compound
823
+ constituents.MSF = compoundConstituentFactory('MSF', [
824
+ { constituent: constituents.S2, factor: 1 },
825
+ { constituent: constituents.M2, factor: -1 }
826
+ ]);
827
+
828
+ // Diurnal
829
+ constituents['2Q1'] = compoundConstituentFactory('2Q1', [
830
+ { constituent: constituents.N2, factor: 1 },
831
+ { constituent: constituents.J1, factor: -1 }
832
+ ]);
833
+ constituents.RHO = compoundConstituentFactory('RHO', [
834
+ { constituent: constituents.NU2, factor: 1 },
835
+ { constituent: constituents.K1, factor: -1 }
836
+ ]);
837
+
838
+ // Semi-Diurnal
839
+
840
+ constituents.MU2 = compoundConstituentFactory('MU2', [
841
+ { constituent: constituents.M2, factor: 2 },
842
+ { constituent: constituents.S2, factor: -1 }
843
+ ]);
844
+ constituents['2SM2'] = compoundConstituentFactory('2SM2', [
845
+ { constituent: constituents.S2, factor: 2 },
846
+ { constituent: constituents.M2, factor: -1 }
847
+ ]);
848
+
849
+ // Third-Diurnal
850
+ constituents['2MK3'] = compoundConstituentFactory('2MK3', [
851
+ { constituent: constituents.M2, factor: 1 },
852
+ { constituent: constituents.O1, factor: 1 }
853
+ ]);
854
+ constituents.MK3 = compoundConstituentFactory('MK3', [
855
+ { constituent: constituents.M2, factor: 1 },
856
+ { constituent: constituents.K1, factor: 1 }
857
+ ]);
858
+
859
+ // Quarter-Diurnal
860
+ constituents.MN4 = compoundConstituentFactory('MN4', [
861
+ { constituent: constituents.M2, factor: 1 },
862
+ { constituent: constituents.N2, factor: 1 }
863
+ ]);
864
+ constituents.M4 = compoundConstituentFactory('M4', [
865
+ { constituent: constituents.M2, factor: 2 }
866
+ ]);
867
+ constituents.MS4 = compoundConstituentFactory('MS4', [
868
+ { constituent: constituents.M2, factor: 1 },
869
+ { constituent: constituents.S2, factor: 1 }
870
+ ]);
871
+ constituents.S4 = compoundConstituentFactory('S4', [
872
+ { constituent: constituents.S2, factor: 2 }
873
+ ]);
874
+
875
+ // Sixth-Diurnal
876
+ constituents.M6 = compoundConstituentFactory('M6', [
877
+ { constituent: constituents.M2, factor: 3 }
878
+ ]);
879
+ constituents.S6 = compoundConstituentFactory('S6', [
880
+ { constituent: constituents.S2, factor: 3 }
881
+ ]);
882
+
883
+ // Eighth-Diurnals
884
+ constituents.M8 = compoundConstituentFactory('M8', [
885
+ { constituent: constituents.M2, factor: 4 }
886
+ ]);
887
+
888
+ const getDate = time => {
889
+ if (time instanceof Date) {
890
+ return time
891
+ }
892
+ if (typeof time === 'number') {
893
+ return new Date(time * 1000)
894
+ }
895
+ throw new Error('Invalid date format, should be a Date object, or timestamp')
896
+ };
897
+
898
+ const getTimeline = (start, end, seconds) => {
899
+ seconds = typeof seconds !== 'undefined' ? seconds : 10 * 60;
900
+ const timeline = [];
901
+ const endTime = end.getTime() / 1000;
902
+ let lastTime = start.getTime() / 1000;
903
+ const startTime = lastTime;
904
+ const hours = [];
905
+ while (lastTime <= endTime) {
906
+ timeline.push(new Date(lastTime * 1000));
907
+ hours.push((lastTime - startTime) / (60 * 60));
908
+ lastTime += seconds;
909
+ }
910
+
911
+ return {
912
+ items: timeline,
913
+ hours: hours
914
+ }
915
+ };
916
+
917
+ const harmonicsFactory = ({ harmonicConstituents, phaseKey, offset }) => {
918
+ if (!Array.isArray(harmonicConstituents)) {
919
+ throw new Error('Harmonic constituents are not an array')
920
+ }
921
+ const constituents$1 = [];
922
+ harmonicConstituents.forEach((constituent, index) => {
923
+ if (typeof constituent.name === 'undefined') {
924
+ throw new Error('Harmonic constituents must have a name property')
925
+ }
926
+ if (typeof constituents[constituent.name] !== 'undefined') {
927
+ constituent._model = constituents[constituent.name];
928
+ constituent._phase = d2r * constituent[phaseKey];
929
+ constituents$1.push(constituent);
930
+ }
931
+ });
932
+
933
+ if (offset !== false) {
934
+ constituents$1.push({
935
+ name: 'Z0',
936
+ _model: constituents.Z0,
937
+ _phase: 0,
938
+ amplitude: offset
939
+ });
940
+ }
941
+
942
+ let start = new Date();
943
+ let end = new Date();
944
+
945
+ const harmonics = {};
946
+
947
+ harmonics.setTimeSpan = (startTime, endTime) => {
948
+ start = getDate(startTime);
949
+ end = getDate(endTime);
950
+ if (start.getTime() >= end.getTime()) {
951
+ throw new Error('Start time must be before end time')
952
+ }
953
+ return harmonics
954
+ };
955
+
956
+ harmonics.prediction = options => {
957
+ options =
958
+ typeof options !== 'undefined' ? options : { timeFidelity: 10 * 60 };
959
+ return predictionFactory({
960
+ timeline: getTimeline(start, end, options.timeFidelity),
961
+ constituents: constituents$1,
962
+ start: start
963
+ })
964
+ };
965
+
966
+ return Object.freeze(harmonics)
967
+ };
968
+
969
+ const tidePredictionFactory = (constituents, options) => {
970
+ const harmonicsOptions = {
971
+ harmonicConstituents: constituents,
972
+ phaseKey: 'phase_GMT',
973
+ offset: false
974
+ };
975
+
976
+ if (typeof options !== 'undefined') {
977
+ Object.keys(harmonicsOptions).forEach(key => {
978
+ if (typeof options[key] !== 'undefined') {
979
+ harmonicsOptions[key] = options[key];
980
+ }
981
+ });
982
+ }
983
+
984
+ const tidePrediction = {
985
+ getTimelinePrediction: ({ start, end }) => {
986
+ return harmonicsFactory(harmonicsOptions)
987
+ .setTimeSpan(start, end)
988
+ .prediction()
989
+ .getTimelinePrediction()
990
+ },
991
+
992
+ getExtremesPrediction: ({ start, end, labels, offsets, timeFidelity }) => {
993
+ return harmonicsFactory(harmonicsOptions)
994
+ .setTimeSpan(start, end)
995
+ .prediction({ timeFidelity: timeFidelity })
996
+ .getExtremesPrediction(labels, offsets)
997
+ },
998
+
999
+ getWaterLevelAtTime: ({ time }) => {
1000
+ const endDate = new Date(time.getTime() + 10 * 60 * 1000);
1001
+ return harmonicsFactory(harmonicsOptions)
1002
+ .setTimeSpan(time, endDate)
1003
+ .prediction()
1004
+ .getTimelinePrediction()[0]
1005
+ }
1006
+ };
1007
+
1008
+ return tidePrediction
1009
+ };
1010
+
1011
+ return tidePredictionFactory;
1012
+
1013
+ }));