Skip to content

Commit f7e9a7d

Browse files
committed
Move useStation into @neaps/tide-pridictor
1 parent 8c354e3 commit f7e9a7d

4 files changed

Lines changed: 201 additions & 148 deletions

File tree

packages/neaps/src/index.ts

Lines changed: 14 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,18 @@ import {
22
stations,
33
near,
44
nearest,
5-
type Station,
65
type NearOptions,
76
type NearestOptions,
87
} from "@neaps/tide-database";
9-
import tidePredictor, {
10-
type TimeSpan,
11-
type ExtremesInput,
12-
Extreme,
13-
TimelinePoint,
8+
import {
9+
useStation,
10+
type Station,
11+
type StationPredictor,
12+
type StationExtremesOptions,
13+
type StationTimelineOptions,
14+
type StationWaterLevelOptions,
1415
} from "@neaps/tide-predictor";
1516

16-
type Units = "meters" | "feet";
17-
type PredictionOptions = {
18-
/** Datum to return predictions in. Defaults to 'MLLW' if available for the nearest station. */
19-
datum?: string;
20-
21-
/** Units for returned water levels. Defaults to 'meters'. */
22-
units?: Units;
23-
};
24-
25-
export type ExtremesOptions = ExtremesInput & PredictionOptions;
26-
export type TimelineOptions = TimeSpan & PredictionOptions;
27-
export type WaterLevelOptions = { time: Date } & PredictionOptions;
28-
29-
export type StationPrediction = {
30-
datum: string | undefined;
31-
units: Units;
32-
station: Station;
33-
distance?: number;
34-
};
35-
36-
export type StationExtremesPrediction = StationPrediction & {
37-
extremes: Extreme[];
38-
};
39-
40-
export type StationTimelinePrediction = StationPrediction & {
41-
timeline: TimelinePoint[];
42-
};
43-
44-
export type StationWaterLevelPrediction = StationPrediction & TimelinePoint;
45-
46-
export type StationPredictor = Station & {
47-
distance?: number;
48-
defaultDatum?: string;
49-
getExtremesPrediction: (options: ExtremesOptions) => StationExtremesPrediction;
50-
getTimelinePrediction: (options: TimelineOptions) => StationTimelinePrediction;
51-
getWaterLevelAtTime: (options: WaterLevelOptions) => StationWaterLevelPrediction;
52-
};
53-
54-
const feetPerMeter = 3.2808399;
55-
const defaultUnits: Units = "meters";
56-
5717
/**
5818
* Get extremes prediction using the nearest station to the given position.
5919
*
@@ -69,21 +29,21 @@ const defaultUnits: Units = "meters";
6929
* datum: 'MLLW', // optional, defaults to MLLW if available
7030
* })
7131
*/
72-
export function getExtremesPrediction(options: NearestOptions & ExtremesOptions) {
32+
export function getExtremesPrediction(options: NearestOptions & StationExtremesOptions) {
7333
return nearestStation(options).getExtremesPrediction(options);
7434
}
7535

7636
/**
7737
* Get timeline prediction using the nearest station to the given position.
7838
*/
79-
export function getTimelinePrediction(options: NearestOptions & TimelineOptions) {
39+
export function getTimelinePrediction(options: NearestOptions & StationTimelineOptions) {
8040
return nearestStation(options).getTimelinePrediction(options);
8141
}
8242

8343
/**
8444
* Get water level at a specific time using the nearest station to the given position.
8545
*/
86-
export function getWaterLevelAtTime(options: NearestOptions & WaterLevelOptions) {
46+
export function getWaterLevelAtTime(options: NearestOptions & StationWaterLevelOptions) {
8747
return nearestStation(options).getWaterLevelAtTime(options);
8848
}
8949

@@ -93,21 +53,21 @@ export function getWaterLevelAtTime(options: NearestOptions & WaterLevelOptions)
9353
export function nearestStation(options: NearestOptions) {
9454
const data = nearest(options);
9555
if (!data) throw new Error(`No stations found with options: ${JSON.stringify(options)}`);
96-
return useStation(...data);
56+
return useStation(...data, findStation);
9757
}
9858

9959
/**
10060
* Find stations near the given position.
10161
* @param limit Maximum number of stations to return (default: 10)
10262
*/
10363
export function stationsNear(options: NearOptions) {
104-
return near(options).map(([station, distance]) => useStation(station, distance));
64+
return near(options).map(([station, distance]) => useStation(station, distance, findStation));
10565
}
10666

10767
/**
10868
* Find a specific station by its ID or source ID.
10969
*/
110-
export function findStation(query: string) {
70+
export function findStation(query: string): StationPredictor {
11171
const searches = [(s: Station) => s.id === query, (s: Station) => s.source.id === query];
11272

11373
let found: Station | undefined = undefined;
@@ -119,96 +79,5 @@ export function findStation(query: string) {
11979

12080
if (!found) throw new Error(`Station not found: ${query}`);
12181

122-
return useStation(found);
123-
}
124-
125-
export function useStation(station: Station, distance?: number): StationPredictor {
126-
// If subordinate station, use the reference station for datums and constituents
127-
let reference = station;
128-
if (station.type === "subordinate" && station.offsets?.reference) {
129-
reference = findStation(station.offsets?.reference);
130-
}
131-
const { datums, harmonic_constituents } = reference;
132-
133-
// Use MLLW as the default datum if available
134-
const defaultDatum = "MLLW" in datums ? "MLLW" : undefined;
135-
136-
function getPredictor({ datum = defaultDatum }: PredictionOptions = {}) {
137-
let offset = 0;
138-
139-
if (datum) {
140-
const datumOffset = datums?.[datum];
141-
const mslOffset = datums?.["MSL"];
142-
143-
if (typeof datumOffset !== "number") {
144-
throw new Error(
145-
`Station ${station.id} missing ${datum} datum. Available datums: ${Object.keys(datums).join(", ")}`,
146-
);
147-
}
148-
149-
if (typeof mslOffset !== "number") {
150-
throw new Error(
151-
`Station ${station.id} missing MSL datum, so predictions can't be given in ${datum}.`,
152-
);
153-
}
154-
155-
offset = mslOffset - datumOffset;
156-
}
157-
158-
return tidePredictor(harmonic_constituents, { offset });
159-
}
160-
161-
return {
162-
...station,
163-
distance,
164-
datums,
165-
harmonic_constituents,
166-
defaultDatum,
167-
getExtremesPrediction({
168-
datum = defaultDatum,
169-
units = defaultUnits,
170-
...options
171-
}: ExtremesOptions) {
172-
const extremes = getPredictor({ datum })
173-
.getExtremesPrediction({ ...options, offsets: station.offsets })
174-
.map((e) => toPreferredUnits(e, units));
175-
176-
return { datum, units, station, distance, extremes };
177-
},
178-
179-
getTimelinePrediction({
180-
datum = defaultDatum,
181-
units = defaultUnits,
182-
...options
183-
}: TimelineOptions) {
184-
if (station.type === "subordinate") {
185-
throw new Error(`Timeline predictions are not supported for subordinate stations.`);
186-
}
187-
const timeline = getPredictor({ datum })
188-
.getTimelinePrediction(options)
189-
.map((e) => toPreferredUnits(e, units));
190-
191-
return { datum, units, station, distance, timeline };
192-
},
193-
194-
getWaterLevelAtTime({ time, datum = defaultDatum, units = defaultUnits }: WaterLevelOptions) {
195-
if (station.type === "subordinate") {
196-
throw new Error(`Water level predictions are not supported for subordinate stations.`);
197-
}
198-
199-
const prediction = toPreferredUnits(
200-
getPredictor({ datum }).getWaterLevelAtTime({ time }),
201-
units,
202-
);
203-
204-
return { datum, units, station, distance, ...prediction };
205-
},
206-
};
207-
}
208-
209-
function toPreferredUnits<T extends { level: number }>(prediction: T, units: Units): T {
210-
let { level } = prediction;
211-
if (units === "feet") level *= feetPerMeter;
212-
else if (units !== "meters") throw new Error(`Unsupported units: ${units}`);
213-
return { ...prediction, level };
82+
return useStation(found, undefined, findStation);
21483
}

packages/neaps/test/index.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
getExtremesPrediction,
44
nearestStation,
55
findStation,
6-
useStation,
76
getTimelinePrediction,
87
getWaterLevelAtTime,
98
stationsNear,
@@ -357,7 +356,7 @@ describe("datum", () => {
357356
);
358357
if (!station) expect.fail("No station without MSL datum found");
359358
expect(() => {
360-
useStation(station).getExtremesPrediction({
359+
findStation(station.id).getExtremesPrediction({
361360
start: new Date("2025-12-17T00:00:00Z"),
362361
end: new Date("2025-12-18T00:00:00Z"),
363362
datum: Object.keys(station.datums)[0],
@@ -371,7 +370,7 @@ describe("datum", () => {
371370
(s) => s.type === "reference" && Object.entries(s.datums).length === 0,
372371
);
373372
if (!station) expect.fail("No station without datums found");
374-
const extremes = useStation(station).getExtremesPrediction({
373+
const extremes = findStation(station.id).getExtremesPrediction({
375374
start: new Date("2025-12-17T00:00:00Z"),
376375
end: new Date("2025-12-18T00:00:00Z"),
377376
});

packages/tide-predictor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ tidePredictionFactory.constituents = constituents;
7575

7676
export default tidePredictionFactory;
7777
export type { HarmonicConstituent, TimelinePoint, Extreme };
78+
export * from "./station.js";

0 commit comments

Comments
 (0)