Skip to content

Commit 373e44b

Browse files
committed
Expose all tide-predictor APIs
1 parent 756c8fe commit 373e44b

2 files changed

Lines changed: 166 additions & 60 deletions

File tree

packages/neaps/src/index.ts

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { getDistance } from 'geolib'
22
import stations, { type Station } from '@neaps/tide-stations'
3-
import tidePredictor, { type ExtremesInput } from '@neaps/tide-predictor'
3+
import tidePredictor, {
4+
type TimeSpan,
5+
type ExtremesInput
6+
} from '@neaps/tide-predictor'
47
import type { GeolibInputCoordinates } from 'geolib/es/types'
58

6-
export type ExtremesOptions = ExtremesInput & {
9+
type DatumOption = {
710
/** Datum to return predictions in. Defaults to 'MLLW' if available for the nearest station. */
811
datum?: string
912
}
1013

14+
export type ExtremesOptions = ExtremesInput & DatumOption
15+
export type TimelineOptions = TimeSpan & DatumOption
16+
export type WaterLevelOptions = { time: Date } & DatumOption
17+
1118
/**
1219
* Get extremes prediction using the nearest station to the given position.
1320
*
@@ -29,6 +36,24 @@ export function getExtremesPrediction(
2936
return nearestStation(options).getExtremesPrediction(options)
3037
}
3138

39+
/**
40+
* Get timeline prediction using the nearest station to the given position.
41+
*/
42+
export function getTimelinePrediction(
43+
options: GeolibInputCoordinates & TimelineOptions
44+
) {
45+
return nearestStation(options).getTimelinePrediction(options)
46+
}
47+
48+
/**
49+
* Get water level at a specific time using the nearest station to the given position.
50+
*/
51+
export function getWaterLevelAtTime(
52+
options: GeolibInputCoordinates & WaterLevelOptions
53+
) {
54+
return nearestStation(options).getWaterLevelAtTime(options)
55+
}
56+
3257
/**
3358
* Find the nearest station to the given position.
3459
*/
@@ -37,7 +62,8 @@ export function nearestStation(position: GeolibInputCoordinates) {
3762
}
3863

3964
/**
40-
* Find stations closest to the given position.
65+
* Find stations near the given position.
66+
* @param limit Maximum number of stations to return (default: 10)
4167
*/
4268
export function stationsNear(position: GeolibInputCoordinates, limit = 10) {
4369
return stations
@@ -48,7 +74,7 @@ export function stationsNear(position: GeolibInputCoordinates, limit = 10) {
4874
}
4975

5076
/**
51-
* Find a station by its ID or source ID.
77+
* Find a specific station by its ID or source ID.
5278
*/
5379
export function findStation(query: string) {
5480
const searches = [
@@ -72,41 +98,63 @@ export function useStation(metadata: Station, distance?: number) {
7298
// Use MLLW as the default datum if available
7399
const defaultDatum = 'MLLW' in metadata.datums ? 'MLLW' : undefined
74100

75-
return {
76-
metadata,
77-
distance,
78-
getExtremesPrediction({ datum = defaultDatum, ...input }: ExtremesOptions) {
79-
let offset = 0
80-
81-
if (datum) {
82-
const datumOffset = metadata.datums?.[datum]
83-
const mslOffset = metadata.datums?.['MSL']
101+
function getPredictor({ datum = defaultDatum }: DatumOption = {}) {
102+
let offset = 0
84103

85-
if (!datumOffset) {
86-
throw new Error(
87-
`Station ${metadata.id} missing ${datum} datum. Available datums: ${Object.keys(metadata.datums || {}).join(', ')}`
88-
)
89-
}
104+
if (datum) {
105+
const datumOffset = metadata.datums?.[datum]
106+
const mslOffset = metadata.datums?.['MSL']
90107

91-
if (!mslOffset) {
92-
throw new Error(
93-
`Station ${metadata.id} missing MSL datum, so predictions can't be given in ${datum}.`
94-
)
95-
}
108+
if (!datumOffset) {
109+
throw new Error(
110+
`Station ${metadata.id} missing ${datum} datum. Available datums: ${Object.keys(metadata.datums || {}).join(', ')}`
111+
)
112+
}
96113

97-
offset = mslOffset - datumOffset
114+
if (!mslOffset) {
115+
throw new Error(
116+
`Station ${metadata.id} missing MSL datum, so predictions can't be given in ${datum}.`
117+
)
98118
}
99119

100-
const predictor = tidePredictor(metadata.harmonic_constituents, {
101-
phaseKey: 'phase_UTC',
102-
offset
103-
})
120+
offset = mslOffset - datumOffset
121+
}
122+
123+
return tidePredictor(metadata.harmonic_constituents, {
124+
phaseKey: 'phase_UTC',
125+
offset
126+
})
127+
}
104128

129+
return {
130+
metadata,
131+
distance,
132+
defaultDatum,
133+
getExtremesPrediction({ datum = defaultDatum, ...input }: ExtremesOptions) {
105134
return {
106135
datum,
107136
distance,
108137
station: metadata,
109-
predictions: predictor.getExtremesPrediction(input)
138+
predictions: getPredictor({ datum }).getExtremesPrediction(input)
139+
}
140+
},
141+
142+
getTimelinePrediction({
143+
datum = defaultDatum,
144+
...params
145+
}: TimelineOptions) {
146+
return {
147+
datum,
148+
station: metadata,
149+
timeline: getPredictor({ datum }).getTimelinePrediction(params)
150+
}
151+
},
152+
153+
getWaterLevelAtTime({ time, datum = defaultDatum }: WaterLevelOptions) {
154+
return {
155+
datum,
156+
station: metadata,
157+
...getPredictor({ datum }).getWaterLevelAtTime({ time })
110158
}
111159
}
112160
}

packages/neaps/test/index.test.ts

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
getExtremesPrediction,
44
nearestStation,
55
findStation,
6-
useStation
6+
useStation,
7+
getTimelinePrediction,
8+
getWaterLevelAtTime
79
} from '../src/index.js'
810
import { describe, test, expect } from 'vitest'
911

@@ -33,18 +35,85 @@ describe('getExtremesPrediction', () => {
3335
expect(predictions[0].low).toBe(true)
3436
expect(predictions[0].label).toBe('Low')
3537
})
36-
;[
37-
{ lat: 26.7, lon: -80.05 },
38-
{ lat: 26.7, lng: -80.05 },
39-
{ latitude: 26.7, longitude: -80.05 }
40-
].forEach((position) => {
41-
test(`accepts position with ${Object.keys(position).join('/')}`, () => {
42-
const extremes = getExtremesPrediction({
43-
...position,
44-
start: new Date('2025-12-17T00:00:00Z'),
45-
end: new Date('2025-12-18T05:00:00Z')
38+
})
39+
40+
describe('getTimelinePrediction', () => {
41+
test('gets timeline from nearest station', () => {
42+
const timeline = getTimelinePrediction({
43+
lat: 26.7,
44+
lon: -80.05,
45+
start: new Date('2025-12-19T00:00:00-05:00'),
46+
end: new Date('2025-12-19T01:00:00-05:00')
47+
})
48+
49+
expect(timeline.station.id).toEqual('us-fl-port-of-west-palm-beach')
50+
expect(timeline.datum).toBe('MLLW')
51+
expect(timeline.timeline.length).toBe(7) // Every 10 minutes for 1 hour = 7 points
52+
})
53+
})
54+
55+
describe('getWaterLevelAtTime', () => {
56+
test('gets water level at specific time from nearest station', () => {
57+
const waterLevel = getWaterLevelAtTime({
58+
lat: 26.7,
59+
lon: -80.05,
60+
time: new Date('2025-12-19T00:30:00-05:00'),
61+
datum: 'MSL'
62+
})
63+
64+
expect(waterLevel.station.id).toEqual('us-fl-port-of-west-palm-beach')
65+
expect(waterLevel.datum).toBe('MSL')
66+
expect(waterLevel.time).toEqual(new Date('2025-12-19T05:30:00.000Z'))
67+
expect(typeof waterLevel.level).toBe('number')
68+
})
69+
})
70+
71+
describe('for a specific station', () => {
72+
const station = nearestStation({ lat: 45.6, lon: -122.7 })
73+
74+
describe('getExtremesPrediction', () => {
75+
test('can return extremes from station', () => {
76+
const station = nearestStation({ lat: 26.7, lon: -80.05 })
77+
78+
const start = new Date('2025-12-17T00:00:00-05:00')
79+
const end = new Date('2025-12-18T05:00:00-05:00')
80+
81+
const { predictions } = station.getExtremesPrediction({
82+
start,
83+
end,
84+
timeFidelity: 6,
85+
datum: 'MLLW'
86+
})
87+
88+
expect(predictions.length).toBe(4)
89+
expect(predictions[0].time).toEqual(new Date('2025-12-17T11:23:06.000Z'))
90+
expect(predictions[0].level).toBeCloseTo(0.9, 1)
91+
expect(predictions[0].high).toBe(true)
92+
expect(predictions[0].low).toBe(false)
93+
expect(predictions[0].label).toBe('High')
94+
})
95+
})
96+
97+
describe('getTimelinePrediction', () => {
98+
test('gets timeline', () => {
99+
const prediction = station.getTimelinePrediction({
100+
start: new Date('2025-12-19T00:00:00Z'),
101+
end: new Date('2025-12-19T01:00:00Z')
46102
})
47-
expect(extremes.station.id).toEqual('us-fl-port-of-west-palm-beach')
103+
// Every 10 minutes for 1 hour = 7 points
104+
expect(prediction.timeline.length).toBe(7)
105+
expect(prediction.datum).toBe('MLLW')
106+
})
107+
})
108+
109+
describe('getWaterLevelAtTime', () => {
110+
test('gets water level at specific time', () => {
111+
const prediction = station.getWaterLevelAtTime({
112+
time: new Date('2025-12-19T00:30:00Z')
113+
})
114+
expect(prediction.time).toEqual(new Date('2025-12-19T00:30:00Z'))
115+
expect(prediction.datum).toBe('MLLW')
116+
expect(typeof prediction.level).toBe('number')
48117
})
49118
})
50119
})
@@ -56,26 +125,15 @@ describe('nearestStation', () => {
56125
expect(station.metadata.latitude).toBeCloseTo(26.77)
57126
expect(station.metadata.longitude).toBeCloseTo(-80.0517)
58127
})
59-
60-
test('can return extremes from station', () => {
61-
const station = nearestStation({ lat: 26.7, lon: -80.05 })
62-
63-
const start = new Date('2025-12-17T00:00:00-05:00')
64-
const end = new Date('2025-12-18T05:00:00-05:00')
65-
66-
const { predictions } = station.getExtremesPrediction({
67-
start,
68-
end,
69-
timeFidelity: 6,
70-
datum: 'MLLW'
128+
;[
129+
{ lat: 26.7, lon: -80.05 },
130+
{ lat: 26.7, lng: -80.05 },
131+
{ latitude: 26.7, longitude: -80.05 }
132+
].forEach((position) => {
133+
test(`finds station with ${Object.keys(position).join('/')}`, () => {
134+
const station = nearestStation(position)
135+
expect(station.metadata.id).toEqual('us-fl-port-of-west-palm-beach')
71136
})
72-
73-
expect(predictions.length).toBe(4)
74-
expect(predictions[0].time).toEqual(new Date('2025-12-17T11:23:06.000Z'))
75-
expect(predictions[0].level).toBeCloseTo(0.9, 1)
76-
expect(predictions[0].high).toBe(true)
77-
expect(predictions[0].low).toBe(false)
78-
expect(predictions[0].label).toBe('High')
79137
})
80138
})
81139

0 commit comments

Comments
 (0)