@neaps/tide-predictor 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.eslintrc.js +22 -0
  2. package/.github/workflows/test.yml +15 -0
  3. package/.prettierrc +4 -0
  4. package/Gruntfile.js +87 -0
  5. package/LICENSE +21 -0
  6. package/README.md +199 -0
  7. package/babel.config.js +9 -0
  8. package/dist/tide-predictor.js +1013 -0
  9. package/examples/browser/index.html +51 -0
  10. package/jest.config.js +14 -0
  11. package/lib/astronomy/coefficients.js +31 -0
  12. package/lib/astronomy/constants.js +10 -0
  13. package/lib/astronomy/index.js +199 -0
  14. package/lib/constituents/compound-constituent.js +67 -0
  15. package/lib/constituents/constituent.js +74 -0
  16. package/lib/constituents/index.js +140 -0
  17. package/lib/harmonics/index.js +113 -0
  18. package/lib/harmonics/prediction.js +195 -0
  19. package/lib/index.es6.js +1005 -0
  20. package/lib/index.js +53 -0
  21. package/lib/node-corrections/index.js +147 -0
  22. package/package.json +45 -0
  23. package/rollup.config.js +21 -0
  24. package/src/__mocks__/constituents.js +335 -0
  25. package/src/__mocks__/secondary-station.js +11 -0
  26. package/src/__tests__/index.js +81 -0
  27. package/src/__tests__/noaa.js +92 -0
  28. package/src/astronomy/__tests__/coefficients.js +12 -0
  29. package/src/astronomy/__tests__/index.js +96 -0
  30. package/src/astronomy/coefficients.js +72 -0
  31. package/src/astronomy/constants.js +4 -0
  32. package/src/astronomy/index.js +201 -0
  33. package/src/constituents/__tests__/compound-constituent.js +44 -0
  34. package/src/constituents/__tests__/constituent.js +65 -0
  35. package/src/constituents/__tests__/index.js +34 -0
  36. package/src/constituents/compound-constituent.js +55 -0
  37. package/src/constituents/constituent.js +74 -0
  38. package/src/constituents/index.js +119 -0
  39. package/src/harmonics/__mocks__/water-levels.js +0 -0
  40. package/src/harmonics/__tests__/index.js +123 -0
  41. package/src/harmonics/__tests__/prediction.js +148 -0
  42. package/src/harmonics/index.js +87 -0
  43. package/src/harmonics/prediction.js +175 -0
  44. package/src/index.js +45 -0
  45. package/src/node-corrections/__tests__/index.js +114 -0
  46. package/src/node-corrections/index.js +208 -0
@@ -0,0 +1,148 @@
1
+ import harmonics from '../index'
2
+ import mockHarmonicConstituents from '../../__mocks__/constituents'
3
+ import mockSecondaryStation from '../../__mocks__/secondary-station'
4
+
5
+ const startDate = new Date()
6
+ startDate.setFullYear(2019)
7
+ startDate.setMonth(8)
8
+ startDate.setDate(1)
9
+ startDate.setHours(0)
10
+ startDate.setMinutes(0)
11
+ startDate.setSeconds(0)
12
+ startDate.setMilliseconds(0)
13
+
14
+ const endDate = new Date()
15
+ endDate.setFullYear(2019)
16
+ endDate.setMonth(8)
17
+ endDate.setDate(1)
18
+ endDate.setHours(6)
19
+ endDate.setMinutes(0)
20
+ endDate.setSeconds(0)
21
+ endDate.setMilliseconds(0)
22
+
23
+ const extremesEndDate = new Date()
24
+ extremesEndDate.setFullYear(2019)
25
+ extremesEndDate.setMonth(8)
26
+ extremesEndDate.setDate(3)
27
+ extremesEndDate.setHours(0)
28
+ extremesEndDate.setMinutes(0)
29
+ extremesEndDate.setSeconds(0)
30
+ extremesEndDate.setMilliseconds(0)
31
+
32
+ const setUpPrediction = () => {
33
+ const harmonic = harmonics({
34
+ harmonicConstituents: mockHarmonicConstituents,
35
+ phaseKey: 'phase_GMT',
36
+ offset: false,
37
+ })
38
+ harmonic.setTimeSpan(startDate, endDate)
39
+ return harmonic.prediction()
40
+ }
41
+
42
+ describe('harmonic prediction', () => {
43
+ test('it creates a timeline prediction', () => {
44
+ const testPrediction = setUpPrediction()
45
+ const results = testPrediction.getTimelinePrediction()
46
+ const lastResult = results.pop()
47
+ expect(results[0].level).toBeCloseTo(-1.347125, 3)
48
+ expect(lastResult.level).toBeCloseTo(2.85263589, 3)
49
+ })
50
+
51
+ test('it creates a timeline prediction with a non-default phase key', () => {
52
+ const results = harmonics({
53
+ harmonicConstituents: mockHarmonicConstituents,
54
+ phaseKey: 'phase_local',
55
+ offset: false,
56
+ })
57
+ .setTimeSpan(startDate, endDate)
58
+ .prediction()
59
+ .getTimelinePrediction()
60
+ expect(results[0].level).toBeCloseTo(2.7560979, 3)
61
+ expect(results.pop().level).toBeCloseTo(-2.9170977, 3)
62
+ })
63
+
64
+ test('it finds high and low tides', () => {
65
+ const results = harmonics({
66
+ harmonicConstituents: mockHarmonicConstituents,
67
+ phaseKey: 'phase_GMT',
68
+ offset: false,
69
+ })
70
+ .setTimeSpan(startDate, extremesEndDate)
71
+ .prediction()
72
+ .getExtremesPrediction()
73
+ expect(results[0].level).toBeCloseTo(-1.5650332, 4)
74
+
75
+ const customLabels = {
76
+ high: 'Super high',
77
+ low: 'Wayyy low',
78
+ }
79
+
80
+ const labelResults = harmonics({
81
+ harmonicConstituents: mockHarmonicConstituents,
82
+ phaseKey: 'phase_GMT',
83
+ offset: false,
84
+ })
85
+ .setTimeSpan(startDate, extremesEndDate)
86
+ .prediction()
87
+ .getExtremesPrediction({ labels: customLabels })
88
+ expect(labelResults[0].label).toBe(customLabels.low)
89
+ })
90
+
91
+ test('it finds high and low tides with high fidelity', () => {
92
+ const results = harmonics({
93
+ harmonicConstituents: mockHarmonicConstituents,
94
+ phaseKey: 'phase_GMT',
95
+ offset: false,
96
+ })
97
+ .setTimeSpan(startDate, extremesEndDate)
98
+ .prediction({ timeFidelity: 60 })
99
+ .getExtremesPrediction()
100
+ expect(results[0].level).toBeCloseTo(-1.5653894, 4)
101
+ })
102
+ })
103
+
104
+ describe('Secondary stations', () => {
105
+ test('it can add offsets to secondary stations', () => {
106
+ const regularResults = harmonics({
107
+ harmonicConstituents: mockHarmonicConstituents,
108
+ phaseKey: 'phase_GMT',
109
+ offset: false,
110
+ })
111
+ .setTimeSpan(startDate, extremesEndDate)
112
+ .prediction()
113
+ .getExtremesPrediction()
114
+
115
+ const offsetResults = harmonics({
116
+ harmonicConstituents: mockHarmonicConstituents,
117
+ phaseKey: 'phase_GMT',
118
+ offset: false,
119
+ })
120
+ .setTimeSpan(startDate, extremesEndDate)
121
+ .prediction()
122
+ .getExtremesPrediction({ offsets: mockSecondaryStation })
123
+
124
+ offsetResults.forEach((offsetResult, index) => {
125
+ if (offsetResult.low) {
126
+ expect(offsetResult.level).toBeCloseTo(
127
+ regularResults[index].level * mockSecondaryStation.height_offset.low,
128
+ 4
129
+ )
130
+ expect(offsetResult.time.getTime()).toBe(
131
+ regularResults[index].time.getTime() +
132
+ mockSecondaryStation.time_offset.low * 60 * 1000
133
+ )
134
+ }
135
+ if (offsetResult.high) {
136
+ expect(offsetResult.level).toBeCloseTo(
137
+ regularResults[index].level * mockSecondaryStation.height_offset.high,
138
+ 4
139
+ )
140
+
141
+ expect(offsetResult.time.getTime()).toBe(
142
+ regularResults[index].time.getTime() +
143
+ mockSecondaryStation.time_offset.high * 60 * 1000
144
+ )
145
+ }
146
+ })
147
+ })
148
+ })
@@ -0,0 +1,87 @@
1
+ import prediction from './prediction'
2
+ import constituentModels from '../constituents/index'
3
+ import { d2r } from '../astronomy/constants'
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 timeline = []
18
+ const endTime = end.getTime() / 1000
19
+ let lastTime = start.getTime() / 1000
20
+ const startTime = lastTime
21
+ const hours = []
22
+ while (lastTime <= endTime) {
23
+ timeline.push(new Date(lastTime * 1000))
24
+ hours.push((lastTime - startTime) / (60 * 60))
25
+ lastTime += seconds
26
+ }
27
+
28
+ return {
29
+ items: timeline,
30
+ hours: 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, index) => {
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: constituents,
79
+ start: start,
80
+ })
81
+ }
82
+
83
+ return Object.freeze(harmonics)
84
+ }
85
+
86
+ export default harmonicsFactory
87
+ export { getDate, getTimeline }
@@ -0,0 +1,175 @@
1
+ import astro from '../astronomy/index'
2
+ import { d2r } from '../astronomy/constants'
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: time,
128
+ hour: 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: baseValue,
166
+ baseSpeed: baseSpeed,
167
+ u: u,
168
+ f: f,
169
+ }
170
+ }
171
+
172
+ return Object.freeze(prediction)
173
+ }
174
+
175
+ export default predictionFactory
package/src/index.js ADDED
@@ -0,0 +1,45 @@
1
+ import harmonics from './harmonics/index'
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: 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