├── .eslintrc.cjs
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist
├── commonjs
│ ├── index.js
│ └── package.json
└── web
│ ├── tide-predictor.js
│ └── tide-predictor.min.js
├── examples
└── browser
│ └── index.html
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── astronomy
│ ├── coefficients.js
│ ├── constants.js
│ └── index.js
├── constituents
│ ├── compound-constituent.js
│ ├── constituent.js
│ └── index.js
├── harmonics
│ ├── index.js
│ └── prediction.js
├── index.js
└── node-corrections
│ └── index.js
└── test
├── _mocks
├── constituents.js
├── secondary-station.js
└── water-levels.js
├── astronomy
├── coefficients.js
└── index.js
├── constituents
├── compound-constituent.js
├── constituent.js
└── index.js
├── harmonics
├── index.js
└── prediction.js
├── index.js
├── lib
└── close-to.js
├── noaa.js
└── node-corrections
└── index.js
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true
6 | },
7 | extends: 'eslint:recommended',
8 | parserOptions: {
9 | ecmaVersion: 'latest',
10 | sourceType: 'module'
11 | },
12 | rules: {
13 | indent: ['error', 2],
14 | 'linebreak-style': ['error', 'unix'],
15 | quotes: ['error', 'single'],
16 | semi: ['error', 'never']
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
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: npm install
13 |
14 | - name: Install codecov
15 | run: npm install -g codecov
16 |
17 | - name: Test
18 | run: npm run ci
19 |
20 | - name: Test build
21 | run: npm run build
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .local
3 | coverage
4 | junit.xml
5 | .test-cache
6 | .DS_Store
7 | .nyc_output
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "none"
5 | }
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Fixed
11 |
12 | - The browser example was not working due to API changes.
13 | - The Readme file was using the wrong package name
14 |
15 | ## [0.1.1] - 2022-07-16
16 |
17 | ### Fix
18 |
19 | - A minor version bump for NPM
20 |
21 | ## [0.1.0] - 2022-07-16
22 |
23 | ### Added
24 |
25 | - Removing Babel
26 | - Using C8 and Mocha for native ES import testing
27 | - Remove Grunt in favor of some simpler scripts in package.json
28 | - Build a CJS module as part of the build process
29 |
30 | ## [0.0.3] - 2020-12-07
31 |
32 | ### Fixed
33 |
34 | - Updated URLs for NOAA's API for the testing service
35 | - Security update to developer dependencies
36 |
37 | ## [0.0.2] - 2019-11-14
38 |
39 | ### Added
40 |
41 | - Tidal extreme calculations are expensive, so you can now use the `timeFidelity` option to define how exact you want the high/low tide dates to be.
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |  [](https://app.fossa.com/projects/git%2Bgithub.com%2Fneaps%2Ftide-predictor?ref=badge_shield) [](https://codecov.io/gh/neaps/tide-predictor)
2 |
3 | # Tide predictor
4 |
5 | A Javascript tide harmonic calculator.
6 |
7 |
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 | #npm
21 | npm install @neaps/tide-predictor
22 |
23 | # yarn
24 | yarn add @neaps/tide-predictor
25 |
26 | ```
27 |
28 | ## Importing
29 |
30 | You can import the module using Ecmascript, or CommonJS. Note that the CommonJS export is transpiled, so deep debugging the module that way will be difficult.
31 |
32 | ```js
33 | import TidePredictor from '@neaps/tide-predictor'
34 | const TidePredictor = require('@neaps/tide-predictor')
35 | ```
36 |
37 | There are also packaged and minified versions for the browser in `dist/web`.
38 |
39 | # Usage
40 |
41 | Neaps requires that you [provide your own tidal harmonics information](#constituent-object) to generate a prediction.
42 |
43 | 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.
44 |
45 | 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.
46 |
47 | ```javascript
48 | import TidePredictor from '@neaps/tide-predictor'
49 |
50 | const constituents = [
51 | {
52 | phase_GMT: 98.7,
53 | phase_local: 313.7,
54 | amplitude: 2.687,
55 | name: 'M2',
56 | speed: 28.984104
57 | }
58 | //....there are usually many, read the docs
59 | ]
60 |
61 | const highLowTides = TidePredictor(constituents, {
62 | phaseKey: 'phase_GMT'
63 | }).getExtremesPrediction({
64 | start: new Date('2019-01-01'),
65 | end: new Date('2019-01-10')
66 | })
67 | ```
68 |
69 | ## Tide prediction object
70 |
71 | Calling `tidePredictor` will generate a new tide prediction object. It accepts the following arguments:
72 |
73 | - `constituents` - An array of [constituent objects](#constituent-object)
74 | - `options` - An object with one of:
75 | - `phaseKey` - The name of the parameter within constituents that is considered the "phase"
76 | - `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.
77 |
78 | ### Tide prediction methods
79 |
80 | The returned tide prediction object has various methods. All of these return regular JavaScript objects.
81 |
82 | #### High and low tide - `getExtremesPrediction`
83 |
84 | Returns the predicted high and low tides between a start and end date.
85 |
86 | ```javascript
87 | const startDate = new Date()
88 | const endDate = new Date(startDate + 3 * 24 * 60 * 60 * 1000)
89 | const tides = TidePredictor(constituents).getExtremesPrediction({
90 | start: startDate,
91 | end: endDate,
92 | labels: {
93 | //optional human-readable labels
94 | high: 'High tide',
95 | low: 'Low tide'
96 | }
97 | })
98 | ```
99 |
100 | 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:
101 |
102 | ```javascript
103 | const tides = TidePredictor(constituents).getExtremesPrediction({
104 | start: startDate,
105 | end: endDate,
106 | offset: {
107 | height_offset: {
108 | high: 1,
109 | low: 2
110 | },
111 | time_offset: {
112 | high: 1,
113 | low: 2
114 | }
115 | }
116 | })
117 | ```
118 |
119 | ##### Options
120 |
121 | The `getExtremesPrediction` accepts a single object with options:
122 |
123 | - `start` - **Required ** - The date & time to start looking for high and low tides
124 | - `end` - **Required ** - The date & time to stop looking for high and low tides
125 | - `timeFidelity` - Number of seconds accurate the time should be, defaults to 10 minutes.
126 | - `labels` - An object to define the human-readable labels for the tides
127 | - `high` - The human-readable label for high tides
128 | - `low` - The human-readable label for low tides
129 | - `offset` - The offset values if these predictions are for a [subservient station](#subservient-station)
130 |
131 | ##### Return values
132 |
133 | High and low tides are returned as arrays of objects:
134 |
135 | - `time` - A Javascript Date object of the time
136 | - `level` - The water level
137 | - `high` - **true** if this is a high tide, **false** if not
138 | - `low` - **true** if this is a low tide, **false** if not
139 | - `label` - The human-readable label (by default, 'High' or 'Low')
140 |
141 | #### Water level at time - `getWaterLevelAtTime`
142 |
143 | Gives you the predicted water level at a specific time.
144 |
145 | ```javascript
146 | const waterLevel = TidePredictor(constituents).getWaterLevelAtTime({
147 | time: new Date()
148 | })
149 | ```
150 |
151 | ##### Options
152 |
153 | The `getWaterLevelAtTime` accepts a single object of options:
154 |
155 | - `time` - A Javascript date object of the time for the prediction
156 |
157 | ##### Return values
158 |
159 | A single object is returned with:
160 |
161 | - `time` - A Javascript date object
162 | - `level` - The predicted water level
163 |
164 | ## Data definitions
165 |
166 | ### Constituent definition
167 |
168 | Tidal constituents should be an array of objects with at least:
169 |
170 | - `name` - **string** - The NOAA constituent name, all upper-case.
171 | - `amplitude` - **float** - The constituent amplitude
172 | - `[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.
173 |
174 | ```
175 | [
176 | {
177 | name: '[constituent name]',
178 | amplitude: 1.3,
179 | phase: 1.33
180 | },
181 | {
182 | name: '[constituent name 2]',
183 | amplitude: 1.3,
184 | phase: 1.33
185 | }
186 | ]
187 | ```
188 |
189 | ### Subservient station definitions
190 |
191 | 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:
192 |
193 | - `height_offset` - **object** - An object of height offets, in the same units as the reference station.
194 | - `high` - **float** - The offset to be added to high tide (can be negative)
195 | - `low` - **float** - The offset to be added to low tide (can be negative)
196 | - `time_offset` - **object** - An object of time offets, in number of minutes
197 | - `high` - **float** - The number of minutes to add to high tide times (can be negative)
198 | - `low` - **float** - The number of minutes to add to low tide times (can be negative)
199 |
200 | ```
201 | {
202 | height_offset: {
203 | high: 1,
204 | low: 2
205 | },
206 | time_offset: {
207 | high: 1,
208 | low: 2
209 | }
210 | }
211 | ```
212 |
213 | # Shout out
214 |
215 | All the really hard math is based on the excellent [Xtide](https://flaterco.com/xtide) and [pytides](https://github.com/sam-cox/pytides).
216 |
--------------------------------------------------------------------------------
/dist/commonjs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const d2r = Math.PI / 180.0;
4 | const r2d = 180.0 / Math.PI;
5 |
6 | // Convert a sexagesimal angle into decimal degrees
7 | const sexagesimalToDecimal = (degrees, arcmins, arcsecs, mas, muas) => {
8 | arcmins = typeof arcmins !== 'undefined' ? arcmins : 0;
9 | arcsecs = typeof arcsecs !== 'undefined' ? arcsecs : 0;
10 | mas = typeof mas !== 'undefined' ? mas : 0;
11 | muas = typeof muas !== 'undefined' ? muas : 0;
12 |
13 | return (
14 | degrees +
15 | arcmins / 60.0 +
16 | arcsecs / (60.0 * 60.0) +
17 | mas / (60.0 * 60.0 * 1e3) +
18 | muas / (60.0 * 60.0 * 1e6)
19 | )
20 | };
21 |
22 | const coefficients = {
23 | // Meeus formula 21.3
24 | terrestrialObliquity: [
25 | sexagesimalToDecimal(23, 26, 21.448),
26 | -sexagesimalToDecimal(0, 0, 4680.93),
27 | -sexagesimalToDecimal(0, 0, 1.55),
28 | sexagesimalToDecimal(0, 0, 1999.25),
29 | -sexagesimalToDecimal(0, 0, 51.38),
30 | -sexagesimalToDecimal(0, 0, 249.67),
31 | -sexagesimalToDecimal(0, 0, 39.05),
32 | sexagesimalToDecimal(0, 0, 7.12),
33 | sexagesimalToDecimal(0, 0, 27.87),
34 | sexagesimalToDecimal(0, 0, 5.79),
35 | sexagesimalToDecimal(0, 0, 2.45),
36 | ].map((number, index) => {
37 | return number * Math.pow(1e-2, index)
38 | }),
39 |
40 | solarPerigee: [
41 | 280.46645 - 357.5291,
42 | 36000.76932 - 35999.0503,
43 | 0.0003032 + 0.0001559,
44 | 0.00000048,
45 | ],
46 |
47 | solarLongitude: [280.46645, 36000.76983, 0.0003032],
48 |
49 | lunarInclination: [5.145],
50 |
51 | lunarLongitude: [
52 | 218.3164591,
53 | 481267.88134236,
54 | -0.0013268,
55 | 1 / 538841.0 - 1 / 65194000.0,
56 | ],
57 |
58 | lunarNode: [
59 | 125.044555,
60 | -1934.1361849,
61 | 0.0020762,
62 | 1 / 467410.0,
63 | -1 / 60616000.0,
64 | ],
65 |
66 | lunarPerigee: [
67 | 83.353243,
68 | 4069.0137111,
69 | -0.0103238,
70 | -1 / 80053.0,
71 | 1 / 18999000.0,
72 | ],
73 | };
74 |
75 | // Evaluates a polynomial at argument
76 | const polynomial = (coefficients, argument) => {
77 | const result = [];
78 | coefficients.forEach((coefficient, index) => {
79 | result.push(coefficient * Math.pow(argument, index));
80 | });
81 | return result.reduce((a, b) => {
82 | return a + b
83 | })
84 | };
85 |
86 | // Evaluates a derivative polynomial at argument
87 | const derivativePolynomial = (coefficients, argument) => {
88 | const result = [];
89 | coefficients.forEach((coefficient, index) => {
90 | result.push(coefficient * index * Math.pow(argument, index - 1));
91 | });
92 | return result.reduce((a, b) => {
93 | return a + b
94 | })
95 | };
96 |
97 | // Meeus formula 11.1
98 | const T = (t) => {
99 | return (JD(t) - 2451545.0) / 36525
100 | };
101 |
102 | // Meeus formula 7.1
103 | const JD = (t) => {
104 | let Y = t.getFullYear();
105 | let M = t.getMonth() + 1;
106 | const D =
107 | t.getDate() +
108 | t.getHours() / 24.0 +
109 | t.getMinutes() / (24.0 * 60.0) +
110 | t.getSeconds() / (24.0 * 60.0 * 60.0) +
111 | t.getMilliseconds() / (24.0 * 60.0 * 60.0 * 1e6);
112 | if (M <= 2) {
113 | Y = Y - 1;
114 | M = M + 12;
115 | }
116 | const A = Math.floor(Y / 100.0);
117 | const B = 2 - A + Math.floor(A / 4.0);
118 | return (
119 | Math.floor(365.25 * (Y + 4716)) +
120 | Math.floor(30.6001 * (M + 1)) +
121 | D +
122 | B -
123 | 1524.5
124 | )
125 | };
126 |
127 | /**
128 | * @todo - What's with the array returned from the arccos?
129 | * @param {*} N
130 | * @param {*} i
131 | * @param {*} omega
132 | */
133 | const _I = (N, i, omega) => {
134 | N = d2r * N;
135 | i = d2r * i;
136 | omega = d2r * omega;
137 | const cosI =
138 | Math.cos(i) * Math.cos(omega) - Math.sin(i) * Math.sin(omega) * Math.cos(N);
139 | return r2d * Math.acos(cosI)
140 | };
141 |
142 | const _xi = (N, i, omega) => {
143 | N = d2r * N;
144 | i = d2r * i;
145 | omega = d2r * omega;
146 | let e1 =
147 | (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
148 | Math.tan(0.5 * N);
149 | let e2 =
150 | (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
151 | Math.tan(0.5 * N);
152 | e1 = Math.atan(e1);
153 | e2 = Math.atan(e2);
154 | e1 = e1 - 0.5 * N;
155 | e2 = e2 - 0.5 * N;
156 | return -(e1 + e2) * r2d
157 | };
158 |
159 | const _nu = (N, i, omega) => {
160 | N = d2r * N;
161 | i = d2r * i;
162 | omega = d2r * omega;
163 | let e1 =
164 | (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
165 | Math.tan(0.5 * N);
166 | let e2 =
167 | (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
168 | Math.tan(0.5 * N);
169 | e1 = Math.atan(e1);
170 | e2 = Math.atan(e2);
171 | e1 = e1 - 0.5 * N;
172 | e2 = e2 - 0.5 * N;
173 | return (e1 - e2) * r2d
174 | };
175 |
176 | // Schureman equation 224
177 | const _nup = (N, i, omega) => {
178 | const I = d2r * _I(N, i, omega);
179 | const nu = d2r * _nu(N, i, omega);
180 | return (
181 | r2d *
182 | Math.atan(
183 | (Math.sin(2 * I) * Math.sin(nu)) /
184 | (Math.sin(2 * I) * Math.cos(nu) + 0.3347)
185 | )
186 | )
187 | };
188 |
189 | // Schureman equation 232
190 | const _nupp = (N, i, omega) => {
191 | const I = d2r * _I(N, i, omega);
192 | const nu = d2r * _nu(N, i, omega);
193 | const tan2nupp =
194 | (Math.sin(I) ** 2 * Math.sin(2 * nu)) /
195 | (Math.sin(I) ** 2 * Math.cos(2 * nu) + 0.0727);
196 | return r2d * 0.5 * Math.atan(tan2nupp)
197 | };
198 |
199 | const modulus$1 = (a, b) => {
200 | return ((a % b) + b) % b
201 | };
202 |
203 | const astro = (time) => {
204 | const result = {};
205 | const polynomials = {
206 | s: coefficients.lunarLongitude,
207 | h: coefficients.solarLongitude,
208 | p: coefficients.lunarPerigee,
209 | N: coefficients.lunarNode,
210 | pp: coefficients.solarPerigee,
211 | 90: [90.0],
212 | omega: coefficients.terrestrialObliquity,
213 | i: coefficients.lunarInclination
214 | };
215 |
216 | // Polynomials are in T, that is Julian Centuries; we want our speeds to be
217 | // in the more convenient unit of degrees per hour.
218 | const dTdHour = 1 / (24 * 365.25 * 100);
219 | Object.keys(polynomials).forEach((name) => {
220 | result[name] = {
221 | value: modulus$1(polynomial(polynomials[name], T(time)), 360.0),
222 | speed: derivativePolynomial(polynomials[name], T(time)) * dTdHour
223 | };
224 | });
225 |
226 | // Some other parameters defined by Schureman which are dependent on the
227 | // parameters N, i, omega for use in node factor calculations. We don't need
228 | // their speeds.
229 | const functions = {
230 | I: _I,
231 | xi: _xi,
232 | nu: _nu,
233 | nup: _nup,
234 | nupp: _nupp
235 | };
236 | Object.keys(functions).forEach((name) => {
237 | const functionCall = functions[name];
238 | result[name] = {
239 | value: modulus$1(
240 | functionCall(result.N.value, result.i.value, result.omega.value),
241 | 360.0
242 | ),
243 | speed: null
244 | };
245 | });
246 |
247 | // We don't work directly with the T (hours) parameter, instead our spanning
248 | // set for equilibrium arguments #is given by T+h-s, s, h, p, N, pp, 90.
249 | // This is in line with convention.
250 | const hour = {
251 | value: (JD(time) - Math.floor(JD(time))) * 360.0,
252 | speed: 15.0
253 | };
254 |
255 | result['T+h-s'] = {
256 | value: hour.value + result.h.value - result.s.value,
257 | speed: hour.speed + result.h.speed - result.s.speed
258 | };
259 |
260 | // It is convenient to calculate Schureman's P here since several node
261 | // factors need it, although it could be argued that these
262 | // (along with I, xi, nu etc) belong somewhere else.
263 | result.P = {
264 | value: result.p.value - (result.xi.value % 360.0),
265 | speed: null
266 | };
267 |
268 | return result
269 | };
270 |
271 | const modulus = (a, b) => {
272 | return ((a % b) + b) % b
273 | };
274 |
275 | const addExtremesOffsets = (extreme, offsets) => {
276 | if (typeof offsets === 'undefined' || !offsets) {
277 | return extreme
278 | }
279 | if (extreme.high && offsets.height_offset && offsets.height_offset.high) {
280 | extreme.level *= offsets.height_offset.high;
281 | }
282 | if (extreme.low && offsets.height_offset && offsets.height_offset.low) {
283 | extreme.level *= offsets.height_offset.low;
284 | }
285 | if (extreme.high && offsets.time_offset && offsets.time_offset.high) {
286 | extreme.time = new Date(
287 | extreme.time.getTime() + offsets.time_offset.high * 60 * 1000
288 | );
289 | }
290 | if (extreme.low && offsets.time_offset && offsets.time_offset.low) {
291 | extreme.time = new Date(
292 | extreme.time.getTime() + offsets.time_offset.low * 60 * 1000
293 | );
294 | }
295 | return extreme
296 | };
297 |
298 | const getExtremeLabel = (label, highLowLabels) => {
299 | if (
300 | typeof highLowLabels !== 'undefined' &&
301 | typeof highLowLabels[label] !== 'undefined'
302 | ) {
303 | return highLowLabels[label]
304 | }
305 | const labels = {
306 | high: 'High',
307 | low: 'Low'
308 | };
309 | return labels[label]
310 | };
311 |
312 | const predictionFactory = ({ timeline, constituents, start }) => {
313 | const getLevel = (hour, modelBaseSpeed, modelU, modelF, modelBaseValue) => {
314 | const amplitudes = [];
315 | let result = 0;
316 |
317 | constituents.forEach((constituent) => {
318 | const amplitude = constituent.amplitude;
319 | const phase = constituent._phase;
320 | const f = modelF[constituent.name];
321 | const speed = modelBaseSpeed[constituent.name];
322 | const u = modelU[constituent.name];
323 | const V0 = modelBaseValue[constituent.name];
324 | amplitudes.push(amplitude * f * Math.cos(speed * hour + (V0 + u) - phase));
325 | });
326 | // sum up each row
327 | amplitudes.forEach((item) => {
328 | result += item;
329 | });
330 | return result
331 | };
332 |
333 | const prediction = {};
334 |
335 | prediction.getExtremesPrediction = (options) => {
336 | const { labels, offsets } = typeof options !== 'undefined' ? options : {};
337 | const results = [];
338 | const { baseSpeed, u, f, baseValue } = prepare();
339 | let goingUp = false;
340 | let goingDown = false;
341 | let lastLevel = getLevel(0, baseSpeed, u[0], f[0], baseValue);
342 | timeline.items.forEach((time, index) => {
343 | const hour = timeline.hours[index];
344 | const level = getLevel(hour, baseSpeed, u[index], f[index], baseValue);
345 | // Compare this level to the last one, if we
346 | // are changing angle, then the last one was high or low
347 | if (level > lastLevel && goingDown) {
348 | results.push(
349 | addExtremesOffsets(
350 | {
351 | time: timeline.items[index - 1],
352 | level: lastLevel,
353 | high: false,
354 | low: true,
355 | label: getExtremeLabel('low', labels)
356 | },
357 | offsets
358 | )
359 | );
360 | }
361 | if (level < lastLevel && goingUp) {
362 | results.push(
363 | addExtremesOffsets(
364 | {
365 | time: timeline.items[index - 1],
366 | level: lastLevel,
367 | high: true,
368 | low: false,
369 | label: getExtremeLabel('high', labels)
370 | },
371 | offsets
372 | )
373 | );
374 | }
375 | if (level > lastLevel) {
376 | goingUp = true;
377 | goingDown = false;
378 | }
379 | if (level < lastLevel) {
380 | goingUp = false;
381 | goingDown = true;
382 | }
383 | lastLevel = level;
384 | });
385 | return results
386 | };
387 |
388 | prediction.getTimelinePrediction = () => {
389 | const results = [];
390 | const { baseSpeed, u, f, baseValue } = prepare();
391 | timeline.items.forEach((time, index) => {
392 | const hour = timeline.hours[index];
393 | const prediction = {
394 | time,
395 | hour,
396 | level: getLevel(hour, baseSpeed, u[index], f[index], baseValue)
397 | };
398 |
399 | results.push(prediction);
400 | });
401 | return results
402 | };
403 |
404 | const prepare = () => {
405 | const baseAstro = astro(start);
406 |
407 | const baseValue = {};
408 | const baseSpeed = {};
409 | const u = [];
410 | const f = [];
411 | constituents.forEach((constituent) => {
412 | const value = constituent._model.value(baseAstro);
413 | const speed = constituent._model.speed(baseAstro);
414 | baseValue[constituent.name] = d2r * value;
415 | baseSpeed[constituent.name] = d2r * speed;
416 | });
417 | timeline.items.forEach((time) => {
418 | const uItem = {};
419 | const fItem = {};
420 | const itemAstro = astro(time);
421 | constituents.forEach((constituent) => {
422 | const constituentU = modulus(constituent._model.u(itemAstro), 360);
423 |
424 | uItem[constituent.name] = d2r * constituentU;
425 | fItem[constituent.name] = modulus(constituent._model.f(itemAstro), 360);
426 | });
427 | u.push(uItem);
428 | f.push(fItem);
429 | });
430 |
431 | return {
432 | baseValue,
433 | baseSpeed,
434 | u,
435 | f
436 | }
437 | };
438 |
439 | return Object.freeze(prediction)
440 | };
441 |
442 | const corrections = {
443 | fUnity() {
444 | return 1
445 | },
446 |
447 | // Schureman equations 73, 65
448 | fMm(a) {
449 | const omega = d2r * a.omega.value;
450 | const i = d2r * a.i.value;
451 | const I = d2r * a.I.value;
452 | const mean =
453 | (2 / 3.0 - Math.pow(Math.sin(omega), 2)) *
454 | (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
455 | return (2 / 3.0 - Math.pow(Math.sin(I), 2)) / mean
456 | },
457 |
458 | // Schureman equations 74, 66
459 | fMf(a) {
460 | const omega = d2r * a.omega.value;
461 | const i = d2r * a.i.value;
462 | const I = d2r * a.I.value;
463 | const mean = Math.pow(Math.sin(omega), 2) * Math.pow(Math.cos(0.5 * i), 4);
464 | return Math.pow(Math.sin(I), 2) / mean
465 | },
466 |
467 | // Schureman equations 75, 67
468 | fO1(a) {
469 | const omega = d2r * a.omega.value;
470 | const i = d2r * a.i.value;
471 | const I = d2r * a.I.value;
472 | const mean =
473 | Math.sin(omega) *
474 | Math.pow(Math.cos(0.5 * omega), 2) *
475 | Math.pow(Math.cos(0.5 * i), 4);
476 | return (Math.sin(I) * Math.pow(Math.cos(0.5 * I), 2)) / mean
477 | },
478 |
479 | // Schureman equations 76, 68
480 | fJ1(a) {
481 | const omega = d2r * a.omega.value;
482 | const i = d2r * a.i.value;
483 | const I = d2r * a.I.value;
484 | const mean =
485 | Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
486 | return Math.sin(2 * I) / mean
487 | },
488 |
489 | // Schureman equations 77, 69
490 | fOO1(a) {
491 | const omega = d2r * a.omega.value;
492 | const i = d2r * a.i.value;
493 | const I = d2r * a.I.value;
494 | const mean =
495 | Math.sin(omega) *
496 | Math.pow(Math.sin(0.5 * omega), 2) *
497 | Math.pow(Math.cos(0.5 * i), 4);
498 | return (Math.sin(I) * Math.pow(Math.sin(0.5 * I), 2)) / mean
499 | },
500 |
501 | // Schureman equations 78, 70
502 | fM2(a) {
503 | const omega = d2r * a.omega.value;
504 | const i = d2r * a.i.value;
505 | const I = d2r * a.I.value;
506 | const mean =
507 | Math.pow(Math.cos(0.5 * omega), 4) * Math.pow(Math.cos(0.5 * i), 4);
508 | return Math.pow(Math.cos(0.5 * I), 4) / mean
509 | },
510 |
511 | // Schureman equations 227, 226, 68
512 | // Should probably eventually include the derivations of the magic numbers (0.5023 etc).
513 | fK1(a) {
514 | const omega = d2r * a.omega.value;
515 | const i = d2r * a.i.value;
516 | const I = d2r * a.I.value;
517 | const nu = d2r * a.nu.value;
518 | const sin2IcosnuMean =
519 | Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2));
520 | const mean = 0.5023 * sin2IcosnuMean + 0.1681;
521 | return (
522 | Math.pow(
523 | 0.2523 * Math.pow(Math.sin(2 * I), 2) +
524 | 0.1689 * Math.sin(2 * I) * Math.cos(nu) +
525 | 0.0283,
526 | 0.5
527 | ) / mean
528 | )
529 | },
530 |
531 | // Schureman equations 215, 213, 204
532 | // It can be (and has been) confirmed that the exponent for R_a reads 1/2 via Schureman Table 7
533 | fL2(a) {
534 | const P = d2r * a.P.value;
535 | const I = d2r * a.I.value;
536 | const rAInv = Math.pow(
537 | 1 -
538 | 12 * Math.pow(Math.tan(0.5 * I), 2) * Math.cos(2 * P) +
539 | 36 * Math.pow(Math.tan(0.5 * I), 4),
540 | 0.5
541 | );
542 | return corrections.fM2(a) * rAInv
543 | },
544 |
545 | // Schureman equations 235, 234, 71
546 | // Again, magic numbers
547 | fK2(a) {
548 | const omega = d2r * a.omega.value;
549 | const i = d2r * a.i.value;
550 | const I = d2r * a.I.value;
551 | const nu = d2r * a.nu.value;
552 | const sinsqIcos2nuMean =
553 | Math.sin(omega) ** 2 * (1 - (3 / 2.0) * Math.sin(i) ** 2);
554 | const mean = 0.5023 * sinsqIcos2nuMean + 0.0365;
555 | return (
556 | Math.pow(
557 | 0.2523 * Math.pow(Math.sin(I), 4) +
558 | 0.0367 * Math.pow(Math.sin(I), 2) * Math.cos(2 * nu) +
559 | 0.0013,
560 | 0.5
561 | ) / mean
562 | )
563 | },
564 | // Schureman equations 206, 207, 195
565 | fM1(a) {
566 | const P = d2r * a.P.value;
567 | const I = d2r * a.I.value;
568 | const qAInv = Math.pow(
569 | 0.25 +
570 | 1.5 *
571 | Math.cos(I) *
572 | Math.cos(2 * P) *
573 | Math.pow(Math.cos(0.5 * I), -0.5) +
574 | 2.25 * Math.pow(Math.cos(I), 2) * Math.pow(Math.cos(0.5 * I), -4),
575 | 0.5
576 | );
577 | return corrections.fO1(a) * qAInv
578 | },
579 |
580 | // See e.g. Schureman equation 149
581 | fModd(a, n) {
582 | return Math.pow(corrections.fM2(a), n / 2.0)
583 | },
584 |
585 | // Node factors u, see Table 2 of Schureman.
586 |
587 | uZero() {
588 | return 0.0
589 | },
590 |
591 | uMf(a) {
592 | return -2.0 * a.xi.value
593 | },
594 |
595 | uO1(a) {
596 | return 2.0 * a.xi.value - a.nu.value
597 | },
598 |
599 | uJ1(a) {
600 | return -a.nu.value
601 | },
602 |
603 | uOO1(a) {
604 | return -2.0 * a.xi.value - a.nu.value
605 | },
606 |
607 | uM2(a) {
608 | return 2.0 * a.xi.value - 2.0 * a.nu.value
609 | },
610 |
611 | uK1(a) {
612 | return -a.nup.value
613 | },
614 |
615 | // Schureman 214
616 | uL2(a) {
617 | const I = d2r * a.I.value;
618 | const P = d2r * a.P.value;
619 | const R =
620 | r2d *
621 | Math.atan(
622 | Math.sin(2 * P) /
623 | ((1 / 6.0) * Math.pow(Math.tan(0.5 * I), -2) - Math.cos(2 * P))
624 | );
625 | return 2.0 * a.xi.value - 2.0 * a.nu.value - R
626 | },
627 |
628 | uK2(a) {
629 | return -2.0 * a.nupp.value
630 | },
631 |
632 | // Schureman 202
633 | uM1(a) {
634 | const I = d2r * a.I.value;
635 | const P = d2r * a.P.value;
636 | const Q =
637 | r2d *
638 | Math.atan(((5 * Math.cos(I) - 1) / (7 * Math.cos(I) + 1)) * Math.tan(P));
639 | return a.xi.value - a.nu.value + Q
640 | },
641 |
642 | uModd(a, n) {
643 | return (n / 2.0) * corrections.uM2(a)
644 | }
645 | };
646 |
647 | /**
648 | * Computes the dot notation of two arrays
649 | * @param {*} a
650 | * @param {*} b
651 | */
652 | const dotArray = (a, b) => {
653 | const results = [];
654 | a.forEach((value, index) => {
655 | results.push(value * b[index]);
656 | });
657 | return results.reduce((total, value) => {
658 | return total + value
659 | })
660 | };
661 |
662 | const astronimicDoodsonNumber = (astro) => {
663 | return [
664 | astro['T+h-s'],
665 | astro.s,
666 | astro.h,
667 | astro.p,
668 | astro.N,
669 | astro.pp,
670 | astro['90']
671 | ]
672 | };
673 |
674 | const astronomicSpeed = (astro) => {
675 | const results = [];
676 | astronimicDoodsonNumber(astro).forEach((number) => {
677 | results.push(number.speed);
678 | });
679 | return results
680 | };
681 |
682 | const astronomicValues = (astro) => {
683 | const results = [];
684 | astronimicDoodsonNumber(astro).forEach((number) => {
685 | results.push(number.value);
686 | });
687 | return results
688 | };
689 |
690 | const constituentFactory = (name, coefficients, u, f) => {
691 | if (!coefficients) {
692 | throw new Error('Coefficient must be defined for a constituent')
693 | }
694 |
695 | const constituent = {
696 | name,
697 | coefficients,
698 |
699 | value: (astro) => {
700 | return dotArray(coefficients, astronomicValues(astro))
701 | },
702 |
703 | speed(astro) {
704 | return dotArray(coefficients, astronomicSpeed(astro))
705 | },
706 |
707 | u: typeof u !== 'undefined' ? u : corrections.uZero,
708 |
709 | f: typeof f !== 'undefined' ? f : corrections.fUnity
710 | };
711 |
712 | return Object.freeze(constituent)
713 | };
714 |
715 | const compoundConstituentFactory = (name, members) => {
716 | const coefficients = [];
717 | members.forEach(({ constituent, factor }) => {
718 | constituent.coefficients.forEach((coefficient, index) => {
719 | if (typeof coefficients[index] === 'undefined') {
720 | coefficients[index] = 0;
721 | }
722 | coefficients[index] += coefficient * factor;
723 | });
724 | });
725 |
726 | const compoundConstituent = {
727 | name,
728 | 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 items = [];
895 | const endTime = end.getTime() / 1000;
896 | let lastTime = start.getTime() / 1000;
897 | const startTime = lastTime;
898 | const hours = [];
899 | while (lastTime <= endTime) {
900 | items.push(new Date(lastTime * 1000));
901 | hours.push((lastTime - startTime) / (60 * 60));
902 | lastTime += seconds;
903 | }
904 |
905 | return {
906 | items,
907 | 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) => {
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
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 })
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 | module.exports = tidePredictionFactory;
1006 |
--------------------------------------------------------------------------------
/dist/commonjs/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
2 |
--------------------------------------------------------------------------------
/dist/web/tide-predictor.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global = typeof globalThis !== 'undefined' ? globalThis : 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$1 = (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$1(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$1(
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 = (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,
399 | 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(constituent._model.u(itemAstro), 360);
427 |
428 | uItem[constituent.name] = d2r * constituentU;
429 | fItem[constituent.name] = modulus(constituent._model.f(itemAstro), 360);
430 | });
431 | u.push(uItem);
432 | f.push(fItem);
433 | });
434 |
435 | return {
436 | baseValue,
437 | baseSpeed,
438 | u,
439 | 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() {
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,
701 | coefficients,
702 |
703 | value: (astro) => {
704 | return dotArray(coefficients, astronomicValues(astro))
705 | },
706 |
707 | speed(astro) {
708 | return dotArray(coefficients, astronomicSpeed(astro))
709 | },
710 |
711 | u: typeof u !== 'undefined' ? u : corrections.uZero,
712 |
713 | f: typeof f !== 'undefined' ? f : corrections.fUnity
714 | };
715 |
716 | return Object.freeze(constituent)
717 | };
718 |
719 | const compoundConstituentFactory = (name, members) => {
720 | const coefficients = [];
721 | members.forEach(({ constituent, factor }) => {
722 | constituent.coefficients.forEach((coefficient, index) => {
723 | if (typeof coefficients[index] === 'undefined') {
724 | coefficients[index] = 0;
725 | }
726 | coefficients[index] += coefficient * factor;
727 | });
728 | });
729 |
730 | const compoundConstituent = {
731 | name,
732 | coefficients,
733 |
734 | speed: (astro) => {
735 | let speed = 0;
736 | members.forEach(({ constituent, factor }) => {
737 | speed += constituent.speed(astro) * factor;
738 | });
739 | return speed
740 | },
741 |
742 | value: (astro) => {
743 | let value = 0;
744 | members.forEach(({ constituent, factor }) => {
745 | value += constituent.value(astro) * factor;
746 | });
747 | return value
748 | },
749 |
750 | u: (astro) => {
751 | let u = 0;
752 | members.forEach(({ constituent, factor }) => {
753 | u += constituent.u(astro) * factor;
754 | });
755 | return u
756 | },
757 |
758 | f: (astro) => {
759 | const f = [];
760 | members.forEach(({ constituent, factor }) => {
761 | f.push(Math.pow(constituent.f(astro), Math.abs(factor)));
762 | });
763 | return f.reduce((previous, value) => {
764 | return previous * value
765 | })
766 | }
767 | };
768 |
769 | return Object.freeze(compoundConstituent)
770 | };
771 |
772 | const constituents = {};
773 | // Long Term
774 | constituents.Z0 = constituentFactory('Z0', [0, 0, 0, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
775 | constituents.SA = constituentFactory('Sa', [0, 0, 1, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
776 | constituents.SSA = constituentFactory(
777 | 'Ssa',
778 | [0, 0, 2, 0, 0, 0, 0],
779 | corrections.uZero,
780 | corrections.fUnity
781 | );
782 | constituents.MM = constituentFactory('MM', [0, 1, 0, -1, 0, 0, 0], corrections.uZero, corrections.fMm);
783 | constituents.MF = constituentFactory('MF', [0, 2, 0, 0, 0, 0, 0], corrections.uMf, corrections.fMf);
784 | // Diurnals
785 | constituents.Q1 = constituentFactory('Q1', [1, -2, 0, 1, 0, 0, 1], corrections.uO1, corrections.fO1);
786 | constituents.O1 = constituentFactory('O1', [1, -1, 0, 0, 0, 0, 1], corrections.uO1, corrections.fO1);
787 | constituents.K1 = constituentFactory('K1', [1, 1, 0, 0, 0, 0, -1], corrections.uK1, corrections.fK1);
788 | constituents.J1 = constituentFactory('J1', [1, 2, 0, -1, 0, 0, -1], corrections.uJ1, corrections.fJ1);
789 | constituents.M1 = constituentFactory('M1', [1, 0, 0, 0, 0, 0, 1], corrections.uM1, corrections.fM1);
790 | constituents.P1 = constituentFactory('P1', [1, 1, -2, 0, 0, 0, 1], corrections.uZero, corrections.fUnity);
791 | constituents.S1 = constituentFactory('S1', [1, 1, -1, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
792 | constituents.OO1 = constituentFactory('OO1', [1, 3, 0, 0, 0, 0, -1], corrections.uOO1, corrections.fOO1);
793 | // Semi diurnals
794 | constituents['2N2'] = constituentFactory('2N2', [2, -2, 0, 2, 0, 0, 0], corrections.uM2, corrections.fM2);
795 | constituents.N2 = constituentFactory('N2', [2, -1, 0, 1, 0, 0, 0], corrections.uM2, corrections.fM2);
796 | constituents.NU2 = constituentFactory('NU2', [2, -1, 2, -1, 0, 0, 0], corrections.uM2, corrections.fM2);
797 | constituents.M2 = constituentFactory('M2', [2, 0, 0, 0, 0, 0, 0], corrections.uM2, corrections.fM2);
798 | constituents.LAM2 = constituentFactory('LAM2', [2, 1, -2, 1, 0, 0, 2], corrections.uM2, corrections.fM2);
799 | constituents.L2 = constituentFactory('L2', [2, 1, 0, -1, 0, 0, 2], corrections.uL2, corrections.fL2);
800 | constituents.T2 = constituentFactory('T2', [2, 2, -3, 0, 0, 1, 0], corrections.uZero, corrections.fUnity);
801 | constituents.S2 = constituentFactory('S2', [2, 2, -2, 0, 0, 0, 0], corrections.uZero, corrections.fUnity);
802 | constituents.R2 = constituentFactory(
803 | 'R2',
804 | [2, 2, -1, 0, 0, -1, 2],
805 | corrections.uZero,
806 | corrections.fUnity
807 | );
808 | constituents.K2 = constituentFactory('K2', [2, 2, 0, 0, 0, 0, 0], corrections.uK2, corrections.fK2);
809 | // Third diurnal
810 | constituents.M3 = constituentFactory(
811 | 'M3',
812 | [3, 0, 0, 0, 0, 0, 0],
813 | (a) => {
814 | return corrections.uModd(a, 3)
815 | },
816 | (a) => {
817 | return corrections.fModd(a, 3)
818 | }
819 | );
820 | // Compound
821 | constituents.MSF = compoundConstituentFactory('MSF', [
822 | { constituent: constituents.S2, factor: 1 },
823 | { constituent: constituents.M2, factor: -1 }
824 | ]);
825 |
826 | // Diurnal
827 | constituents['2Q1'] = compoundConstituentFactory('2Q1', [
828 | { constituent: constituents.N2, factor: 1 },
829 | { constituent: constituents.J1, factor: -1 }
830 | ]);
831 | constituents.RHO = compoundConstituentFactory('RHO', [
832 | { constituent: constituents.NU2, factor: 1 },
833 | { constituent: constituents.K1, factor: -1 }
834 | ]);
835 |
836 | // Semi-Diurnal
837 |
838 | constituents.MU2 = compoundConstituentFactory('MU2', [
839 | { constituent: constituents.M2, factor: 2 },
840 | { constituent: constituents.S2, factor: -1 }
841 | ]);
842 | constituents['2SM2'] = compoundConstituentFactory('2SM2', [
843 | { constituent: constituents.S2, factor: 2 },
844 | { constituent: constituents.M2, factor: -1 }
845 | ]);
846 |
847 | // Third-Diurnal
848 | constituents['2MK3'] = compoundConstituentFactory('2MK3', [
849 | { constituent: constituents.M2, factor: 1 },
850 | { constituent: constituents.O1, factor: 1 }
851 | ]);
852 | constituents.MK3 = compoundConstituentFactory('MK3', [
853 | { constituent: constituents.M2, factor: 1 },
854 | { constituent: constituents.K1, factor: 1 }
855 | ]);
856 |
857 | // Quarter-Diurnal
858 | constituents.MN4 = compoundConstituentFactory('MN4', [
859 | { constituent: constituents.M2, factor: 1 },
860 | { constituent: constituents.N2, factor: 1 }
861 | ]);
862 | constituents.M4 = compoundConstituentFactory('M4', [
863 | { constituent: constituents.M2, factor: 2 }
864 | ]);
865 | constituents.MS4 = compoundConstituentFactory('MS4', [
866 | { constituent: constituents.M2, factor: 1 },
867 | { constituent: constituents.S2, factor: 1 }
868 | ]);
869 | constituents.S4 = compoundConstituentFactory('S4', [
870 | { constituent: constituents.S2, factor: 2 }
871 | ]);
872 |
873 | // Sixth-Diurnal
874 | constituents.M6 = compoundConstituentFactory('M6', [
875 | { constituent: constituents.M2, factor: 3 }
876 | ]);
877 | constituents.S6 = compoundConstituentFactory('S6', [
878 | { constituent: constituents.S2, factor: 3 }
879 | ]);
880 |
881 | // Eighth-Diurnals
882 | constituents.M8 = compoundConstituentFactory('M8', [
883 | { constituent: constituents.M2, factor: 4 }
884 | ]);
885 |
886 | const getDate = (time) => {
887 | if (time instanceof Date) {
888 | return time
889 | }
890 | if (typeof time === 'number') {
891 | return new Date(time * 1000)
892 | }
893 | throw new Error('Invalid date format, should be a Date object, or timestamp')
894 | };
895 |
896 | const getTimeline = (start, end, seconds) => {
897 | seconds = typeof seconds !== 'undefined' ? seconds : 10 * 60;
898 | const items = [];
899 | const endTime = end.getTime() / 1000;
900 | let lastTime = start.getTime() / 1000;
901 | const startTime = lastTime;
902 | const hours = [];
903 | while (lastTime <= endTime) {
904 | items.push(new Date(lastTime * 1000));
905 | hours.push((lastTime - startTime) / (60 * 60));
906 | lastTime += seconds;
907 | }
908 |
909 | return {
910 | items,
911 | hours
912 | }
913 | };
914 |
915 | const harmonicsFactory = ({ harmonicConstituents, phaseKey, offset }) => {
916 | if (!Array.isArray(harmonicConstituents)) {
917 | throw new Error('Harmonic constituents are not an array')
918 | }
919 | const constituents$1 = [];
920 | harmonicConstituents.forEach((constituent) => {
921 | if (typeof constituent.name === 'undefined') {
922 | throw new Error('Harmonic constituents must have a name property')
923 | }
924 | if (typeof constituents[constituent.name] !== 'undefined') {
925 | constituent._model = constituents[constituent.name];
926 | constituent._phase = d2r * constituent[phaseKey];
927 | constituents$1.push(constituent);
928 | }
929 | });
930 |
931 | if (offset !== false) {
932 | constituents$1.push({
933 | name: 'Z0',
934 | _model: constituents.Z0,
935 | _phase: 0,
936 | amplitude: offset
937 | });
938 | }
939 |
940 | let start = new Date();
941 | let end = new Date();
942 |
943 | const harmonics = {};
944 |
945 | harmonics.setTimeSpan = (startTime, endTime) => {
946 | start = getDate(startTime);
947 | end = getDate(endTime);
948 | if (start.getTime() >= end.getTime()) {
949 | throw new Error('Start time must be before end time')
950 | }
951 | return harmonics
952 | };
953 |
954 | harmonics.prediction = (options) => {
955 | options =
956 | typeof options !== 'undefined' ? options : { timeFidelity: 10 * 60 };
957 | return predictionFactory({
958 | timeline: getTimeline(start, end, options.timeFidelity),
959 | constituents: constituents$1,
960 | start
961 | })
962 | };
963 |
964 | return Object.freeze(harmonics)
965 | };
966 |
967 | const tidePredictionFactory = (constituents, options) => {
968 | const harmonicsOptions = {
969 | harmonicConstituents: constituents,
970 | phaseKey: 'phase_GMT',
971 | offset: false
972 | };
973 |
974 | if (typeof options !== 'undefined') {
975 | Object.keys(harmonicsOptions).forEach((key) => {
976 | if (typeof options[key] !== 'undefined') {
977 | harmonicsOptions[key] = options[key];
978 | }
979 | });
980 | }
981 |
982 | const tidePrediction = {
983 | getTimelinePrediction: ({ start, end }) => {
984 | return harmonicsFactory(harmonicsOptions)
985 | .setTimeSpan(start, end)
986 | .prediction()
987 | .getTimelinePrediction()
988 | },
989 |
990 | getExtremesPrediction: ({ start, end, labels, offsets, timeFidelity }) => {
991 | return harmonicsFactory(harmonicsOptions)
992 | .setTimeSpan(start, end)
993 | .prediction({ timeFidelity })
994 | .getExtremesPrediction(labels, offsets)
995 | },
996 |
997 | getWaterLevelAtTime: ({ time }) => {
998 | const endDate = new Date(time.getTime() + 10 * 60 * 1000);
999 | return harmonicsFactory(harmonicsOptions)
1000 | .setTimeSpan(time, endDate)
1001 | .prediction()
1002 | .getTimelinePrediction()[0]
1003 | }
1004 | };
1005 |
1006 | return tidePrediction
1007 | };
1008 |
1009 | return tidePredictionFactory;
1010 |
1011 | }));
1012 |
--------------------------------------------------------------------------------
/dist/web/tide-predictor.min.js:
--------------------------------------------------------------------------------
1 | (function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=typeof globalThis!=="undefined"?globalThis:global||self,global.tidePredictor=factory())})(this,function(){"use strict";const d2r=Math.PI/180;const r2d=180/Math.PI;const sexagesimalToDecimal=(degrees,arcmins,arcsecs,mas,muas)=>{arcmins=typeof arcmins!=="undefined"?arcmins:0;arcsecs=typeof arcsecs!=="undefined"?arcsecs:0;mas=typeof mas!=="undefined"?mas:0;muas=typeof muas!=="undefined"?muas:0;return degrees+arcmins/60+arcsecs/(60*60)+mas/(60*60*1e3)+muas/(60*60*1e6)};const coefficients={terrestrialObliquity:[sexagesimalToDecimal(23,26,21.448),-sexagesimalToDecimal(0,0,4680.93),-sexagesimalToDecimal(0,0,1.55),sexagesimalToDecimal(0,0,1999.25),-sexagesimalToDecimal(0,0,51.38),-sexagesimalToDecimal(0,0,249.67),-sexagesimalToDecimal(0,0,39.05),sexagesimalToDecimal(0,0,7.12),sexagesimalToDecimal(0,0,27.87),sexagesimalToDecimal(0,0,5.79),sexagesimalToDecimal(0,0,2.45)].map((number,index)=>{return number*Math.pow(.01,index)}),solarPerigee:[280.46645-357.5291,36000.76932-35999.0503,3032e-7+1559e-7,48e-8],solarLongitude:[280.46645,36000.76983,3032e-7],lunarInclination:[5.145],lunarLongitude:[218.3164591,481267.88134236,-.0013268,1/538841-1/65194e3],lunarNode:[125.044555,-1934.1361849,.0020762,1/467410,-1/60616e3],lunarPerigee:[83.353243,4069.0137111,-.0103238,-1/80053,1/18999e3]};const polynomial=(coefficients,argument)=>{const result=[];coefficients.forEach((coefficient,index)=>{result.push(coefficient*Math.pow(argument,index))});return result.reduce((a,b)=>{return a+b})};const derivativePolynomial=(coefficients,argument)=>{const result=[];coefficients.forEach((coefficient,index)=>{result.push(coefficient*index*Math.pow(argument,index-1))});return result.reduce((a,b)=>{return a+b})};const T=t=>{return(JD(t)-2451545)/36525};const JD=t=>{let Y=t.getFullYear();let M=t.getMonth()+1;const D=t.getDate()+t.getHours()/24+t.getMinutes()/(24*60)+t.getSeconds()/(24*60*60)+t.getMilliseconds()/(24*60*60*1e6);if(M<=2){Y=Y-1;M=M+12}const A=Math.floor(Y/100);const B=2-A+Math.floor(A/4);return Math.floor(365.25*(Y+4716))+Math.floor(30.6001*(M+1))+D+B-1524.5};const _I=(N,i,omega)=>{N=d2r*N;i=d2r*i;omega=d2r*omega;const cosI=Math.cos(i)*Math.cos(omega)-Math.sin(i)*Math.sin(omega)*Math.cos(N);return r2d*Math.acos(cosI)};const _xi=(N,i,omega)=>{N=d2r*N;i=d2r*i;omega=d2r*omega;let e1=Math.cos(.5*(omega-i))/Math.cos(.5*(omega+i))*Math.tan(.5*N);let e2=Math.sin(.5*(omega-i))/Math.sin(.5*(omega+i))*Math.tan(.5*N);e1=Math.atan(e1);e2=Math.atan(e2);e1=e1-.5*N;e2=e2-.5*N;return-(e1+e2)*r2d};const _nu=(N,i,omega)=>{N=d2r*N;i=d2r*i;omega=d2r*omega;let e1=Math.cos(.5*(omega-i))/Math.cos(.5*(omega+i))*Math.tan(.5*N);let e2=Math.sin(.5*(omega-i))/Math.sin(.5*(omega+i))*Math.tan(.5*N);e1=Math.atan(e1);e2=Math.atan(e2);e1=e1-.5*N;e2=e2-.5*N;return(e1-e2)*r2d};const _nup=(N,i,omega)=>{const I=d2r*_I(N,i,omega);const nu=d2r*_nu(N,i,omega);return r2d*Math.atan(Math.sin(2*I)*Math.sin(nu)/(Math.sin(2*I)*Math.cos(nu)+.3347))};const _nupp=(N,i,omega)=>{const I=d2r*_I(N,i,omega);const nu=d2r*_nu(N,i,omega);const tan2nupp=Math.sin(I)**2*Math.sin(2*nu)/(Math.sin(I)**2*Math.cos(2*nu)+.0727);return r2d*.5*Math.atan(tan2nupp)};const modulus$1=(a,b)=>{return(a%b+b)%b};const astro=time=>{const result={};const polynomials={s:coefficients.lunarLongitude,h:coefficients.solarLongitude,p:coefficients.lunarPerigee,N:coefficients.lunarNode,pp:coefficients.solarPerigee,90:[90],omega:coefficients.terrestrialObliquity,i:coefficients.lunarInclination};const dTdHour=1/(24*365.25*100);Object.keys(polynomials).forEach(name=>{result[name]={value:modulus$1(polynomial(polynomials[name],T(time)),360),speed:derivativePolynomial(polynomials[name],T(time))*dTdHour}});const functions={I:_I,xi:_xi,nu:_nu,nup:_nup,nupp:_nupp};Object.keys(functions).forEach(name=>{const functionCall=functions[name];result[name]={value:modulus$1(functionCall(result.N.value,result.i.value,result.omega.value),360),speed:null}});const hour={value:(JD(time)-Math.floor(JD(time)))*360,speed:15};result["T+h-s"]={value:hour.value+result.h.value-result.s.value,speed:hour.speed+result.h.speed-result.s.speed};result.P={value:result.p.value-result.xi.value%360,speed:null};return result};const modulus=(a,b)=>{return(a%b+b)%b};const addExtremesOffsets=(extreme,offsets)=>{if(typeof offsets==="undefined"||!offsets){return extreme}if(extreme.high&&offsets.height_offset&&offsets.height_offset.high){extreme.level*=offsets.height_offset.high}if(extreme.low&&offsets.height_offset&&offsets.height_offset.low){extreme.level*=offsets.height_offset.low}if(extreme.high&&offsets.time_offset&&offsets.time_offset.high){extreme.time=new Date(extreme.time.getTime()+offsets.time_offset.high*60*1e3)}if(extreme.low&&offsets.time_offset&&offsets.time_offset.low){extreme.time=new Date(extreme.time.getTime()+offsets.time_offset.low*60*1e3)}return extreme};const getExtremeLabel=(label,highLowLabels)=>{if(typeof highLowLabels!=="undefined"&&typeof highLowLabels[label]!=="undefined"){return highLowLabels[label]}const labels={high:"High",low:"Low"};return labels[label]};const predictionFactory=({timeline,constituents,start})=>{const getLevel=(hour,modelBaseSpeed,modelU,modelF,modelBaseValue)=>{const amplitudes=[];let result=0;constituents.forEach(constituent=>{const amplitude=constituent.amplitude;const phase=constituent._phase;const f=modelF[constituent.name];const speed=modelBaseSpeed[constituent.name];const u=modelU[constituent.name];const V0=modelBaseValue[constituent.name];amplitudes.push(amplitude*f*Math.cos(speed*hour+(V0+u)-phase))});amplitudes.forEach(item=>{result+=item});return result};const prediction={};prediction.getExtremesPrediction=options=>{const{labels,offsets}=typeof options!=="undefined"?options:{};const results=[];const{baseSpeed,u,f,baseValue}=prepare();let goingUp=false;let goingDown=false;let lastLevel=getLevel(0,baseSpeed,u[0],f[0],baseValue);timeline.items.forEach((time,index)=>{const hour=timeline.hours[index];const level=getLevel(hour,baseSpeed,u[index],f[index],baseValue);if(level>lastLevel&&goingDown){results.push(addExtremesOffsets({time:timeline.items[index-1],level:lastLevel,high:false,low:true,label:getExtremeLabel("low",labels)},offsets))}if(levellastLevel){goingUp=true;goingDown=false}if(level{const results=[];const{baseSpeed,u,f,baseValue}=prepare();timeline.items.forEach((time,index)=>{const hour=timeline.hours[index];const prediction={time:time,hour:hour,level:getLevel(hour,baseSpeed,u[index],f[index],baseValue)};results.push(prediction)});return results};const prepare=()=>{const baseAstro=astro(start);const baseValue={};const baseSpeed={};const u=[];const f=[];constituents.forEach(constituent=>{const value=constituent._model.value(baseAstro);const speed=constituent._model.speed(baseAstro);baseValue[constituent.name]=d2r*value;baseSpeed[constituent.name]=d2r*speed});timeline.items.forEach(time=>{const uItem={};const fItem={};const itemAstro=astro(time);constituents.forEach(constituent=>{const constituentU=modulus(constituent._model.u(itemAstro),360);uItem[constituent.name]=d2r*constituentU;fItem[constituent.name]=modulus(constituent._model.f(itemAstro),360)});u.push(uItem);f.push(fItem)});return{baseValue:baseValue,baseSpeed:baseSpeed,u:u,f:f}};return Object.freeze(prediction)};const corrections={fUnity(){return 1},fMm(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const mean=(2/3-Math.pow(Math.sin(omega),2))*(1-3/2*Math.pow(Math.sin(i),2));return(2/3-Math.pow(Math.sin(I),2))/mean},fMf(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const mean=Math.pow(Math.sin(omega),2)*Math.pow(Math.cos(.5*i),4);return Math.pow(Math.sin(I),2)/mean},fO1(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const mean=Math.sin(omega)*Math.pow(Math.cos(.5*omega),2)*Math.pow(Math.cos(.5*i),4);return Math.sin(I)*Math.pow(Math.cos(.5*I),2)/mean},fJ1(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const mean=Math.sin(2*omega)*(1-3/2*Math.pow(Math.sin(i),2));return Math.sin(2*I)/mean},fOO1(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const mean=Math.sin(omega)*Math.pow(Math.sin(.5*omega),2)*Math.pow(Math.cos(.5*i),4);return Math.sin(I)*Math.pow(Math.sin(.5*I),2)/mean},fM2(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const mean=Math.pow(Math.cos(.5*omega),4)*Math.pow(Math.cos(.5*i),4);return Math.pow(Math.cos(.5*I),4)/mean},fK1(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const nu=d2r*a.nu.value;const sin2IcosnuMean=Math.sin(2*omega)*(1-3/2*Math.pow(Math.sin(i),2));const mean=.5023*sin2IcosnuMean+.1681;return Math.pow(.2523*Math.pow(Math.sin(2*I),2)+.1689*Math.sin(2*I)*Math.cos(nu)+.0283,.5)/mean},fL2(a){const P=d2r*a.P.value;const I=d2r*a.I.value;const rAInv=Math.pow(1-12*Math.pow(Math.tan(.5*I),2)*Math.cos(2*P)+36*Math.pow(Math.tan(.5*I),4),.5);return corrections.fM2(a)*rAInv},fK2(a){const omega=d2r*a.omega.value;const i=d2r*a.i.value;const I=d2r*a.I.value;const nu=d2r*a.nu.value;const sinsqIcos2nuMean=Math.sin(omega)**2*(1-3/2*Math.sin(i)**2);const mean=.5023*sinsqIcos2nuMean+.0365;return Math.pow(.2523*Math.pow(Math.sin(I),4)+.0367*Math.pow(Math.sin(I),2)*Math.cos(2*nu)+.0013,.5)/mean},fM1(a){const P=d2r*a.P.value;const I=d2r*a.I.value;const qAInv=Math.pow(.25+1.5*Math.cos(I)*Math.cos(2*P)*Math.pow(Math.cos(.5*I),-.5)+2.25*Math.pow(Math.cos(I),2)*Math.pow(Math.cos(.5*I),-4),.5);return corrections.fO1(a)*qAInv},fModd(a,n){return Math.pow(corrections.fM2(a),n/2)},uZero(){return 0},uMf(a){return-2*a.xi.value},uO1(a){return 2*a.xi.value-a.nu.value},uJ1(a){return-a.nu.value},uOO1(a){return-2*a.xi.value-a.nu.value},uM2(a){return 2*a.xi.value-2*a.nu.value},uK1(a){return-a.nup.value},uL2(a){const I=d2r*a.I.value;const P=d2r*a.P.value;const R=r2d*Math.atan(Math.sin(2*P)/(1/6*Math.pow(Math.tan(.5*I),-2)-Math.cos(2*P)));return 2*a.xi.value-2*a.nu.value-R},uK2(a){return-2*a.nupp.value},uM1(a){const I=d2r*a.I.value;const P=d2r*a.P.value;const Q=r2d*Math.atan((5*Math.cos(I)-1)/(7*Math.cos(I)+1)*Math.tan(P));return a.xi.value-a.nu.value+Q},uModd(a,n){return n/2*corrections.uM2(a)}};const dotArray=(a,b)=>{const results=[];a.forEach((value,index)=>{results.push(value*b[index])});return results.reduce((total,value)=>{return total+value})};const astronimicDoodsonNumber=astro=>{return[astro["T+h-s"],astro.s,astro.h,astro.p,astro.N,astro.pp,astro["90"]]};const astronomicSpeed=astro=>{const results=[];astronimicDoodsonNumber(astro).forEach(number=>{results.push(number.speed)});return results};const astronomicValues=astro=>{const results=[];astronimicDoodsonNumber(astro).forEach(number=>{results.push(number.value)});return results};const constituentFactory=(name,coefficients,u,f)=>{if(!coefficients){throw new Error("Coefficient must be defined for a constituent")}const constituent={name:name,coefficients:coefficients,value:astro=>{return dotArray(coefficients,astronomicValues(astro))},speed(astro){return dotArray(coefficients,astronomicSpeed(astro))},u:typeof u!=="undefined"?u:corrections.uZero,f:typeof f!=="undefined"?f:corrections.fUnity};return Object.freeze(constituent)};const compoundConstituentFactory=(name,members)=>{const coefficients=[];members.forEach(({constituent,factor})=>{constituent.coefficients.forEach((coefficient,index)=>{if(typeof coefficients[index]==="undefined"){coefficients[index]=0}coefficients[index]+=coefficient*factor})});const compoundConstituent={name:name,coefficients:coefficients,speed:astro=>{let speed=0;members.forEach(({constituent,factor})=>{speed+=constituent.speed(astro)*factor});return speed},value:astro=>{let value=0;members.forEach(({constituent,factor})=>{value+=constituent.value(astro)*factor});return value},u:astro=>{let u=0;members.forEach(({constituent,factor})=>{u+=constituent.u(astro)*factor});return u},f:astro=>{const f=[];members.forEach(({constituent,factor})=>{f.push(Math.pow(constituent.f(astro),Math.abs(factor)))});return f.reduce((previous,value)=>{return previous*value})}};return Object.freeze(compoundConstituent)};const constituents={};constituents.Z0=constituentFactory("Z0",[0,0,0,0,0,0,0],corrections.uZero,corrections.fUnity);constituents.SA=constituentFactory("Sa",[0,0,1,0,0,0,0],corrections.uZero,corrections.fUnity);constituents.SSA=constituentFactory("Ssa",[0,0,2,0,0,0,0],corrections.uZero,corrections.fUnity);constituents.MM=constituentFactory("MM",[0,1,0,-1,0,0,0],corrections.uZero,corrections.fMm);constituents.MF=constituentFactory("MF",[0,2,0,0,0,0,0],corrections.uMf,corrections.fMf);constituents.Q1=constituentFactory("Q1",[1,-2,0,1,0,0,1],corrections.uO1,corrections.fO1);constituents.O1=constituentFactory("O1",[1,-1,0,0,0,0,1],corrections.uO1,corrections.fO1);constituents.K1=constituentFactory("K1",[1,1,0,0,0,0,-1],corrections.uK1,corrections.fK1);constituents.J1=constituentFactory("J1",[1,2,0,-1,0,0,-1],corrections.uJ1,corrections.fJ1);constituents.M1=constituentFactory("M1",[1,0,0,0,0,0,1],corrections.uM1,corrections.fM1);constituents.P1=constituentFactory("P1",[1,1,-2,0,0,0,1],corrections.uZero,corrections.fUnity);constituents.S1=constituentFactory("S1",[1,1,-1,0,0,0,0],corrections.uZero,corrections.fUnity);constituents.OO1=constituentFactory("OO1",[1,3,0,0,0,0,-1],corrections.uOO1,corrections.fOO1);constituents["2N2"]=constituentFactory("2N2",[2,-2,0,2,0,0,0],corrections.uM2,corrections.fM2);constituents.N2=constituentFactory("N2",[2,-1,0,1,0,0,0],corrections.uM2,corrections.fM2);constituents.NU2=constituentFactory("NU2",[2,-1,2,-1,0,0,0],corrections.uM2,corrections.fM2);constituents.M2=constituentFactory("M2",[2,0,0,0,0,0,0],corrections.uM2,corrections.fM2);constituents.LAM2=constituentFactory("LAM2",[2,1,-2,1,0,0,2],corrections.uM2,corrections.fM2);constituents.L2=constituentFactory("L2",[2,1,0,-1,0,0,2],corrections.uL2,corrections.fL2);constituents.T2=constituentFactory("T2",[2,2,-3,0,0,1,0],corrections.uZero,corrections.fUnity);constituents.S2=constituentFactory("S2",[2,2,-2,0,0,0,0],corrections.uZero,corrections.fUnity);constituents.R2=constituentFactory("R2",[2,2,-1,0,0,-1,2],corrections.uZero,corrections.fUnity);constituents.K2=constituentFactory("K2",[2,2,0,0,0,0,0],corrections.uK2,corrections.fK2);constituents.M3=constituentFactory("M3",[3,0,0,0,0,0,0],a=>{return corrections.uModd(a,3)},a=>{return corrections.fModd(a,3)});constituents.MSF=compoundConstituentFactory("MSF",[{constituent:constituents.S2,factor:1},{constituent:constituents.M2,factor:-1}]);constituents["2Q1"]=compoundConstituentFactory("2Q1",[{constituent:constituents.N2,factor:1},{constituent:constituents.J1,factor:-1}]);constituents.RHO=compoundConstituentFactory("RHO",[{constituent:constituents.NU2,factor:1},{constituent:constituents.K1,factor:-1}]);constituents.MU2=compoundConstituentFactory("MU2",[{constituent:constituents.M2,factor:2},{constituent:constituents.S2,factor:-1}]);constituents["2SM2"]=compoundConstituentFactory("2SM2",[{constituent:constituents.S2,factor:2},{constituent:constituents.M2,factor:-1}]);constituents["2MK3"]=compoundConstituentFactory("2MK3",[{constituent:constituents.M2,factor:1},{constituent:constituents.O1,factor:1}]);constituents.MK3=compoundConstituentFactory("MK3",[{constituent:constituents.M2,factor:1},{constituent:constituents.K1,factor:1}]);constituents.MN4=compoundConstituentFactory("MN4",[{constituent:constituents.M2,factor:1},{constituent:constituents.N2,factor:1}]);constituents.M4=compoundConstituentFactory("M4",[{constituent:constituents.M2,factor:2}]);constituents.MS4=compoundConstituentFactory("MS4",[{constituent:constituents.M2,factor:1},{constituent:constituents.S2,factor:1}]);constituents.S4=compoundConstituentFactory("S4",[{constituent:constituents.S2,factor:2}]);constituents.M6=compoundConstituentFactory("M6",[{constituent:constituents.M2,factor:3}]);constituents.S6=compoundConstituentFactory("S6",[{constituent:constituents.S2,factor:3}]);constituents.M8=compoundConstituentFactory("M8",[{constituent:constituents.M2,factor:4}]);const getDate=time=>{if(time instanceof Date){return time}if(typeof time==="number"){return new Date(time*1e3)}throw new Error("Invalid date format, should be a Date object, or timestamp")};const getTimeline=(start,end,seconds)=>{seconds=typeof seconds!=="undefined"?seconds:10*60;const items=[];const endTime=end.getTime()/1e3;let lastTime=start.getTime()/1e3;const startTime=lastTime;const hours=[];while(lastTime<=endTime){items.push(new Date(lastTime*1e3));hours.push((lastTime-startTime)/(60*60));lastTime+=seconds}return{items:items,hours:hours}};const harmonicsFactory=({harmonicConstituents,phaseKey,offset})=>{if(!Array.isArray(harmonicConstituents)){throw new Error("Harmonic constituents are not an array")}const constituents$1=[];harmonicConstituents.forEach(constituent=>{if(typeof constituent.name==="undefined"){throw new Error("Harmonic constituents must have a name property")}if(typeof constituents[constituent.name]!=="undefined"){constituent._model=constituents[constituent.name];constituent._phase=d2r*constituent[phaseKey];constituents$1.push(constituent)}});if(offset!==false){constituents$1.push({name:"Z0",_model:constituents.Z0,_phase:0,amplitude:offset})}let start=new Date;let end=new Date;const harmonics={};harmonics.setTimeSpan=(startTime,endTime)=>{start=getDate(startTime);end=getDate(endTime);if(start.getTime()>=end.getTime()){throw new Error("Start time must be before end time")}return harmonics};harmonics.prediction=options=>{options=typeof options!=="undefined"?options:{timeFidelity:10*60};return predictionFactory({timeline:getTimeline(start,end,options.timeFidelity),constituents:constituents$1,start:start})};return Object.freeze(harmonics)};const tidePredictionFactory=(constituents,options)=>{const harmonicsOptions={harmonicConstituents:constituents,phaseKey:"phase_GMT",offset:false};if(typeof options!=="undefined"){Object.keys(harmonicsOptions).forEach(key=>{if(typeof options[key]!=="undefined"){harmonicsOptions[key]=options[key]}})}const tidePrediction={getTimelinePrediction:({start,end})=>{return harmonicsFactory(harmonicsOptions).setTimeSpan(start,end).prediction().getTimelinePrediction()},getExtremesPrediction:({start,end,labels,offsets,timeFidelity})=>{return harmonicsFactory(harmonicsOptions).setTimeSpan(start,end).prediction({timeFidelity:timeFidelity}).getExtremesPrediction(labels,offsets)},getWaterLevelAtTime:({time})=>{const endDate=new Date(time.getTime()+10*60*1e3);return harmonicsFactory(harmonicsOptions).setTimeSpan(time,endDate).prediction().getTimelinePrediction()[0]}};return tidePrediction};return tidePredictionFactory});
--------------------------------------------------------------------------------
/examples/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tide Predictor in the browser
5 |
6 |
7 |
8 |
9 |
High/low tides for Monterey, CA
10 |
11 |
12 |
13 | Time |
14 | High/Low |
15 | Level (meters) |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
52 |
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@neaps/tide-predictor",
3 | "version": "0.1.1",
4 | "description": "Tide predictor",
5 | "repository": "https://github.com/neaps/tide-predictor",
6 | "author": "Kevin Miller ",
7 | "license": "MIT",
8 | "type": "module",
9 | "module": "./src/index.js",
10 | "browser": "./dist/web/tide-predictor.js",
11 | "commonjs": "./dist/commonjs/index.js",
12 | "exports": {
13 | ".": {
14 | "import": "./src/index.js",
15 | "require": "./dist/commonjs/index.js"
16 | }
17 | },
18 | "devDependencies": {
19 | "c8": "^7.11.3",
20 | "eslint": "^8.18.0",
21 | "mocha": "^10.0.0",
22 | "npm-run-all": "^4.1.5",
23 | "rollup-plugin-node-resolve": "^5.2.0",
24 | "uglify-js": "^3.16.1"
25 | },
26 | "scripts": {
27 | "test": "mocha --recursive",
28 | "lint": "eslint ./src",
29 | "coverage": "c8 --reporter=lcov mocha --recursive && codecov",
30 | "ci": "run-s lint test coverage",
31 | "build:rollup": "rollup --config rollup.config.js",
32 | "build:uglify": "uglifyjs ./dist/web/tide-predictor.js -o ./dist/web/tide-predictor.min.js",
33 | "build:commonDist": "echo \"{\\\"type\\\":\\\"commonjs\\\"}\" > ./dist/commonjs/package.json",
34 | "build": "run-s build:rollup build:commonDist build:uglify"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import pkg from './package.json'
2 | import resolve from 'rollup-plugin-node-resolve'
3 |
4 | export default {
5 | input: './src/index.js',
6 | output: [
7 | {
8 | name: 'tidePredictor',
9 | file: pkg.browser,
10 | format: 'umd'
11 | },
12 | { file: pkg.commonjs, format: 'commonjs', exports: 'default' }
13 | ],
14 | plugins: [
15 | resolve({
16 | mainFields: ['module', 'main'],
17 |
18 | jail: '/src'
19 | })
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/src/astronomy/coefficients.js:
--------------------------------------------------------------------------------
1 | // Convert a sexagesimal angle into decimal degrees
2 | const sexagesimalToDecimal = (degrees, arcmins, arcsecs, mas, muas) => {
3 | arcmins = typeof arcmins !== 'undefined' ? arcmins : 0
4 | arcsecs = typeof arcsecs !== 'undefined' ? arcsecs : 0
5 | mas = typeof mas !== 'undefined' ? mas : 0
6 | muas = typeof muas !== 'undefined' ? muas : 0
7 |
8 | return (
9 | degrees +
10 | arcmins / 60.0 +
11 | arcsecs / (60.0 * 60.0) +
12 | mas / (60.0 * 60.0 * 1e3) +
13 | muas / (60.0 * 60.0 * 1e6)
14 | )
15 | }
16 |
17 | const coefficients = {
18 | // Meeus formula 21.3
19 | terrestrialObliquity: [
20 | sexagesimalToDecimal(23, 26, 21.448),
21 | -sexagesimalToDecimal(0, 0, 4680.93),
22 | -sexagesimalToDecimal(0, 0, 1.55),
23 | sexagesimalToDecimal(0, 0, 1999.25),
24 | -sexagesimalToDecimal(0, 0, 51.38),
25 | -sexagesimalToDecimal(0, 0, 249.67),
26 | -sexagesimalToDecimal(0, 0, 39.05),
27 | sexagesimalToDecimal(0, 0, 7.12),
28 | sexagesimalToDecimal(0, 0, 27.87),
29 | sexagesimalToDecimal(0, 0, 5.79),
30 | sexagesimalToDecimal(0, 0, 2.45),
31 | ].map((number, index) => {
32 | return number * Math.pow(1e-2, index)
33 | }),
34 |
35 | solarPerigee: [
36 | 280.46645 - 357.5291,
37 | 36000.76932 - 35999.0503,
38 | 0.0003032 + 0.0001559,
39 | 0.00000048,
40 | ],
41 |
42 | solarLongitude: [280.46645, 36000.76983, 0.0003032],
43 |
44 | lunarInclination: [5.145],
45 |
46 | lunarLongitude: [
47 | 218.3164591,
48 | 481267.88134236,
49 | -0.0013268,
50 | 1 / 538841.0 - 1 / 65194000.0,
51 | ],
52 |
53 | lunarNode: [
54 | 125.044555,
55 | -1934.1361849,
56 | 0.0020762,
57 | 1 / 467410.0,
58 | -1 / 60616000.0,
59 | ],
60 |
61 | lunarPerigee: [
62 | 83.353243,
63 | 4069.0137111,
64 | -0.0103238,
65 | -1 / 80053.0,
66 | 1 / 18999000.0,
67 | ],
68 | }
69 |
70 | export default coefficients
71 |
72 | export { sexagesimalToDecimal }
73 |
--------------------------------------------------------------------------------
/src/astronomy/constants.js:
--------------------------------------------------------------------------------
1 | const d2r = Math.PI / 180.0
2 | const r2d = 180.0 / Math.PI
3 |
4 | export { d2r, r2d }
5 |
--------------------------------------------------------------------------------
/src/astronomy/index.js:
--------------------------------------------------------------------------------
1 | import { d2r, r2d } from './constants.js'
2 | import coefficients from './coefficients.js'
3 |
4 | // Evaluates a polynomial at argument
5 | const polynomial = (coefficients, argument) => {
6 | const result = []
7 | coefficients.forEach((coefficient, index) => {
8 | result.push(coefficient * Math.pow(argument, index))
9 | })
10 | return result.reduce((a, b) => {
11 | return a + b
12 | })
13 | }
14 |
15 | // Evaluates a derivative polynomial at argument
16 | const derivativePolynomial = (coefficients, argument) => {
17 | const result = []
18 | coefficients.forEach((coefficient, index) => {
19 | result.push(coefficient * index * Math.pow(argument, index - 1))
20 | })
21 | return result.reduce((a, b) => {
22 | return a + b
23 | })
24 | }
25 |
26 | // Meeus formula 11.1
27 | const T = (t) => {
28 | return (JD(t) - 2451545.0) / 36525
29 | }
30 |
31 | // Meeus formula 7.1
32 | const JD = (t) => {
33 | let Y = t.getFullYear()
34 | let M = t.getMonth() + 1
35 | const D =
36 | t.getDate() +
37 | t.getHours() / 24.0 +
38 | t.getMinutes() / (24.0 * 60.0) +
39 | t.getSeconds() / (24.0 * 60.0 * 60.0) +
40 | t.getMilliseconds() / (24.0 * 60.0 * 60.0 * 1e6)
41 | if (M <= 2) {
42 | Y = Y - 1
43 | M = M + 12
44 | }
45 | const A = Math.floor(Y / 100.0)
46 | const B = 2 - A + Math.floor(A / 4.0)
47 | return (
48 | Math.floor(365.25 * (Y + 4716)) +
49 | Math.floor(30.6001 * (M + 1)) +
50 | D +
51 | B -
52 | 1524.5
53 | )
54 | }
55 |
56 | /**
57 | * @todo - What's with the array returned from the arccos?
58 | * @param {*} N
59 | * @param {*} i
60 | * @param {*} omega
61 | */
62 | const _I = (N, i, omega) => {
63 | N = d2r * N
64 | i = d2r * i
65 | omega = d2r * omega
66 | const cosI =
67 | Math.cos(i) * Math.cos(omega) - Math.sin(i) * Math.sin(omega) * Math.cos(N)
68 | return r2d * Math.acos(cosI)
69 | }
70 |
71 | const _xi = (N, i, omega) => {
72 | N = d2r * N
73 | i = d2r * i
74 | omega = d2r * omega
75 | let e1 =
76 | (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
77 | Math.tan(0.5 * N)
78 | let e2 =
79 | (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
80 | Math.tan(0.5 * N)
81 | e1 = Math.atan(e1)
82 | e2 = Math.atan(e2)
83 | e1 = e1 - 0.5 * N
84 | e2 = e2 - 0.5 * N
85 | return -(e1 + e2) * r2d
86 | }
87 |
88 | const _nu = (N, i, omega) => {
89 | N = d2r * N
90 | i = d2r * i
91 | omega = d2r * omega
92 | let e1 =
93 | (Math.cos(0.5 * (omega - i)) / Math.cos(0.5 * (omega + i))) *
94 | Math.tan(0.5 * N)
95 | let e2 =
96 | (Math.sin(0.5 * (omega - i)) / Math.sin(0.5 * (omega + i))) *
97 | Math.tan(0.5 * N)
98 | e1 = Math.atan(e1)
99 | e2 = Math.atan(e2)
100 | e1 = e1 - 0.5 * N
101 | e2 = e2 - 0.5 * N
102 | return (e1 - e2) * r2d
103 | }
104 |
105 | // Schureman equation 224
106 | const _nup = (N, i, omega) => {
107 | const I = d2r * _I(N, i, omega)
108 | const nu = d2r * _nu(N, i, omega)
109 | return (
110 | r2d *
111 | Math.atan(
112 | (Math.sin(2 * I) * Math.sin(nu)) /
113 | (Math.sin(2 * I) * Math.cos(nu) + 0.3347)
114 | )
115 | )
116 | }
117 |
118 | // Schureman equation 232
119 | const _nupp = (N, i, omega) => {
120 | const I = d2r * _I(N, i, omega)
121 | const nu = d2r * _nu(N, i, omega)
122 | const tan2nupp =
123 | (Math.sin(I) ** 2 * Math.sin(2 * nu)) /
124 | (Math.sin(I) ** 2 * Math.cos(2 * nu) + 0.0727)
125 | return r2d * 0.5 * Math.atan(tan2nupp)
126 | }
127 |
128 | const modulus = (a, b) => {
129 | return ((a % b) + b) % b
130 | }
131 |
132 | const astro = (time) => {
133 | const result = {}
134 | const polynomials = {
135 | s: coefficients.lunarLongitude,
136 | h: coefficients.solarLongitude,
137 | p: coefficients.lunarPerigee,
138 | N: coefficients.lunarNode,
139 | pp: coefficients.solarPerigee,
140 | 90: [90.0],
141 | omega: coefficients.terrestrialObliquity,
142 | i: coefficients.lunarInclination
143 | }
144 |
145 | // Polynomials are in T, that is Julian Centuries; we want our speeds to be
146 | // in the more convenient unit of degrees per hour.
147 | const dTdHour = 1 / (24 * 365.25 * 100)
148 | Object.keys(polynomials).forEach((name) => {
149 | result[name] = {
150 | value: modulus(polynomial(polynomials[name], T(time)), 360.0),
151 | speed: derivativePolynomial(polynomials[name], T(time)) * dTdHour
152 | }
153 | })
154 |
155 | // Some other parameters defined by Schureman which are dependent on the
156 | // parameters N, i, omega for use in node factor calculations. We don't need
157 | // their speeds.
158 | const functions = {
159 | I: _I,
160 | xi: _xi,
161 | nu: _nu,
162 | nup: _nup,
163 | nupp: _nupp
164 | }
165 | Object.keys(functions).forEach((name) => {
166 | const functionCall = functions[name]
167 | result[name] = {
168 | value: modulus(
169 | functionCall(result.N.value, result.i.value, result.omega.value),
170 | 360.0
171 | ),
172 | speed: null
173 | }
174 | })
175 |
176 | // We don't work directly with the T (hours) parameter, instead our spanning
177 | // set for equilibrium arguments #is given by T+h-s, s, h, p, N, pp, 90.
178 | // This is in line with convention.
179 | const hour = {
180 | value: (JD(time) - Math.floor(JD(time))) * 360.0,
181 | speed: 15.0
182 | }
183 |
184 | result['T+h-s'] = {
185 | value: hour.value + result.h.value - result.s.value,
186 | speed: hour.speed + result.h.speed - result.s.speed
187 | }
188 |
189 | // It is convenient to calculate Schureman's P here since several node
190 | // factors need it, although it could be argued that these
191 | // (along with I, xi, nu etc) belong somewhere else.
192 | result.P = {
193 | value: result.p.value - (result.xi.value % 360.0),
194 | speed: null
195 | }
196 |
197 | return result
198 | }
199 |
200 | export default astro
201 | export { polynomial, derivativePolynomial, T, JD, _I, _xi, _nu, _nup, _nupp }
202 |
--------------------------------------------------------------------------------
/src/constituents/compound-constituent.js:
--------------------------------------------------------------------------------
1 | const compoundConstituentFactory = (name, members) => {
2 | const coefficients = []
3 | members.forEach(({ constituent, factor }) => {
4 | constituent.coefficients.forEach((coefficient, index) => {
5 | if (typeof coefficients[index] === 'undefined') {
6 | coefficients[index] = 0
7 | }
8 | coefficients[index] += coefficient * factor
9 | })
10 | })
11 |
12 | const compoundConstituent = {
13 | name,
14 | coefficients,
15 |
16 | speed: (astro) => {
17 | let speed = 0
18 | members.forEach(({ constituent, factor }) => {
19 | speed += constituent.speed(astro) * factor
20 | })
21 | return speed
22 | },
23 |
24 | value: (astro) => {
25 | let value = 0
26 | members.forEach(({ constituent, factor }) => {
27 | value += constituent.value(astro) * factor
28 | })
29 | return value
30 | },
31 |
32 | u: (astro) => {
33 | let u = 0
34 | members.forEach(({ constituent, factor }) => {
35 | u += constituent.u(astro) * factor
36 | })
37 | return u
38 | },
39 |
40 | f: (astro) => {
41 | const f = []
42 | members.forEach(({ constituent, factor }) => {
43 | f.push(Math.pow(constituent.f(astro), Math.abs(factor)))
44 | })
45 | return f.reduce((previous, value) => {
46 | return previous * value
47 | })
48 | }
49 | }
50 |
51 | return Object.freeze(compoundConstituent)
52 | }
53 |
54 | export default compoundConstituentFactory
55 |
--------------------------------------------------------------------------------
/src/constituents/constituent.js:
--------------------------------------------------------------------------------
1 | import nodeCorrections from '../node-corrections/index.js'
2 |
3 | /**
4 | * Computes the dot notation of two arrays
5 | * @param {*} a
6 | * @param {*} b
7 | */
8 | const dotArray = (a, b) => {
9 | const results = []
10 | a.forEach((value, index) => {
11 | results.push(value * b[index])
12 | })
13 | return results.reduce((total, value) => {
14 | return total + value
15 | })
16 | }
17 |
18 | const astronimicDoodsonNumber = (astro) => {
19 | return [
20 | astro['T+h-s'],
21 | astro.s,
22 | astro.h,
23 | astro.p,
24 | astro.N,
25 | astro.pp,
26 | astro['90']
27 | ]
28 | }
29 |
30 | const astronomicSpeed = (astro) => {
31 | const results = []
32 | astronimicDoodsonNumber(astro).forEach((number) => {
33 | results.push(number.speed)
34 | })
35 | return results
36 | }
37 |
38 | const astronomicValues = (astro) => {
39 | const results = []
40 | astronimicDoodsonNumber(astro).forEach((number) => {
41 | results.push(number.value)
42 | })
43 | return results
44 | }
45 |
46 | const constituentFactory = (name, coefficients, u, f) => {
47 | if (!coefficients) {
48 | throw new Error('Coefficient must be defined for a constituent')
49 | }
50 |
51 | const constituent = {
52 | name,
53 | coefficients,
54 |
55 | value: (astro) => {
56 | return dotArray(coefficients, astronomicValues(astro))
57 | },
58 |
59 | speed(astro) {
60 | return dotArray(coefficients, astronomicSpeed(astro))
61 | },
62 |
63 | u: typeof u !== 'undefined' ? u : nodeCorrections.uZero,
64 |
65 | f: typeof f !== 'undefined' ? f : nodeCorrections.fUnity
66 | }
67 |
68 | return Object.freeze(constituent)
69 | }
70 |
71 | export default constituentFactory
72 |
73 | export { astronimicDoodsonNumber, astronomicSpeed, astronomicValues }
74 |
--------------------------------------------------------------------------------
/src/constituents/index.js:
--------------------------------------------------------------------------------
1 | import constituent from './constituent.js'
2 | import compoundConstituent from './compound-constituent.js'
3 | import nc from '../node-corrections/index.js'
4 |
5 | const constituents = {}
6 | // Long Term
7 | constituents.Z0 = constituent('Z0', [0, 0, 0, 0, 0, 0, 0], nc.uZero, nc.fUnity)
8 | constituents.SA = constituent('Sa', [0, 0, 1, 0, 0, 0, 0], nc.uZero, nc.fUnity)
9 | constituents.SSA = constituent(
10 | 'Ssa',
11 | [0, 0, 2, 0, 0, 0, 0],
12 | nc.uZero,
13 | nc.fUnity
14 | )
15 | constituents.MM = constituent('MM', [0, 1, 0, -1, 0, 0, 0], nc.uZero, nc.fMm)
16 | constituents.MF = constituent('MF', [0, 2, 0, 0, 0, 0, 0], nc.uMf, nc.fMf)
17 | // Diurnals
18 | constituents.Q1 = constituent('Q1', [1, -2, 0, 1, 0, 0, 1], nc.uO1, nc.fO1)
19 | constituents.O1 = constituent('O1', [1, -1, 0, 0, 0, 0, 1], nc.uO1, nc.fO1)
20 | constituents.K1 = constituent('K1', [1, 1, 0, 0, 0, 0, -1], nc.uK1, nc.fK1)
21 | constituents.J1 = constituent('J1', [1, 2, 0, -1, 0, 0, -1], nc.uJ1, nc.fJ1)
22 | constituents.M1 = constituent('M1', [1, 0, 0, 0, 0, 0, 1], nc.uM1, nc.fM1)
23 | constituents.P1 = constituent('P1', [1, 1, -2, 0, 0, 0, 1], nc.uZero, nc.fUnity)
24 | constituents.S1 = constituent('S1', [1, 1, -1, 0, 0, 0, 0], nc.uZero, nc.fUnity)
25 | constituents.OO1 = constituent('OO1', [1, 3, 0, 0, 0, 0, -1], nc.uOO1, nc.fOO1)
26 | // Semi diurnals
27 | constituents['2N2'] = constituent('2N2', [2, -2, 0, 2, 0, 0, 0], nc.uM2, nc.fM2)
28 | constituents.N2 = constituent('N2', [2, -1, 0, 1, 0, 0, 0], nc.uM2, nc.fM2)
29 | constituents.NU2 = constituent('NU2', [2, -1, 2, -1, 0, 0, 0], nc.uM2, nc.fM2)
30 | constituents.M2 = constituent('M2', [2, 0, 0, 0, 0, 0, 0], nc.uM2, nc.fM2)
31 | constituents.LAM2 = constituent('LAM2', [2, 1, -2, 1, 0, 0, 2], nc.uM2, nc.fM2)
32 | constituents.L2 = constituent('L2', [2, 1, 0, -1, 0, 0, 2], nc.uL2, nc.fL2)
33 | constituents.T2 = constituent('T2', [2, 2, -3, 0, 0, 1, 0], nc.uZero, nc.fUnity)
34 | constituents.S2 = constituent('S2', [2, 2, -2, 0, 0, 0, 0], nc.uZero, nc.fUnity)
35 | constituents.R2 = constituent(
36 | 'R2',
37 | [2, 2, -1, 0, 0, -1, 2],
38 | nc.uZero,
39 | nc.fUnity
40 | )
41 | constituents.K2 = constituent('K2', [2, 2, 0, 0, 0, 0, 0], nc.uK2, nc.fK2)
42 | // Third diurnal
43 | constituents.M3 = constituent(
44 | 'M3',
45 | [3, 0, 0, 0, 0, 0, 0],
46 | (a) => {
47 | return nc.uModd(a, 3)
48 | },
49 | (a) => {
50 | return nc.fModd(a, 3)
51 | }
52 | )
53 | // Compound
54 | constituents.MSF = compoundConstituent('MSF', [
55 | { constituent: constituents.S2, factor: 1 },
56 | { constituent: constituents.M2, factor: -1 }
57 | ])
58 |
59 | // Diurnal
60 | constituents['2Q1'] = compoundConstituent('2Q1', [
61 | { constituent: constituents.N2, factor: 1 },
62 | { constituent: constituents.J1, factor: -1 }
63 | ])
64 | constituents.RHO = compoundConstituent('RHO', [
65 | { constituent: constituents.NU2, factor: 1 },
66 | { constituent: constituents.K1, factor: -1 }
67 | ])
68 |
69 | // Semi-Diurnal
70 |
71 | constituents.MU2 = compoundConstituent('MU2', [
72 | { constituent: constituents.M2, factor: 2 },
73 | { constituent: constituents.S2, factor: -1 }
74 | ])
75 | constituents['2SM2'] = compoundConstituent('2SM2', [
76 | { constituent: constituents.S2, factor: 2 },
77 | { constituent: constituents.M2, factor: -1 }
78 | ])
79 |
80 | // Third-Diurnal
81 | constituents['2MK3'] = compoundConstituent('2MK3', [
82 | { constituent: constituents.M2, factor: 1 },
83 | { constituent: constituents.O1, factor: 1 }
84 | ])
85 | constituents.MK3 = compoundConstituent('MK3', [
86 | { constituent: constituents.M2, factor: 1 },
87 | { constituent: constituents.K1, factor: 1 }
88 | ])
89 |
90 | // Quarter-Diurnal
91 | constituents.MN4 = compoundConstituent('MN4', [
92 | { constituent: constituents.M2, factor: 1 },
93 | { constituent: constituents.N2, factor: 1 }
94 | ])
95 | constituents.M4 = compoundConstituent('M4', [
96 | { constituent: constituents.M2, factor: 2 }
97 | ])
98 | constituents.MS4 = compoundConstituent('MS4', [
99 | { constituent: constituents.M2, factor: 1 },
100 | { constituent: constituents.S2, factor: 1 }
101 | ])
102 | constituents.S4 = compoundConstituent('S4', [
103 | { constituent: constituents.S2, factor: 2 }
104 | ])
105 |
106 | // Sixth-Diurnal
107 | constituents.M6 = compoundConstituent('M6', [
108 | { constituent: constituents.M2, factor: 3 }
109 | ])
110 | constituents.S6 = compoundConstituent('S6', [
111 | { constituent: constituents.S2, factor: 3 }
112 | ])
113 |
114 | // Eighth-Diurnals
115 | constituents.M8 = compoundConstituent('M8', [
116 | { constituent: constituents.M2, factor: 4 }
117 | ])
118 |
119 | export default constituents
120 |
--------------------------------------------------------------------------------
/src/harmonics/index.js:
--------------------------------------------------------------------------------
1 | import prediction from './prediction.js'
2 | import constituentModels from '../constituents/index.js'
3 | import { d2r } from '../astronomy/constants.js'
4 |
5 | const getDate = (time) => {
6 | if (time instanceof Date) {
7 | return time
8 | }
9 | if (typeof time === 'number') {
10 | return new Date(time * 1000)
11 | }
12 | throw new Error('Invalid date format, should be a Date object, or timestamp')
13 | }
14 |
15 | const getTimeline = (start, end, seconds) => {
16 | seconds = typeof seconds !== 'undefined' ? seconds : 10 * 60
17 | const items = []
18 | const endTime = end.getTime() / 1000
19 | let lastTime = start.getTime() / 1000
20 | const startTime = lastTime
21 | const hours = []
22 | while (lastTime <= endTime) {
23 | items.push(new Date(lastTime * 1000))
24 | hours.push((lastTime - startTime) / (60 * 60))
25 | lastTime += seconds
26 | }
27 |
28 | return {
29 | items,
30 | hours
31 | }
32 | }
33 |
34 | const harmonicsFactory = ({ harmonicConstituents, phaseKey, offset }) => {
35 | if (!Array.isArray(harmonicConstituents)) {
36 | throw new Error('Harmonic constituents are not an array')
37 | }
38 | const constituents = []
39 | harmonicConstituents.forEach((constituent) => {
40 | if (typeof constituent.name === 'undefined') {
41 | throw new Error('Harmonic constituents must have a name property')
42 | }
43 | if (typeof constituentModels[constituent.name] !== 'undefined') {
44 | constituent._model = constituentModels[constituent.name]
45 | constituent._phase = d2r * constituent[phaseKey]
46 | constituents.push(constituent)
47 | }
48 | })
49 |
50 | if (offset !== false) {
51 | constituents.push({
52 | name: 'Z0',
53 | _model: constituentModels.Z0,
54 | _phase: 0,
55 | amplitude: offset
56 | })
57 | }
58 |
59 | let start = new Date()
60 | let end = new Date()
61 |
62 | const harmonics = {}
63 |
64 | harmonics.setTimeSpan = (startTime, endTime) => {
65 | start = getDate(startTime)
66 | end = getDate(endTime)
67 | if (start.getTime() >= end.getTime()) {
68 | throw new Error('Start time must be before end time')
69 | }
70 | return harmonics
71 | }
72 |
73 | harmonics.prediction = (options) => {
74 | options =
75 | typeof options !== 'undefined' ? options : { timeFidelity: 10 * 60 }
76 | return prediction({
77 | timeline: getTimeline(start, end, options.timeFidelity),
78 | constituents,
79 | start
80 | })
81 | }
82 |
83 | return Object.freeze(harmonics)
84 | }
85 |
86 | export default harmonicsFactory
87 | export { getDate, getTimeline }
88 |
--------------------------------------------------------------------------------
/src/harmonics/prediction.js:
--------------------------------------------------------------------------------
1 | import astro from '../astronomy/index.js'
2 | import { d2r } from '../astronomy/constants.js'
3 |
4 | const modulus = (a, b) => {
5 | return ((a % b) + b) % b
6 | }
7 |
8 | const addExtremesOffsets = (extreme, offsets) => {
9 | if (typeof offsets === 'undefined' || !offsets) {
10 | return extreme
11 | }
12 | if (extreme.high && offsets.height_offset && offsets.height_offset.high) {
13 | extreme.level *= offsets.height_offset.high
14 | }
15 | if (extreme.low && offsets.height_offset && offsets.height_offset.low) {
16 | extreme.level *= offsets.height_offset.low
17 | }
18 | if (extreme.high && offsets.time_offset && offsets.time_offset.high) {
19 | extreme.time = new Date(
20 | extreme.time.getTime() + offsets.time_offset.high * 60 * 1000
21 | )
22 | }
23 | if (extreme.low && offsets.time_offset && offsets.time_offset.low) {
24 | extreme.time = new Date(
25 | extreme.time.getTime() + offsets.time_offset.low * 60 * 1000
26 | )
27 | }
28 | return extreme
29 | }
30 |
31 | const getExtremeLabel = (label, highLowLabels) => {
32 | if (
33 | typeof highLowLabels !== 'undefined' &&
34 | typeof highLowLabels[label] !== 'undefined'
35 | ) {
36 | return highLowLabels[label]
37 | }
38 | const labels = {
39 | high: 'High',
40 | low: 'Low'
41 | }
42 | return labels[label]
43 | }
44 |
45 | const predictionFactory = ({ timeline, constituents, start }) => {
46 | const getLevel = (hour, modelBaseSpeed, modelU, modelF, modelBaseValue) => {
47 | const amplitudes = []
48 | let result = 0
49 |
50 | constituents.forEach((constituent) => {
51 | const amplitude = constituent.amplitude
52 | const phase = constituent._phase
53 | const f = modelF[constituent.name]
54 | const speed = modelBaseSpeed[constituent.name]
55 | const u = modelU[constituent.name]
56 | const V0 = modelBaseValue[constituent.name]
57 | amplitudes.push(amplitude * f * Math.cos(speed * hour + (V0 + u) - phase))
58 | })
59 | // sum up each row
60 | amplitudes.forEach((item) => {
61 | result += item
62 | })
63 | return result
64 | }
65 |
66 | const prediction = {}
67 |
68 | prediction.getExtremesPrediction = (options) => {
69 | const { labels, offsets } = typeof options !== 'undefined' ? options : {}
70 | const results = []
71 | const { baseSpeed, u, f, baseValue } = prepare()
72 | let goingUp = false
73 | let goingDown = false
74 | let lastLevel = getLevel(0, baseSpeed, u[0], f[0], baseValue)
75 | timeline.items.forEach((time, index) => {
76 | const hour = timeline.hours[index]
77 | const level = getLevel(hour, baseSpeed, u[index], f[index], baseValue)
78 | // Compare this level to the last one, if we
79 | // are changing angle, then the last one was high or low
80 | if (level > lastLevel && goingDown) {
81 | results.push(
82 | addExtremesOffsets(
83 | {
84 | time: timeline.items[index - 1],
85 | level: lastLevel,
86 | high: false,
87 | low: true,
88 | label: getExtremeLabel('low', labels)
89 | },
90 | offsets
91 | )
92 | )
93 | }
94 | if (level < lastLevel && goingUp) {
95 | results.push(
96 | addExtremesOffsets(
97 | {
98 | time: timeline.items[index - 1],
99 | level: lastLevel,
100 | high: true,
101 | low: false,
102 | label: getExtremeLabel('high', labels)
103 | },
104 | offsets
105 | )
106 | )
107 | }
108 | if (level > lastLevel) {
109 | goingUp = true
110 | goingDown = false
111 | }
112 | if (level < lastLevel) {
113 | goingUp = false
114 | goingDown = true
115 | }
116 | lastLevel = level
117 | })
118 | return results
119 | }
120 |
121 | prediction.getTimelinePrediction = () => {
122 | const results = []
123 | const { baseSpeed, u, f, baseValue } = prepare()
124 | timeline.items.forEach((time, index) => {
125 | const hour = timeline.hours[index]
126 | const prediction = {
127 | time,
128 | hour,
129 | level: getLevel(hour, baseSpeed, u[index], f[index], baseValue)
130 | }
131 |
132 | results.push(prediction)
133 | })
134 | return results
135 | }
136 |
137 | const prepare = () => {
138 | const baseAstro = astro(start)
139 |
140 | const baseValue = {}
141 | const baseSpeed = {}
142 | const u = []
143 | const f = []
144 | constituents.forEach((constituent) => {
145 | const value = constituent._model.value(baseAstro)
146 | const speed = constituent._model.speed(baseAstro)
147 | baseValue[constituent.name] = d2r * value
148 | baseSpeed[constituent.name] = d2r * speed
149 | })
150 | timeline.items.forEach((time) => {
151 | const uItem = {}
152 | const fItem = {}
153 | const itemAstro = astro(time)
154 | constituents.forEach((constituent) => {
155 | const constituentU = modulus(constituent._model.u(itemAstro), 360)
156 |
157 | uItem[constituent.name] = d2r * constituentU
158 | fItem[constituent.name] = modulus(constituent._model.f(itemAstro), 360)
159 | })
160 | u.push(uItem)
161 | f.push(fItem)
162 | })
163 |
164 | return {
165 | baseValue,
166 | baseSpeed,
167 | u,
168 | f
169 | }
170 | }
171 |
172 | return Object.freeze(prediction)
173 | }
174 |
175 | export default predictionFactory
176 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import harmonics from './harmonics/index.js'
2 |
3 | const tidePredictionFactory = (constituents, options) => {
4 | const harmonicsOptions = {
5 | harmonicConstituents: constituents,
6 | phaseKey: 'phase_GMT',
7 | offset: false
8 | }
9 |
10 | if (typeof options !== 'undefined') {
11 | Object.keys(harmonicsOptions).forEach((key) => {
12 | if (typeof options[key] !== 'undefined') {
13 | harmonicsOptions[key] = options[key]
14 | }
15 | })
16 | }
17 |
18 | const tidePrediction = {
19 | getTimelinePrediction: ({ start, end }) => {
20 | return harmonics(harmonicsOptions)
21 | .setTimeSpan(start, end)
22 | .prediction()
23 | .getTimelinePrediction()
24 | },
25 |
26 | getExtremesPrediction: ({ start, end, labels, offsets, timeFidelity }) => {
27 | return harmonics(harmonicsOptions)
28 | .setTimeSpan(start, end)
29 | .prediction({ timeFidelity })
30 | .getExtremesPrediction(labels, offsets)
31 | },
32 |
33 | getWaterLevelAtTime: ({ time }) => {
34 | const endDate = new Date(time.getTime() + 10 * 60 * 1000)
35 | return harmonics(harmonicsOptions)
36 | .setTimeSpan(time, endDate)
37 | .prediction()
38 | .getTimelinePrediction()[0]
39 | }
40 | }
41 |
42 | return tidePrediction
43 | }
44 |
45 | export default tidePredictionFactory
46 |
--------------------------------------------------------------------------------
/src/node-corrections/index.js:
--------------------------------------------------------------------------------
1 | import { d2r, r2d } from '../astronomy/constants.js'
2 |
3 | const corrections = {
4 | fUnity() {
5 | return 1
6 | },
7 |
8 | // Schureman equations 73, 65
9 | fMm(a) {
10 | const omega = d2r * a.omega.value
11 | const i = d2r * a.i.value
12 | const I = d2r * a.I.value
13 | const mean =
14 | (2 / 3.0 - Math.pow(Math.sin(omega), 2)) *
15 | (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2))
16 | return (2 / 3.0 - Math.pow(Math.sin(I), 2)) / mean
17 | },
18 |
19 | // Schureman equations 74, 66
20 | fMf(a) {
21 | const omega = d2r * a.omega.value
22 | const i = d2r * a.i.value
23 | const I = d2r * a.I.value
24 | const mean = Math.pow(Math.sin(omega), 2) * Math.pow(Math.cos(0.5 * i), 4)
25 | return Math.pow(Math.sin(I), 2) / mean
26 | },
27 |
28 | // Schureman equations 75, 67
29 | fO1(a) {
30 | const omega = d2r * a.omega.value
31 | const i = d2r * a.i.value
32 | const I = d2r * a.I.value
33 | const mean =
34 | Math.sin(omega) *
35 | Math.pow(Math.cos(0.5 * omega), 2) *
36 | Math.pow(Math.cos(0.5 * i), 4)
37 | return (Math.sin(I) * Math.pow(Math.cos(0.5 * I), 2)) / mean
38 | },
39 |
40 | // Schureman equations 76, 68
41 | fJ1(a) {
42 | const omega = d2r * a.omega.value
43 | const i = d2r * a.i.value
44 | const I = d2r * a.I.value
45 | const mean =
46 | Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2))
47 | return Math.sin(2 * I) / mean
48 | },
49 |
50 | // Schureman equations 77, 69
51 | fOO1(a) {
52 | const omega = d2r * a.omega.value
53 | const i = d2r * a.i.value
54 | const I = d2r * a.I.value
55 | const mean =
56 | Math.sin(omega) *
57 | Math.pow(Math.sin(0.5 * omega), 2) *
58 | Math.pow(Math.cos(0.5 * i), 4)
59 | return (Math.sin(I) * Math.pow(Math.sin(0.5 * I), 2)) / mean
60 | },
61 |
62 | // Schureman equations 78, 70
63 | fM2(a) {
64 | const omega = d2r * a.omega.value
65 | const i = d2r * a.i.value
66 | const I = d2r * a.I.value
67 | const mean =
68 | Math.pow(Math.cos(0.5 * omega), 4) * Math.pow(Math.cos(0.5 * i), 4)
69 | return Math.pow(Math.cos(0.5 * I), 4) / mean
70 | },
71 |
72 | // Schureman equations 227, 226, 68
73 | // Should probably eventually include the derivations of the magic numbers (0.5023 etc).
74 | fK1(a) {
75 | const omega = d2r * a.omega.value
76 | const i = d2r * a.i.value
77 | const I = d2r * a.I.value
78 | const nu = d2r * a.nu.value
79 | const sin2IcosnuMean =
80 | Math.sin(2 * omega) * (1 - (3 / 2.0) * Math.pow(Math.sin(i), 2))
81 | const mean = 0.5023 * sin2IcosnuMean + 0.1681
82 | return (
83 | Math.pow(
84 | 0.2523 * Math.pow(Math.sin(2 * I), 2) +
85 | 0.1689 * Math.sin(2 * I) * Math.cos(nu) +
86 | 0.0283,
87 | 0.5
88 | ) / mean
89 | )
90 | },
91 |
92 | // Schureman equations 215, 213, 204
93 | // It can be (and has been) confirmed that the exponent for R_a reads 1/2 via Schureman Table 7
94 | fL2(a) {
95 | const P = d2r * a.P.value
96 | const I = d2r * a.I.value
97 | const rAInv = Math.pow(
98 | 1 -
99 | 12 * Math.pow(Math.tan(0.5 * I), 2) * Math.cos(2 * P) +
100 | 36 * Math.pow(Math.tan(0.5 * I), 4),
101 | 0.5
102 | )
103 | return corrections.fM2(a) * rAInv
104 | },
105 |
106 | // Schureman equations 235, 234, 71
107 | // Again, magic numbers
108 | fK2(a) {
109 | const omega = d2r * a.omega.value
110 | const i = d2r * a.i.value
111 | const I = d2r * a.I.value
112 | const nu = d2r * a.nu.value
113 | const sinsqIcos2nuMean =
114 | Math.sin(omega) ** 2 * (1 - (3 / 2.0) * Math.sin(i) ** 2)
115 | const mean = 0.5023 * sinsqIcos2nuMean + 0.0365
116 | return (
117 | Math.pow(
118 | 0.2523 * Math.pow(Math.sin(I), 4) +
119 | 0.0367 * Math.pow(Math.sin(I), 2) * Math.cos(2 * nu) +
120 | 0.0013,
121 | 0.5
122 | ) / mean
123 | )
124 | },
125 | // Schureman equations 206, 207, 195
126 | fM1(a) {
127 | const P = d2r * a.P.value
128 | const I = d2r * a.I.value
129 | const qAInv = Math.pow(
130 | 0.25 +
131 | 1.5 *
132 | Math.cos(I) *
133 | Math.cos(2 * P) *
134 | Math.pow(Math.cos(0.5 * I), -0.5) +
135 | 2.25 * Math.pow(Math.cos(I), 2) * Math.pow(Math.cos(0.5 * I), -4),
136 | 0.5
137 | )
138 | return corrections.fO1(a) * qAInv
139 | },
140 |
141 | // See e.g. Schureman equation 149
142 | fModd(a, n) {
143 | return Math.pow(corrections.fM2(a), n / 2.0)
144 | },
145 |
146 | // Node factors u, see Table 2 of Schureman.
147 |
148 | uZero() {
149 | return 0.0
150 | },
151 |
152 | uMf(a) {
153 | return -2.0 * a.xi.value
154 | },
155 |
156 | uO1(a) {
157 | return 2.0 * a.xi.value - a.nu.value
158 | },
159 |
160 | uJ1(a) {
161 | return -a.nu.value
162 | },
163 |
164 | uOO1(a) {
165 | return -2.0 * a.xi.value - a.nu.value
166 | },
167 |
168 | uM2(a) {
169 | return 2.0 * a.xi.value - 2.0 * a.nu.value
170 | },
171 |
172 | uK1(a) {
173 | return -a.nup.value
174 | },
175 |
176 | // Schureman 214
177 | uL2(a) {
178 | const I = d2r * a.I.value
179 | const P = d2r * a.P.value
180 | const R =
181 | r2d *
182 | Math.atan(
183 | Math.sin(2 * P) /
184 | ((1 / 6.0) * Math.pow(Math.tan(0.5 * I), -2) - Math.cos(2 * P))
185 | )
186 | return 2.0 * a.xi.value - 2.0 * a.nu.value - R
187 | },
188 |
189 | uK2(a) {
190 | return -2.0 * a.nupp.value
191 | },
192 |
193 | // Schureman 202
194 | uM1(a) {
195 | const I = d2r * a.I.value
196 | const P = d2r * a.P.value
197 | const Q =
198 | r2d *
199 | Math.atan(((5 * Math.cos(I) - 1) / (7 * Math.cos(I) + 1)) * Math.tan(P))
200 | return a.xi.value - a.nu.value + Q
201 | },
202 |
203 | uModd(a, n) {
204 | return (n / 2.0) * corrections.uM2(a)
205 | }
206 | }
207 |
208 | export default corrections
209 |
--------------------------------------------------------------------------------
/test/_mocks/constituents.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | number: 1,
4 | name: 'M2',
5 | description: 'Principal lunar semidiurnal constituent',
6 | amplitude: 1.61,
7 | phase_GMT: 181.3,
8 | phase_local: 309.4,
9 | speed: 28.984104
10 | },
11 | {
12 | number: 2,
13 | name: 'S2',
14 | description: 'Principal solar semidiurnal constituent',
15 | amplitude: 0.43,
16 | phase_GMT: 180.1,
17 | phase_local: 300.1,
18 | speed: 30.0
19 | },
20 | {
21 | number: 3,
22 | name: 'N2',
23 | description: 'Larger lunar elliptic semidiurnal constituent',
24 | amplitude: 0.37,
25 | phase_GMT: 155.0,
26 | phase_local: 287.4,
27 | speed: 28.43973
28 | },
29 | {
30 | number: 4,
31 | name: 'K1',
32 | description: 'Lunar diurnal constituent',
33 | amplitude: 1.2,
34 | phase_GMT: 219.6,
35 | phase_local: 99.2,
36 | speed: 15.041069
37 | },
38 | {
39 | number: 5,
40 | name: 'M4',
41 | description: 'Shallow water overtides of principal lunar constituent',
42 | amplitude: 0.0,
43 | phase_GMT: 272.7,
44 | phase_local: 168.9,
45 | speed: 57.96821
46 | },
47 | {
48 | number: 6,
49 | name: 'O1',
50 | description: 'Lunar diurnal constituent',
51 | amplitude: 0.75,
52 | phase_GMT: 203.5,
53 | phase_local: 91.9,
54 | speed: 13.943035
55 | },
56 | {
57 | number: 7,
58 | name: 'M6',
59 | description: 'Shallow water overtides of principal lunar constituent',
60 | amplitude: 0.0,
61 | phase_GMT: 0.0,
62 | phase_local: 0.0,
63 | speed: 86.95232
64 | },
65 | {
66 | number: 8,
67 | name: 'MK3',
68 | description: 'Shallow water terdiurnal',
69 | amplitude: 0.0,
70 | phase_GMT: 0.0,
71 | phase_local: 0.0,
72 | speed: 44.025173
73 | },
74 | {
75 | number: 9,
76 | name: 'S4',
77 | description: 'Shallow water overtides of principal solar constituent',
78 | amplitude: 0.0,
79 | phase_GMT: 343.5,
80 | phase_local: 223.5,
81 | speed: 60.0
82 | },
83 | {
84 | number: 10,
85 | name: 'MN4',
86 | description: 'Shallow water quarter diurnal constituent',
87 | amplitude: 0.0,
88 | phase_GMT: 233.4,
89 | phase_local: 134.0,
90 | speed: 57.423832
91 | },
92 | {
93 | number: 11,
94 | name: 'NU2',
95 | description: 'Larger lunar evectional constituent',
96 | amplitude: 0.07,
97 | phase_GMT: 160.9,
98 | phase_local: 292.8,
99 | speed: 28.512583
100 | },
101 | {
102 | number: 12,
103 | name: 'S6',
104 | description: 'Shallow water overtides of principal solar constituent',
105 | amplitude: 0.0,
106 | phase_GMT: 0.0,
107 | phase_local: 0.0,
108 | speed: 90.0
109 | },
110 | {
111 | number: 13,
112 | name: 'MU2',
113 | description: 'Variational constituent',
114 | amplitude: 0.04,
115 | phase_GMT: 114.7,
116 | phase_local: 250.9,
117 | speed: 27.968208
118 | },
119 | {
120 | number: 14,
121 | name: '2N2',
122 | description: 'Lunar elliptical semidiurnal second-order constituent',
123 | amplitude: 0.05,
124 | phase_GMT: 125.4,
125 | phase_local: 262.2,
126 | speed: 27.895355
127 | },
128 | {
129 | number: 15,
130 | name: 'OO1',
131 | description: 'Lunar diurnal',
132 | amplitude: 0.04,
133 | phase_GMT: 246.4,
134 | phase_local: 117.3,
135 | speed: 16.139101
136 | },
137 | {
138 | number: 16,
139 | name: 'LAM2',
140 | description: 'Smaller lunar evectional constituent',
141 | amplitude: 0.01,
142 | phase_GMT: 213.6,
143 | phase_local: 337.9,
144 | speed: 29.455626
145 | },
146 | {
147 | number: 17,
148 | name: 'S1',
149 | description: 'Solar diurnal constituent',
150 | amplitude: 0.03,
151 | phase_GMT: 317.1,
152 | phase_local: 197.1,
153 | speed: 15.0
154 | },
155 | {
156 | number: 18,
157 | name: 'M1',
158 | description: 'Smaller lunar elliptic diurnal constituent',
159 | amplitude: 0.04,
160 | phase_GMT: 224.9,
161 | phase_local: 108.9,
162 | speed: 14.496694
163 | },
164 | {
165 | number: 19,
166 | name: 'J1',
167 | description: 'Smaller lunar elliptic diurnal constituent',
168 | amplitude: 0.07,
169 | phase_GMT: 232.3,
170 | phase_local: 107.5,
171 | speed: 15.5854435
172 | },
173 | {
174 | number: 20,
175 | name: 'MM',
176 | description: 'Lunar monthly constituent',
177 | amplitude: 0.0,
178 | phase_GMT: 0.0,
179 | phase_local: 0.0,
180 | speed: 0.5443747
181 | },
182 | {
183 | number: 21,
184 | name: 'SSA',
185 | description: 'Solar semiannual constituent',
186 | amplitude: 0.07,
187 | phase_GMT: 264.6,
188 | phase_local: 263.9,
189 | speed: 0.0821373
190 | },
191 | {
192 | number: 22,
193 | name: 'SA',
194 | description: 'Solar annual constituent',
195 | amplitude: 0.2,
196 | phase_GMT: 198.5,
197 | phase_local: 198.1,
198 | speed: 0.0410686
199 | },
200 | {
201 | number: 23,
202 | name: 'MSF',
203 | description: 'Lunisolar synodic fortnightly constituent',
204 | amplitude: 0.0,
205 | phase_GMT: 0.0,
206 | phase_local: 0.0,
207 | speed: 1.0158958
208 | },
209 | {
210 | number: 24,
211 | name: 'MF',
212 | description: 'Lunisolar fortnightly constituent',
213 | amplitude: 0.04,
214 | phase_GMT: 138.1,
215 | phase_local: 129.3,
216 | speed: 1.0980331
217 | },
218 | {
219 | number: 25,
220 | name: 'RHO',
221 | description: 'Larger lunar evectional diurnal constituent',
222 | amplitude: 0.02,
223 | phase_GMT: 197.0,
224 | phase_local: 89.2,
225 | speed: 13.471515
226 | },
227 | {
228 | number: 26,
229 | name: 'Q1',
230 | description: 'Larger lunar elliptic diurnal constituent',
231 | amplitude: 0.13,
232 | phase_GMT: 194.8,
233 | phase_local: 87.5,
234 | speed: 13.398661
235 | },
236 | {
237 | number: 27,
238 | name: 'T2',
239 | description: 'Larger solar elliptic constituent',
240 | amplitude: 0.02,
241 | phase_GMT: 165.3,
242 | phase_local: 285.6,
243 | speed: 29.958933
244 | },
245 | {
246 | number: 28,
247 | name: 'R2',
248 | description: 'Smaller solar elliptic constituent',
249 | amplitude: 0.0,
250 | phase_GMT: 163.2,
251 | phase_local: 282.9,
252 | speed: 30.041067
253 | },
254 | {
255 | number: 29,
256 | name: '2Q1',
257 | description: 'Larger elliptic diurnal',
258 | amplitude: 0.02,
259 | phase_GMT: 191.7,
260 | phase_local: 88.8,
261 | speed: 12.854286
262 | },
263 | {
264 | number: 30,
265 | name: 'P1',
266 | description: 'Solar diurnal constituent',
267 | amplitude: 0.38,
268 | phase_GMT: 215.8,
269 | phase_local: 96.1,
270 | speed: 14.958931
271 | },
272 | {
273 | number: 31,
274 | name: '2SM2',
275 | description: 'Shallow water semidiurnal constituent',
276 | amplitude: 0.0,
277 | phase_GMT: 348.1,
278 | phase_local: 99.9,
279 | speed: 31.015896
280 | },
281 | {
282 | number: 32,
283 | name: 'M3',
284 | description: 'Lunar terdiurnal constituent',
285 | amplitude: 0.01,
286 | phase_GMT: 7.0,
287 | phase_local: 19.1,
288 | speed: 43.47616
289 | },
290 | {
291 | number: 33,
292 | name: 'L2',
293 | description: 'Smaller lunar elliptic semidiurnal constituent',
294 | amplitude: 0.04,
295 | phase_GMT: 213.4,
296 | phase_local: 337.1,
297 | speed: 29.528479
298 | },
299 | {
300 | number: 34,
301 | name: '2MK3',
302 | description: 'Shallow water terdiurnal constituent',
303 | amplitude: 0.0,
304 | phase_GMT: 126.8,
305 | phase_local: 143.4,
306 | speed: 42.92714
307 | },
308 | {
309 | number: 35,
310 | name: 'K2',
311 | description: 'Lunisolar semidiurnal constituent',
312 | amplitude: 0.12,
313 | phase_GMT: 170.6,
314 | phase_local: 289.9,
315 | speed: 30.082138
316 | },
317 | {
318 | number: 36,
319 | name: 'M8',
320 | description: 'Shallow water eighth diurnal constituent',
321 | amplitude: 0.0,
322 | phase_GMT: 0.0,
323 | phase_local: 0.0,
324 | speed: 115.93642
325 | },
326 | {
327 | number: 37,
328 | name: 'MS4',
329 | description: 'Shallow water quarter diurnal constituent',
330 | amplitude: 0.0,
331 | phase_GMT: 264.0,
332 | phase_local: 152.1,
333 | speed: 58.984104
334 | }
335 | ]
336 |
--------------------------------------------------------------------------------
/test/_mocks/secondary-station.js:
--------------------------------------------------------------------------------
1 | export default {
2 | reference_station: '',
3 | height_offset: {
4 | high: 1,
5 | low: 2
6 | },
7 | time_offset: {
8 | high: 1,
9 | low: 2
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/_mocks/water-levels.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neaps/tide-predictor/d4c87d91bb13dad56cd72a9f875ac50c37007dac/test/_mocks/water-levels.js
--------------------------------------------------------------------------------
/test/astronomy/coefficients.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import coefficients, {
3 | sexagesimalToDecimal
4 | } from '../../src/astronomy/coefficients.js'
5 |
6 | describe('astronomy coefficients', () => {
7 | it('converts a sexagesimal angle into decimal degrees', () => {
8 | assert.ok(sexagesimalToDecimal(10, 10, 10, 10, 10) === 10.169447225)
9 | assert.ok(sexagesimalToDecimal(10) === 10)
10 | })
11 |
12 | it('calculates terrestrial oliquity coefficients rewritten to T', () => {
13 | assert.ok(coefficients.terrestrialObliquity[1] === -0.013002583333333335)
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/test/astronomy/index.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import closeTo from '../lib/close-to.js'
3 | import astro, {
4 | polynomial,
5 | derivativePolynomial,
6 | JD,
7 | T,
8 | _I,
9 | _xi,
10 | _nu,
11 | _nup,
12 | _nupp
13 | } from '../../src/astronomy/index.js'
14 |
15 | const sampleTime = new Date()
16 | sampleTime.setFullYear(2019)
17 | sampleTime.setMonth(9)
18 | sampleTime.setDate(4)
19 | sampleTime.setHours(10)
20 | sampleTime.setMinutes(15)
21 | sampleTime.setSeconds(40)
22 | sampleTime.setMilliseconds(10)
23 |
24 | describe('astronomy', () => {
25 | it('complete astronomic calculation', () => {
26 | const result = astro(sampleTime)
27 | closeTo(result.s.value, 258.23871057233191, 4)
28 | closeTo(result.s.speed, 0.54901651929993922, 4)
29 |
30 | closeTo(result.pp.value, 283.27697979858613, 4)
31 | closeTo(result.pp.speed, 1.9612154426341654e-6, 4)
32 |
33 | closeTo(result.h.value, 192.82639897760328, 4)
34 | closeTo(result.h.speed, 0.041068640143510367, 4)
35 |
36 | closeTo(result.xi.value, 11.989946298635664, 4)
37 | assert.ok(result.xi.speed === null)
38 |
39 | closeTo(result.I.value, 22.811296275568843, 4)
40 | assert.ok(result.I.speed === null)
41 |
42 | closeTo(result.P.value, 155.24265065565865, 4)
43 | assert.ok(result.P.speed === null)
44 |
45 | closeTo(result.nupp.value, 8.8162480626605451, 4)
46 | assert.ok(result.nupp.speed === null)
47 |
48 | closeTo(result.nu.value, 13.028571777192044, 4)
49 | assert.ok(result.nu.speed === null)
50 |
51 | closeTo(result['T+h-s'].value, 268.50435506200392, 4)
52 | closeTo(result['T+h-s'].speed, 14.492052120843571, 4)
53 |
54 | closeTo(result.omega.value, 23.436722306067253, 4)
55 | closeTo(result.omega.speed, -1.4832917321024327e-8, 4)
56 | })
57 |
58 | it('evaluates a polynomial', () => {
59 | assert.ok(polynomial([1, 2, 3], 3) === 34)
60 | })
61 |
62 | it('evaluates derivative polynomials', () => {
63 | assert.ok(derivativePolynomial([1, 2, 3], 3) === 20)
64 | })
65 |
66 | it('evaluates Meeus formula 7.1 (JD) correctly', () => {
67 | sampleTime.setMonth(9)
68 | closeTo(JD(sampleTime), 2458760.92755, 2)
69 | // Months of less than 2 go back a year
70 | sampleTime.setMonth(0)
71 | closeTo(JD(sampleTime), 2458487.92755, 2)
72 | })
73 |
74 | it('evaluates Meeus formula 11.1 (T) correctly', () => {
75 | sampleTime.setMonth(9)
76 | closeTo(T(sampleTime), 0.19756132, 2)
77 | })
78 |
79 | it('evaluates value for _I correctly', () => {
80 | closeTo(_I(4, 10, 5), 14.9918364991, 4)
81 | })
82 |
83 | it('evaluates value for _xi correctly', () => {
84 | closeTo(_xi(4, 3, 10), 0.911946348144, 4)
85 | })
86 |
87 | it('evaluates value for _nu correctly', () => {
88 | closeTo(_nu(10, 4, 5), 4.45767377718, 4)
89 | })
90 |
91 | it('evaluates value for _nup correctly', () => {
92 | closeTo(_nup(10, 4, 5), 2.13580480226, 4)
93 | })
94 |
95 | it('evaluates value for _nupp correctly', () => {
96 | closeTo(_nupp(10, 4, 5), 1.1146589591, 4)
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/test/constituents/compound-constituent.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import closeTo from '../lib/close-to.js'
3 | import compoundConstituent from '../../src/constituents/compound-constituent.js'
4 | import Constituent from '../../src/constituents/constituent.js'
5 | import astro from '../../src/astronomy/index.js'
6 |
7 | const sampleTime = new Date()
8 | sampleTime.setFullYear(2019)
9 | sampleTime.setMonth(9)
10 | sampleTime.setDate(4)
11 | sampleTime.setHours(10)
12 | sampleTime.setMinutes(15)
13 | sampleTime.setSeconds(40)
14 | sampleTime.setMilliseconds(10)
15 |
16 | const testAstro = astro(sampleTime)
17 |
18 | // This is a made-up doodson number for a test coefficient
19 | const testConstituentA = Constituent('testa', [1, 1, -1, 0, 0, 0, 1])
20 | const testConstituentB = Constituent('testb', [0, 1, -1, 0, 0, 0, 1])
21 |
22 | const compoundTest = compoundConstituent('test compound', [
23 | { constituent: testConstituentA, factor: 1 },
24 | { constituent: testConstituentB, factor: -1 }
25 | ])
26 | describe('compund constituent', () => {
27 | it('it calculates compound coefficients', () => {
28 | assert.deepStrictEqual(compoundTest.coefficients, [1, 0, 0, 0, 0, 0, 0])
29 | })
30 |
31 | it('it calculates speed', () => {
32 | closeTo(compoundTest.speed(testAstro), 14.4920521208, 4)
33 | })
34 |
35 | it('it calculates value', () => {
36 | closeTo(compoundTest.value(testAstro), 268.504355062, 4)
37 | })
38 |
39 | it('it returns u correctly', () => {
40 | assert.ok(compoundTest.u(testAstro) === 0)
41 | })
42 |
43 | it('it returns f correctly', () => {
44 | assert.ok(compoundTest.f(testAstro) === 1)
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/test/constituents/constituent.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import closeTo from '../lib/close-to.js'
3 | import constituent, {
4 | astronimicDoodsonNumber,
5 | astronomicSpeed,
6 | astronomicValues
7 | } from '../../src/constituents/constituent.js'
8 | import astro from '../../src/astronomy/index.js'
9 |
10 | const sampleTime = new Date()
11 | sampleTime.setFullYear(2019)
12 | sampleTime.setMonth(9)
13 | sampleTime.setDate(4)
14 | sampleTime.setHours(10)
15 | sampleTime.setMinutes(15)
16 | sampleTime.setSeconds(40)
17 | sampleTime.setMilliseconds(10)
18 |
19 | const testAstro = astro(sampleTime)
20 |
21 | // This is a made-up doodson number for a test coefficient
22 | const testConstituent = constituent('test', [1, 1, -1, 0, 0, 0, 1])
23 |
24 | describe('constituent', () => {
25 | it('it throws error if missing coefficients', () => {
26 | let errorMessage = false
27 | try {
28 | const a = constituent('fail') // eslint-disable-line
29 | } catch (error) {
30 | errorMessage = error
31 | }
32 | assert.ok(
33 | errorMessage.message === 'Coefficient must be defined for a constituent'
34 | )
35 | })
36 |
37 | it('it fetches astronimic Doodson Number values', () => {
38 | const values = astronimicDoodsonNumber(testAstro)
39 | assert.ok(values[0].value === testAstro['T+h-s'].value)
40 | })
41 |
42 | it('it fetches astronimic speed', () => {
43 | const values = astronomicSpeed(testAstro)
44 | assert.ok(values[0] === testAstro['T+h-s'].speed)
45 | })
46 |
47 | it('it fetches astronimic values', () => {
48 | const values = astronomicValues(testAstro)
49 | assert.ok(values[0] === testAstro['T+h-s'].value)
50 | })
51 |
52 | it('it computes constituent value', () => {
53 | closeTo(testConstituent.value(testAstro), 423.916666657, 4)
54 | })
55 |
56 | it('it computes constituent speed', () => {
57 | assert.ok(testConstituent.speed(testAstro) === 15)
58 | })
59 |
60 | it('it returns u correctly', () => {
61 | assert.ok(testConstituent.u(testAstro) === 0)
62 | })
63 |
64 | it('it returns f correctly', () => {
65 | assert.ok(testConstituent.f(testAstro) === 1)
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/test/constituents/index.js:
--------------------------------------------------------------------------------
1 | import closeTo from '../lib/close-to.js'
2 | import constituents from '../../src/constituents/index.js'
3 | import astro from '../../src/astronomy/index.js'
4 |
5 | const sampleTime = new Date()
6 | sampleTime.setFullYear(2019)
7 | sampleTime.setMonth(9)
8 | sampleTime.setDate(4)
9 | sampleTime.setHours(10)
10 | sampleTime.setMinutes(15)
11 | sampleTime.setSeconds(40)
12 | sampleTime.setMilliseconds(10)
13 |
14 | const testAstro = astro(sampleTime)
15 |
16 | describe('Base constituent definitions', () => {
17 | it('it prepared constituent SA', () => {
18 | closeTo(constituents.SA.value(testAstro), 192.826398978, 4)
19 | })
20 |
21 | it('it prepared constituent SSA', () => {
22 | closeTo(constituents.SSA.value(testAstro), 385.652797955, 4)
23 | })
24 |
25 | it('it prepared constituent M2', () => {
26 | closeTo(constituents.M2.value(testAstro), 537.008710124, 4)
27 | closeTo(constituents.M2.u(testAstro), -2.07725095711, 4)
28 | closeTo(constituents.M2.f(testAstro), 1.00853563237, 4)
29 | })
30 |
31 | it('has a correct lambda for M3', () => {
32 | closeTo(constituents.M3.u(testAstro), -3.11587643567, 4)
33 | closeTo(constituents.M3.f(testAstro), 1.01283073119, 4)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/test/harmonics/index.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import harmonics, { getDate, getTimeline } from '../../src/harmonics/index.js'
3 | import mockHarmonicConstituents from '../_mocks/constituents.js'
4 |
5 | const startDate = new Date(1567346400 * 1000) // 2019-09-01
6 | const endDate = new Date(1569966078 * 1000) // 2019-10-01
7 |
8 | describe('harmonics', () => {
9 | it('it checks constituents', () => {
10 | let errorMessage = false
11 |
12 | try {
13 | harmonics({ harmonicConstituents: 'not array' })
14 | } catch (error) {
15 | errorMessage = error
16 | }
17 | assert.ok(errorMessage.message === 'Harmonic constituents are not an array')
18 |
19 | errorMessage = false
20 |
21 | try {
22 | harmonics({
23 | harmonicConstituents: [
24 | {
25 | name: 'M2',
26 | description: 'Principal lunar semidiurnal constituent',
27 | amplitude: 1.61,
28 | phase_GMT: 181.3,
29 | phase_local: 309.4,
30 | speed: 28.984104
31 | },
32 | {
33 | description: 'Principal solar semidiurnal constituent',
34 | amplitude: 0.43,
35 | phase_GMT: 180.1,
36 | phase_local: 309.4
37 | }
38 | ]
39 | })
40 | } catch (error) {
41 | errorMessage = error
42 | }
43 | assert.ok(
44 | errorMessage.message === 'Harmonic constituents must have a name property'
45 | )
46 |
47 | errorMessage = false
48 |
49 | try {
50 | harmonics({
51 | harmonicConstituents: [
52 | {
53 | name: 'not a name',
54 | description: 'Principal lunar semidiurnal constituent',
55 | amplitude: 1.61,
56 | phase_GMT: 181.3,
57 | phase_local: 309.4,
58 | speed: 28.984104
59 | },
60 | {
61 | name: 'M2',
62 | description: 'Principal solar semidiurnal constituent',
63 | amplitude: 0.43,
64 | phase_GMT: 180.1,
65 | phase_local: 309.4
66 | }
67 | ]
68 | })
69 | } catch (error) {
70 | errorMessage = error
71 | }
72 | assert.ok(!errorMessage.message)
73 | })
74 |
75 | it('it checks start and end times', () => {
76 | const testHarmonics = harmonics({
77 | harmonicConstituents: mockHarmonicConstituents
78 | })
79 | let timeErrorMessage = false
80 | try {
81 | testHarmonics.setTimeSpan('lkjsdlf', 'sdfklj')
82 | } catch (error) {
83 | timeErrorMessage = error
84 | }
85 | assert.ok(
86 | timeErrorMessage.message ===
87 | 'Invalid date format, should be a Date object, or timestamp'
88 | )
89 |
90 | timeErrorMessage = false
91 | try {
92 | testHarmonics.setTimeSpan(startDate, startDate)
93 | } catch (error) {
94 | timeErrorMessage = error
95 | }
96 | assert.ok(timeErrorMessage.message === 'Start time must be before end time')
97 |
98 | timeErrorMessage = false
99 | try {
100 | testHarmonics.setTimeSpan(startDate, endDate)
101 | } catch (error) {
102 | timeErrorMessage = error
103 | }
104 | assert.ok(!timeErrorMessage.message)
105 | })
106 |
107 | it('it parses dates correctly', () => {
108 | const parsedDate = getDate(startDate)
109 | assert.ok(parsedDate.getTime() === startDate.getTime())
110 |
111 | const parsedUnixDate = getDate(startDate.getTime() / 1000)
112 | assert.ok(parsedUnixDate.getTime() === startDate.getTime())
113 | })
114 |
115 | it('it creates timeline correctly', () => {
116 | const seconds = 20 * 60
117 | const difference =
118 | Math.round(
119 | (endDate.getTime() / 1000 - startDate.getTime() / 1000) / seconds
120 | ) + 1
121 | const { items, hours } = getTimeline(startDate, endDate, seconds)
122 | assert.ok(items.length === difference)
123 | assert.ok(hours.length === difference)
124 | })
125 | })
126 |
--------------------------------------------------------------------------------
/test/harmonics/prediction.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import closeTo from '../lib/close-to.js'
3 | import harmonics from '../../src/harmonics/index.js'
4 | import mockHarmonicConstituents from '../_mocks/constituents.js'
5 | import mockSecondaryStation from '../_mocks/secondary-station.js'
6 |
7 | const startDate = new Date()
8 | startDate.setFullYear(2019)
9 | startDate.setMonth(8)
10 | startDate.setDate(1)
11 | startDate.setHours(0)
12 | startDate.setMinutes(0)
13 | startDate.setSeconds(0)
14 | startDate.setMilliseconds(0)
15 |
16 | const endDate = new Date()
17 | endDate.setFullYear(2019)
18 | endDate.setMonth(8)
19 | endDate.setDate(1)
20 | endDate.setHours(6)
21 | endDate.setMinutes(0)
22 | endDate.setSeconds(0)
23 | endDate.setMilliseconds(0)
24 |
25 | const extremesEndDate = new Date()
26 | extremesEndDate.setFullYear(2019)
27 | extremesEndDate.setMonth(8)
28 | extremesEndDate.setDate(3)
29 | extremesEndDate.setHours(0)
30 | extremesEndDate.setMinutes(0)
31 | extremesEndDate.setSeconds(0)
32 | extremesEndDate.setMilliseconds(0)
33 |
34 | const setUpPrediction = () => {
35 | const harmonic = harmonics({
36 | harmonicConstituents: mockHarmonicConstituents,
37 | phaseKey: 'phase_GMT',
38 | offset: false
39 | })
40 | harmonic.setTimeSpan(startDate, endDate)
41 | return harmonic.prediction()
42 | }
43 |
44 | describe('harmonic prediction', () => {
45 | it('it creates a timeline prediction', () => {
46 | const testPrediction = setUpPrediction()
47 | const results = testPrediction.getTimelinePrediction()
48 | const lastResult = results.pop()
49 | closeTo(results[0].level, -1.347125, 3)
50 | closeTo(lastResult.level, 2.85263589, 3)
51 | })
52 |
53 | it('it creates a timeline prediction with a non-default phase key', () => {
54 | const results = harmonics({
55 | harmonicConstituents: mockHarmonicConstituents,
56 | phaseKey: 'phase_local',
57 | offset: false
58 | })
59 | .setTimeSpan(startDate, endDate)
60 | .prediction()
61 | .getTimelinePrediction()
62 | closeTo(results[0].level, 2.7560979, 3)
63 | closeTo(results.pop().level, -2.9170977, 3)
64 | })
65 |
66 | it('it finds high and low tides', () => {
67 | const results = harmonics({
68 | harmonicConstituents: mockHarmonicConstituents,
69 | phaseKey: 'phase_GMT',
70 | offset: false
71 | })
72 | .setTimeSpan(startDate, extremesEndDate)
73 | .prediction()
74 | .getExtremesPrediction()
75 | closeTo(results[0].level, -1.5650332, 4)
76 |
77 | const customLabels = {
78 | high: 'Super high',
79 | low: 'Wayyy low'
80 | }
81 |
82 | const labelResults = harmonics({
83 | harmonicConstituents: mockHarmonicConstituents,
84 | phaseKey: 'phase_GMT',
85 | offset: false
86 | })
87 | .setTimeSpan(startDate, extremesEndDate)
88 | .prediction()
89 | .getExtremesPrediction({ labels: customLabels })
90 | assert.ok(labelResults[0].label === customLabels.low)
91 | })
92 |
93 | it('it finds high and low tides with high fidelity', () => {
94 | const results = harmonics({
95 | harmonicConstituents: mockHarmonicConstituents,
96 | phaseKey: 'phase_GMT',
97 | offset: false
98 | })
99 | .setTimeSpan(startDate, extremesEndDate)
100 | .prediction({ timeFidelity: 60 })
101 | .getExtremesPrediction()
102 | closeTo(results[0].level, -1.5653894, 4)
103 | })
104 | })
105 |
106 | describe('Secondary stations', () => {
107 | it('it can add offsets to secondary stations', () => {
108 | const regularResults = harmonics({
109 | harmonicConstituents: mockHarmonicConstituents,
110 | phaseKey: 'phase_GMT',
111 | offset: false
112 | })
113 | .setTimeSpan(startDate, extremesEndDate)
114 | .prediction()
115 | .getExtremesPrediction()
116 |
117 | const offsetResults = harmonics({
118 | harmonicConstituents: mockHarmonicConstituents,
119 | phaseKey: 'phase_GMT',
120 | offset: false
121 | })
122 | .setTimeSpan(startDate, extremesEndDate)
123 | .prediction()
124 | .getExtremesPrediction({ offsets: mockSecondaryStation })
125 |
126 | offsetResults.forEach((offsetResult, index) => {
127 | if (offsetResult.low) {
128 | closeTo(
129 | offsetResult.level,
130 | regularResults[index].level * mockSecondaryStation.height_offset.low,
131 | 4
132 | )
133 | assert.ok(
134 | offsetResult.time.getTime() ===
135 | regularResults[index].time.getTime() +
136 | mockSecondaryStation.time_offset.low * 60 * 1000
137 | )
138 | }
139 | if (offsetResult.high) {
140 | closeTo(
141 | offsetResult.level,
142 | regularResults[index].level * mockSecondaryStation.height_offset.high,
143 | 4
144 | )
145 |
146 | assert.ok(
147 | offsetResult.time.getTime() ===
148 | regularResults[index].time.getTime() +
149 | mockSecondaryStation.time_offset.high * 60 * 1000
150 | )
151 | }
152 | })
153 | })
154 | })
155 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import closeTo from './lib/close-to.js'
3 | import mockConstituents from './_mocks/constituents.js'
4 | import tidePrediction from '../src/index.js'
5 |
6 | const startDate = new Date()
7 | startDate.setFullYear(2019)
8 | startDate.setMonth(8)
9 | startDate.setDate(1)
10 | startDate.setHours(0)
11 | startDate.setMinutes(0)
12 | startDate.setSeconds(0)
13 | startDate.setMilliseconds(0)
14 |
15 | const endDate = new Date()
16 | endDate.setFullYear(2019)
17 | endDate.setMonth(8)
18 | endDate.setDate(1)
19 | endDate.setHours(6)
20 | endDate.setMinutes(0)
21 | endDate.setSeconds(0)
22 | endDate.setMilliseconds(0)
23 |
24 | describe('Tidal station', () => {
25 | it('it is created correctly', () => {
26 | let stationCreated = true
27 |
28 | try {
29 | tidePrediction(mockConstituents)
30 | } catch (e) {
31 | stationCreated = false
32 | }
33 | assert.ok(stationCreated)
34 |
35 | try {
36 | tidePrediction(mockConstituents)
37 | } catch (e) {
38 | stationCreated = false
39 | }
40 | assert.ok(stationCreated)
41 | })
42 |
43 | it('it predicts the tides in a timeline', () => {
44 | const results = tidePrediction(mockConstituents).getTimelinePrediction({
45 | start: startDate,
46 | end: endDate
47 | })
48 | closeTo(results[0].level, -1.34712509, 3)
49 | closeTo(results.pop().level, 2.85263589, 3)
50 | })
51 |
52 | it('it predicts the tidal extremes', () => {
53 | const results = tidePrediction(mockConstituents).getExtremesPrediction({
54 | start: startDate,
55 | end: endDate
56 | })
57 | closeTo(results[0].level, -1.565033, 4)
58 | })
59 |
60 | it('it predicts the tidal extremes with high fidelity', () => {
61 | const results = tidePrediction(mockConstituents).getExtremesPrediction({
62 | start: startDate,
63 | end: endDate,
64 | timeFidelity: 60
65 | })
66 | closeTo(results[0].level, -1.565389, 4)
67 | })
68 |
69 | it('it fetches a single water level', () => {
70 | const result = tidePrediction(mockConstituents).getWaterLevelAtTime({
71 | time: startDate
72 | })
73 | closeTo(result.level, -1.34712509, 4)
74 | })
75 |
76 | it('it adds offset phases', () => {
77 | const results = tidePrediction(mockConstituents, {
78 | offset: 3
79 | }).getExtremesPrediction({ start: startDate, end: endDate })
80 |
81 | closeTo(results[0].level, 1.43496678, 4)
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/test/lib/close-to.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 |
3 | export default (received, expected, precision) => {
4 | const expectedDiff = Math.pow(10, -precision) / 2
5 | const receivedDiff = Math.abs(expected - received)
6 | assert.ok(receivedDiff < expectedDiff)
7 | }
8 |
--------------------------------------------------------------------------------
/test/noaa.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import fs from 'fs'
3 | import https from 'https'
4 | import tidePrediction from '../src/index.js'
5 |
6 | // Create a directory for test cache
7 | if (!fs.existsSync('./.test-cache')) {
8 | fs.mkdirSync('./.test-cache')
9 | }
10 |
11 | const stations = ['9413450', '9411340', '2695535', '8761724', '8410140']
12 |
13 | const makeRequest = (url) =>
14 | new Promise((resolve, reject) => {
15 | https.get(url, (response) => {
16 | const data = []
17 |
18 | response.on('data', (fragment) => {
19 | data.push(fragment)
20 | })
21 |
22 | response.on('end', () => {
23 | const body = Buffer.concat(data)
24 | resolve(JSON.parse(body.toString()))
25 | })
26 |
27 | response.on('error', (error) => {
28 | reject(error)
29 | })
30 | })
31 | })
32 |
33 | const getStation = (station, callback) => {
34 | const filePath = `./.test-cache/${station}.json`
35 | if (fs.existsSync(filePath)) {
36 | fs.readFile(filePath, (err, data) => {
37 | if (err) {
38 | throw new Error('Cannot access test cache')
39 | }
40 | callback(JSON.parse(data))
41 | })
42 | return
43 | }
44 | const stationData = {}
45 | const tasks = []
46 |
47 | tasks.push(
48 | makeRequest(
49 | `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations/${station}/harcon.json?units=metric`
50 | ).then((data) => {
51 | stationData.harmonics = data
52 | })
53 | )
54 |
55 | tasks.push(
56 | makeRequest(
57 | `https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=recent&station=${station}&product=predictions&datum=MTL&time_zone=gmt&units=metric&format=json`
58 | ).then((data) => {
59 | stationData.levels = data
60 | })
61 | )
62 |
63 | tasks.push(
64 | makeRequest(
65 | `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations/${station}/datums.json?units=metric`
66 | ).then((data) => {
67 | stationData.info = data
68 | })
69 | )
70 |
71 | Promise.all(tasks).then(() => {
72 | fs.writeFile(filePath, JSON.stringify(stationData), (error) => {
73 | if (error) {
74 | throw new Error('Cannot write to test cache')
75 | }
76 | callback(stationData)
77 | })
78 | })
79 | }
80 |
81 | describe('Results compare to NOAA', () => {
82 | stations.forEach((station) => {
83 | it(`it compares with station ${station}`, (done) => {
84 | getStation(station, ({ harmonics, levels, info }) => {
85 | let mtl = 0
86 | let mllw = 0
87 | info.datums.forEach((datum) => {
88 | if (datum.name === 'MTL') {
89 | mtl = datum.value
90 | }
91 | if (datum.name === 'MLLW') {
92 | mllw = datum.value
93 | }
94 | })
95 | const tideStation = tidePrediction(
96 | harmonics.HarmonicConstituents,
97 | mtl - mllw
98 | )
99 | levels.predictions.forEach((prediction) => {
100 | const neapsPrediction = tideStation.getWaterLevelAtTime({
101 | time: new Date(prediction.t)
102 | })
103 | assert.ok(parseFloat(prediction.v) >= neapsPrediction.level - 0.5)
104 | assert.ok(parseFloat(prediction.v) <= neapsPrediction.level + 0.5)
105 | })
106 | done()
107 | })
108 | }).timeout(20000)
109 | })
110 | })
111 |
--------------------------------------------------------------------------------
/test/node-corrections/index.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import closeTo from '../lib/close-to.js'
3 | import nodeCorrections from '../../src/node-corrections/index.js'
4 |
5 | const testItems = {
6 | i: {
7 | value: 5
8 | },
9 | I: { value: 6 },
10 | omega: { value: 3 },
11 | nu: { value: 4 },
12 | nup: { value: 4 },
13 | nupp: { value: 2 },
14 | P: { value: 14 },
15 | xi: {
16 | value: 4
17 | }
18 | }
19 |
20 | describe('Node corrections', () => {
21 | it('have correct unity', () => {
22 | assert.ok(nodeCorrections.fUnity() === 1)
23 | })
24 |
25 | it('calculates Schureman equations 73, 65 (f_Mm)', () => {
26 | closeTo(nodeCorrections.fMm(testItems), 0.999051998091, 4)
27 | })
28 |
29 | it('calculates Schureman equations 74, 66 (f_Mf)', () => {
30 | closeTo(nodeCorrections.fMf(testItems), 4.00426673883, 4)
31 | })
32 |
33 | it('calculates Schureman equations 75, 67 (f_O1)', () => {
34 | closeTo(nodeCorrections.fO1(testItems), 2.00076050158, 4)
35 | })
36 |
37 | it('calculates Schureman equations 76, 68 (f_J1)', () => {
38 | closeTo(nodeCorrections.fJ1(testItems), 2.0119685329, 4)
39 | })
40 |
41 | it('calculates Schureman equations 77, 69 (f_OO1)', () => {
42 | closeTo(nodeCorrections.fOO1(testItems), 8.01402871709, 4)
43 | })
44 |
45 | it('calculates Schureman equations 78, 70 (f_M2)', () => {
46 | closeTo(nodeCorrections.fM2(testItems), 0.999694287563, 4)
47 | })
48 |
49 | it('calculates Schureman equations 227, 226, 68 (f_K1)', () => {
50 | closeTo(nodeCorrections.fK1(testItems), 1.23843964182, 4)
51 | })
52 |
53 | it('calculates Schureman equations 215, 213, 204 (f_L2)', () => {
54 | closeTo(nodeCorrections.fL2(testItems), 0.98517860327, 4)
55 | })
56 |
57 | it('calculates Schureman equations 235, 234, 71 (f_K2)', () => {
58 | closeTo(nodeCorrections.fK2(testItems), 1.09775430048, 4)
59 | })
60 |
61 | it('calculates Schureman equations 206, 207, 195 (f_M1)', () => {
62 | closeTo(nodeCorrections.fM1(testItems), 3.90313810168, 4)
63 | })
64 |
65 | it('calculates e.g. Schureman equation 149 (f_Modd)', () => {
66 | closeTo(nodeCorrections.fModd(testItems, 3), 0.999541466395, 4)
67 | })
68 |
69 | it('has a zero for u_zero', () => {
70 | assert.ok(nodeCorrections.uZero() === 0.0)
71 | })
72 |
73 | it('calculates u_Mf', () => {
74 | assert.ok(nodeCorrections.uMf(testItems) === -8.0)
75 | })
76 |
77 | it('calculates u_O1', () => {
78 | assert.ok(nodeCorrections.uO1(testItems) === 4.0)
79 | })
80 |
81 | it('calculates u_J1', () => {
82 | assert.ok(nodeCorrections.uJ1(testItems) === -4)
83 | })
84 |
85 | it('calculates u_OO1', () => {
86 | assert.ok(nodeCorrections.uOO1(testItems) === -12.0)
87 | })
88 |
89 | it('calculates u_M2', () => {
90 | assert.ok(nodeCorrections.uM2(testItems) === 0.0)
91 | })
92 |
93 | it('calculates u_K1', () => {
94 | assert.ok(nodeCorrections.uK1(testItems) === -4)
95 | })
96 |
97 | it('calculates u_L2', () => {
98 | closeTo(nodeCorrections.uL2(testItems), -0.449812364499, 4)
99 | })
100 |
101 | it('calculates u_K2', () => {
102 | assert.ok(nodeCorrections.uK2(testItems) === -4.0)
103 | })
104 |
105 | it('calculates u_K2', () => {
106 | assert.ok(nodeCorrections.uK2(testItems) === -4.0)
107 | })
108 |
109 | it('calculates u_M1', () => {
110 | closeTo(nodeCorrections.uM1(testItems), 7.09154172301, 4)
111 | })
112 |
113 | it('calculates u_Modd', () => {
114 | assert.ok(nodeCorrections.uModd(testItems, 3) === 0)
115 | })
116 | })
117 |
--------------------------------------------------------------------------------