@ryancardin/noaa-tides-currents-mcp-server 1.0.0 → 2.0.1
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.
- package/README.md +164 -199
- package/dist/client/cache.d.ts +15 -0
- package/dist/client/cache.js +44 -0
- package/dist/client/http.d.ts +28 -0
- package/dist/client/http.js +178 -0
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +28 -0
- package/dist/format/respond.d.ts +25 -0
- package/dist/format/respond.js +40 -0
- package/dist/format/series.d.ts +16 -0
- package/dist/format/series.js +42 -0
- package/dist/format/units.d.ts +16 -0
- package/dist/format/units.js +37 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +78 -8
- package/dist/interfaces/moon.d.ts +4 -4
- package/dist/interfaces/moon.js +58 -17
- package/dist/interfaces/sun.d.ts +4 -4
- package/dist/interfaces/sun.js +94 -25
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.js +89 -0
- package/dist/reference/content.d.ts +10 -0
- package/dist/reference/content.js +202 -0
- package/dist/resources/index.d.ts +7 -0
- package/dist/resources/index.js +73 -0
- package/dist/schemas/common.d.ts +38 -14
- package/dist/schemas/common.js +82 -18
- package/dist/services/data-api.d.ts +81 -0
- package/dist/services/data-api.js +117 -0
- package/dist/services/dpapi.d.ts +55 -0
- package/dist/services/dpapi.js +60 -0
- package/dist/services/metadata-api.d.ts +62 -0
- package/dist/services/metadata-api.js +105 -0
- package/dist/services/moon-phase-service.d.ts +2 -2
- package/dist/services/moon-phase-service.js +23 -25
- package/dist/services/sun-service.d.ts +2 -2
- package/dist/services/sun-service.js +53 -31
- package/dist/tools/astronomy.d.ts +7 -0
- package/dist/tools/astronomy.js +270 -0
- package/dist/tools/currents.d.ts +5 -0
- package/dist/tools/currents.js +168 -0
- package/dist/tools/derived.d.ts +6 -0
- package/dist/tools/derived.js +296 -0
- package/dist/tools/index.d.ts +3 -14
- package/dist/tools/index.js +17 -32
- package/dist/tools/met.d.ts +5 -0
- package/dist/tools/met.js +110 -0
- package/dist/tools/reference.d.ts +6 -0
- package/dist/tools/reference.js +33 -0
- package/dist/tools/station-metadata.d.ts +6 -0
- package/dist/tools/station-metadata.js +208 -0
- package/dist/tools/stations.d.ts +5 -0
- package/dist/tools/stations.js +265 -0
- package/dist/tools/water.d.ts +5 -0
- package/dist/tools/water.js +270 -0
- package/dist/validation/dates.d.ts +50 -0
- package/dist/validation/dates.js +139 -0
- package/package.json +27 -14
- package/.claude/settings.local.json +0 -29
- package/CLAUDE.md +0 -71
- package/Dockerfile +0 -14
- package/smithery.yaml +0 -16
- package/src/index.ts +0 -13
- package/src/interfaces/moon.ts +0 -44
- package/src/interfaces/noaa.ts +0 -130
- package/src/interfaces/parameters.ts +0 -20
- package/src/interfaces/sun.ts +0 -57
- package/src/schemas/common.ts +0 -23
- package/src/schemas/dpapi.ts +0 -99
- package/src/server/config.ts +0 -43
- package/src/server/mcp-server.ts +0 -135
- package/src/services/dpapi-service.ts +0 -187
- package/src/services/moon-phase-service.ts +0 -167
- package/src/services/noaa-parameters-service.ts +0 -139
- package/src/services/noaa-service.ts +0 -171
- package/src/services/sun-service.ts +0 -275
- package/src/tools/derived-product-tools.ts +0 -180
- package/src/tools/index.ts +0 -40
- package/src/tools/moon-tools.ts +0 -79
- package/src/tools/parameter-tools.ts +0 -82
- package/src/tools/station-tools.ts +0 -57
- package/src/tools/sun-tools.ts +0 -120
- package/src/tools/water-tools.ts +0 -166
- package/src/types/moon.ts +0 -27
- package/src/types/sun.ts +0 -51
- package/src/types/suncalc.d.ts +0 -110
- package/test-dpapi.js +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOAA CO-OPS Data Retrieval API service.
|
|
3
|
+
*
|
|
4
|
+
* Encodes the per-product rules discovered from the official docs:
|
|
5
|
+
* - which products need a datum
|
|
6
|
+
* - per-product/interval maximum request spans (validated before calling)
|
|
7
|
+
* - product-specific restrictions (daily_mean is Great Lakes-only and
|
|
8
|
+
* requires time_zone=lst; bin=0 caps currents at 7 days; etc.)
|
|
9
|
+
*/
|
|
10
|
+
import { fetchDataApi } from "../client/http.js";
|
|
11
|
+
import { assertSpanWithinLimit, resolveDateParams, } from "../validation/dates.js";
|
|
12
|
+
function dateQuery(resolved) {
|
|
13
|
+
return {
|
|
14
|
+
date: resolved.date,
|
|
15
|
+
begin_date: resolved.begin_date,
|
|
16
|
+
end_date: resolved.end_date,
|
|
17
|
+
range: resolved.range,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const WATER_LEVEL_PRODUCTS = {
|
|
21
|
+
"1": "one_minute_water_level",
|
|
22
|
+
"6": "water_level",
|
|
23
|
+
hourly: "hourly_height",
|
|
24
|
+
};
|
|
25
|
+
export async function getWaterLevels(params) {
|
|
26
|
+
const product = WATER_LEVEL_PRODUCTS[params.interval];
|
|
27
|
+
const resolved = resolveDateParams(params);
|
|
28
|
+
assertSpanWithinLimit(resolved.spanDays, product, `${params.interval}-minute-interval water levels`);
|
|
29
|
+
const response = await fetchDataApi({
|
|
30
|
+
product,
|
|
31
|
+
station: params.station,
|
|
32
|
+
datum: params.datum,
|
|
33
|
+
units: params.units,
|
|
34
|
+
time_zone: params.time_zone,
|
|
35
|
+
...dateQuery(resolved),
|
|
36
|
+
});
|
|
37
|
+
return { product, response };
|
|
38
|
+
}
|
|
39
|
+
export async function getWaterLevelSummaries(params) {
|
|
40
|
+
const resolved = resolveDateParams(params);
|
|
41
|
+
assertSpanWithinLimit(resolved.spanDays, params.product, params.product);
|
|
42
|
+
// daily_mean is only computed for Great Lakes stations and NOAA requires
|
|
43
|
+
// time_zone=lst for it — force it rather than surface a cryptic upstream error.
|
|
44
|
+
let timeZone = params.time_zone;
|
|
45
|
+
let timeZoneForced = false;
|
|
46
|
+
if (params.product === "daily_mean" && timeZone !== "lst") {
|
|
47
|
+
timeZone = "lst";
|
|
48
|
+
timeZoneForced = true;
|
|
49
|
+
}
|
|
50
|
+
const response = await fetchDataApi({
|
|
51
|
+
product: params.product,
|
|
52
|
+
station: params.station,
|
|
53
|
+
datum: params.datum,
|
|
54
|
+
units: params.units,
|
|
55
|
+
time_zone: timeZone,
|
|
56
|
+
...dateQuery(resolved),
|
|
57
|
+
});
|
|
58
|
+
return { response, timeZoneForced };
|
|
59
|
+
}
|
|
60
|
+
export async function getTidePredictions(params) {
|
|
61
|
+
const resolved = resolveDateParams(params);
|
|
62
|
+
const limitKey = params.interval === "hilo" ? "predictions:hilo" : "predictions";
|
|
63
|
+
assertSpanWithinLimit(resolved.spanDays, limitKey, params.interval === "hilo"
|
|
64
|
+
? "high/low tide predictions"
|
|
65
|
+
: "interval tide predictions");
|
|
66
|
+
return fetchDataApi({
|
|
67
|
+
product: "predictions",
|
|
68
|
+
station: params.station,
|
|
69
|
+
datum: params.datum,
|
|
70
|
+
units: params.units,
|
|
71
|
+
time_zone: params.time_zone,
|
|
72
|
+
interval: params.interval,
|
|
73
|
+
...dateQuery(resolved),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export async function getCurrents(params) {
|
|
77
|
+
const resolved = resolveDateParams(params);
|
|
78
|
+
assertSpanWithinLimit(resolved.spanDays, "currents", "observed currents");
|
|
79
|
+
return fetchDataApi({
|
|
80
|
+
product: "currents",
|
|
81
|
+
station: params.station,
|
|
82
|
+
units: params.units,
|
|
83
|
+
time_zone: params.time_zone,
|
|
84
|
+
bin: params.bin,
|
|
85
|
+
expand: params.expand_detailed ? "detailed" : undefined,
|
|
86
|
+
...dateQuery(resolved),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function getCurrentPredictions(params) {
|
|
90
|
+
const resolved = resolveDateParams(params);
|
|
91
|
+
const limitKey = params.interval === "max_slack"
|
|
92
|
+
? "currents_predictions:max_slack"
|
|
93
|
+
: "currents_predictions";
|
|
94
|
+
assertSpanWithinLimit(resolved.spanDays, limitKey, "current predictions");
|
|
95
|
+
return fetchDataApi({
|
|
96
|
+
product: "currents_predictions",
|
|
97
|
+
station: params.station,
|
|
98
|
+
units: params.units,
|
|
99
|
+
time_zone: params.time_zone,
|
|
100
|
+
bin: params.bin,
|
|
101
|
+
interval: params.interval,
|
|
102
|
+
vel_type: params.vel_type === "speed_dir" ? "speed_dir" : undefined,
|
|
103
|
+
...dateQuery(resolved),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export async function getMeteorologicalData(params) {
|
|
107
|
+
const resolved = resolveDateParams(params);
|
|
108
|
+
assertSpanWithinLimit(resolved.spanDays, params.product === "air_gap" ? "air_gap" : "met", params.product);
|
|
109
|
+
return fetchDataApi({
|
|
110
|
+
product: params.product,
|
|
111
|
+
station: params.station,
|
|
112
|
+
units: params.units,
|
|
113
|
+
time_zone: params.time_zone,
|
|
114
|
+
interval: params.interval === "h" ? "h" : undefined,
|
|
115
|
+
...dateQuery(resolved),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOAA CO-OPS Derived Product API (DPAPI) service.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint paths verified live (2026-07):
|
|
5
|
+
* /webapi/product/sealvltrends.json — relative sea level trends
|
|
6
|
+
* /webapi/product.json?name=extremewaterlevels
|
|
7
|
+
* /webapi/product.json?name=toptenwaterlevels|peakwaterlevels
|
|
8
|
+
* /webapi/product/slr_projections.json — sea level rise projections
|
|
9
|
+
* /webapi/htf/htf_daily|htf_monthly|htf_seasonal|htf_annual|
|
|
10
|
+
* htf_met_year_annual|htf_met_year_annual_outlook|
|
|
11
|
+
* htf_projection_decadal|htf_record|htf_likely_decadal_scenarios .json
|
|
12
|
+
* /webapi/htb.json — HTF daily likelihoods
|
|
13
|
+
*/
|
|
14
|
+
import type { UnitSystem } from "../format/units.js";
|
|
15
|
+
export declare function getSeaLevelTrends(params: {
|
|
16
|
+
station?: string;
|
|
17
|
+
affil?: "Global" | "US";
|
|
18
|
+
}): Promise<Record<string, unknown>>;
|
|
19
|
+
export declare function getExtremeWaterLevels(params: {
|
|
20
|
+
station: string;
|
|
21
|
+
units: UnitSystem;
|
|
22
|
+
extremeType?: "annuals" | "monthlies";
|
|
23
|
+
levelType?: "high" | "low";
|
|
24
|
+
datum?: string;
|
|
25
|
+
}): Promise<Record<string, unknown>>;
|
|
26
|
+
export declare function getTopTenWaterLevels(params: {
|
|
27
|
+
station: string;
|
|
28
|
+
analysis: "toptenwaterlevels" | "peakwaterlevels";
|
|
29
|
+
units: UnitSystem;
|
|
30
|
+
datum?: string;
|
|
31
|
+
year?: number;
|
|
32
|
+
}): Promise<Record<string, unknown>>;
|
|
33
|
+
export declare function getSeaLevelRiseProjections(params: {
|
|
34
|
+
station?: string;
|
|
35
|
+
scenario?: string;
|
|
36
|
+
projection_year?: number;
|
|
37
|
+
report_year?: number;
|
|
38
|
+
units?: UnitSystem;
|
|
39
|
+
}): Promise<Record<string, unknown>>;
|
|
40
|
+
export type HtfReport = "daily" | "monthly" | "seasonal" | "annual" | "met_year_annual" | "annual_outlook" | "projections" | "record_days" | "likely_scenarios" | "daily_likelihoods";
|
|
41
|
+
export interface HtfParams {
|
|
42
|
+
station: string;
|
|
43
|
+
start_date?: string;
|
|
44
|
+
end_date?: string;
|
|
45
|
+
year?: number;
|
|
46
|
+
month?: number;
|
|
47
|
+
range?: number;
|
|
48
|
+
season_months?: "DJF" | "MAM" | "JJA" | "SON";
|
|
49
|
+
met_year?: number;
|
|
50
|
+
decade?: number;
|
|
51
|
+
flood_threshold?: "minor" | "moderate" | "major";
|
|
52
|
+
units?: UnitSystem;
|
|
53
|
+
datum?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function getHighTideFlooding(report: HtfReport, params: HtfParams): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOAA CO-OPS Derived Product API (DPAPI) service.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint paths verified live (2026-07):
|
|
5
|
+
* /webapi/product/sealvltrends.json — relative sea level trends
|
|
6
|
+
* /webapi/product.json?name=extremewaterlevels
|
|
7
|
+
* /webapi/product.json?name=toptenwaterlevels|peakwaterlevels
|
|
8
|
+
* /webapi/product/slr_projections.json — sea level rise projections
|
|
9
|
+
* /webapi/htf/htf_daily|htf_monthly|htf_seasonal|htf_annual|
|
|
10
|
+
* htf_met_year_annual|htf_met_year_annual_outlook|
|
|
11
|
+
* htf_projection_decadal|htf_record|htf_likely_decadal_scenarios .json
|
|
12
|
+
* /webapi/htb.json — HTF daily likelihoods
|
|
13
|
+
*/
|
|
14
|
+
import { fetchDpapi } from "../client/http.js";
|
|
15
|
+
export async function getSeaLevelTrends(params) {
|
|
16
|
+
return fetchDpapi("/webapi/product/sealvltrends.json", params);
|
|
17
|
+
}
|
|
18
|
+
export async function getExtremeWaterLevels(params) {
|
|
19
|
+
return fetchDpapi("/webapi/product.json", {
|
|
20
|
+
name: "extremewaterlevels",
|
|
21
|
+
...params,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function getTopTenWaterLevels(params) {
|
|
25
|
+
const { analysis, ...rest } = params;
|
|
26
|
+
return fetchDpapi("/webapi/product.json", { name: analysis, ...rest });
|
|
27
|
+
}
|
|
28
|
+
export async function getSeaLevelRiseProjections(params) {
|
|
29
|
+
return fetchDpapi("/webapi/product/slr_projections.json", params);
|
|
30
|
+
}
|
|
31
|
+
const HTF_PATHS = {
|
|
32
|
+
daily: "/webapi/htf/htf_daily.json",
|
|
33
|
+
monthly: "/webapi/htf/htf_monthly.json",
|
|
34
|
+
seasonal: "/webapi/htf/htf_seasonal.json",
|
|
35
|
+
annual: "/webapi/htf/htf_annual.json",
|
|
36
|
+
met_year_annual: "/webapi/htf/htf_met_year_annual.json",
|
|
37
|
+
annual_outlook: "/webapi/htf/htf_met_year_annual_outlook.json",
|
|
38
|
+
projections: "/webapi/htf/htf_projection_decadal.json",
|
|
39
|
+
record_days: "/webapi/htf/htf_record.json",
|
|
40
|
+
likely_scenarios: "/webapi/htf/htf_likely_decadal_scenarios.json",
|
|
41
|
+
daily_likelihoods: "/webapi/htb.json",
|
|
42
|
+
};
|
|
43
|
+
export async function getHighTideFlooding(report, params) {
|
|
44
|
+
if (report === "daily" && (!params.start_date || !params.end_date)) {
|
|
45
|
+
throw new Error("The daily high-tide-flooding report requires both start_date and end_date (YYYYMMDD).");
|
|
46
|
+
}
|
|
47
|
+
const query = { ...params };
|
|
48
|
+
// NOAA's `range` only takes effect alongside a starting year (year..year+range).
|
|
49
|
+
// A bare range reads most naturally as "the last N years" — translate it.
|
|
50
|
+
if (query.range !== undefined) {
|
|
51
|
+
const startYear = new Date().getUTCFullYear() - query.range;
|
|
52
|
+
if (report === "met_year_annual" || report === "annual_outlook") {
|
|
53
|
+
query.met_year = query.met_year ?? startYear;
|
|
54
|
+
}
|
|
55
|
+
else if (query.year === undefined) {
|
|
56
|
+
query.year = startYear;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return fetchDpapi(HTF_PATHS[report], { ...query });
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOAA CO-OPS Metadata API (MDAPI) service.
|
|
3
|
+
*
|
|
4
|
+
* Notes from live verification of the API (2026-07):
|
|
5
|
+
* - The stations list endpoint IGNORES lat/lon/radius query params, so
|
|
6
|
+
* "find stations near a point" is implemented client-side with a cached
|
|
7
|
+
* station directory and Haversine distance.
|
|
8
|
+
* - Response array keys differ from NOAA's own docs prose (e.g. datums not
|
|
9
|
+
* datumList, HarmonicConstituents not harconList) — parse defensively.
|
|
10
|
+
* - 404s are bare (no JSON body), unlike the Data API's structured errors.
|
|
11
|
+
*/
|
|
12
|
+
import type { UnitSystem } from "../format/units.js";
|
|
13
|
+
export declare const STATION_TYPES: readonly ["waterlevels", "historicwl", "met", "waterlevelsandmet", "tidepredictions", "harcon", "datums", "supersededdatums", "benchmarks", "supersededbenchmarks", "currents", "historiccurrents", "surveycurrents", "currentpredictions", "cond", "watertemp", "physocean", "tcoon", "1minute", "airgap", "visibility", "highwater", "lowwater"];
|
|
14
|
+
export type StationType = (typeof STATION_TYPES)[number];
|
|
15
|
+
export interface StationSummary {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
lat: number;
|
|
19
|
+
lng: number;
|
|
20
|
+
state?: string;
|
|
21
|
+
affiliations?: string;
|
|
22
|
+
portscode?: string | null;
|
|
23
|
+
tideType?: string;
|
|
24
|
+
tidal?: boolean;
|
|
25
|
+
greatlakes?: boolean;
|
|
26
|
+
type?: string;
|
|
27
|
+
reference_id?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
interface StationListResponse {
|
|
31
|
+
count: number;
|
|
32
|
+
units: string | null;
|
|
33
|
+
stations: StationSummary[];
|
|
34
|
+
}
|
|
35
|
+
/** Cached fetch of the full station directory for one station type. */
|
|
36
|
+
export declare function listStations(type: StationType | undefined, units: UnitSystem): Promise<StationListResponse>;
|
|
37
|
+
export interface StationSearchFilters {
|
|
38
|
+
type?: StationType;
|
|
39
|
+
name?: string;
|
|
40
|
+
state?: string;
|
|
41
|
+
units: UnitSystem;
|
|
42
|
+
}
|
|
43
|
+
/** Client-side filtered station search over the cached directory. */
|
|
44
|
+
export declare function searchStations(filters: StationSearchFilters): Promise<StationSummary[]>;
|
|
45
|
+
export declare function haversineKm(lat1: number, lon1: number, lat2: number, lon2: number): number;
|
|
46
|
+
export interface NearbyStation extends StationSummary {
|
|
47
|
+
distance_km: number;
|
|
48
|
+
distance_mi: number;
|
|
49
|
+
}
|
|
50
|
+
/** Nearest stations to an arbitrary point (MDAPI has no native lat/lon search). */
|
|
51
|
+
export declare function findNearestStations(lat: number, lon: number, type: StationType | undefined, limit: number, maxDistanceKm?: number): Promise<NearbyStation[]>;
|
|
52
|
+
/** Single-station overview, optionally expanding sub-resources inline. */
|
|
53
|
+
export declare function getStationInfo(stationId: string, expand: string[] | undefined, units: UnitSystem): Promise<Record<string, unknown>>;
|
|
54
|
+
/** Generic station sub-resource fetch (datums, harcon, tidepredoffsets, ...). */
|
|
55
|
+
export declare function getStationResource<T = Record<string, unknown>>(stationId: string, resource: string, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Defensive array extraction: MDAPI's real key names differ from its docs
|
|
58
|
+
* (observed live: `datums`, `HarmonicConstituents`, `sensors`, `bins`,
|
|
59
|
+
* `notices`, `products` — docs claim `datumList`, `harconList`, ...).
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractList<T = Record<string, unknown>>(payload: Record<string, unknown>, ...candidateKeys: string[]): T[];
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOAA CO-OPS Metadata API (MDAPI) service.
|
|
3
|
+
*
|
|
4
|
+
* Notes from live verification of the API (2026-07):
|
|
5
|
+
* - The stations list endpoint IGNORES lat/lon/radius query params, so
|
|
6
|
+
* "find stations near a point" is implemented client-side with a cached
|
|
7
|
+
* station directory and Haversine distance.
|
|
8
|
+
* - Response array keys differ from NOAA's own docs prose (e.g. datums not
|
|
9
|
+
* datumList, HarmonicConstituents not harconList) — parse defensively.
|
|
10
|
+
* - 404s are bare (no JSON body), unlike the Data API's structured errors.
|
|
11
|
+
*/
|
|
12
|
+
import { fetchMetadataApi } from "../client/http.js";
|
|
13
|
+
import { cache } from "../client/cache.js";
|
|
14
|
+
import { CACHE_TTL } from "../constants.js";
|
|
15
|
+
export const STATION_TYPES = [
|
|
16
|
+
"waterlevels",
|
|
17
|
+
"historicwl",
|
|
18
|
+
"met",
|
|
19
|
+
"waterlevelsandmet",
|
|
20
|
+
"tidepredictions",
|
|
21
|
+
"harcon",
|
|
22
|
+
"datums",
|
|
23
|
+
"supersededdatums",
|
|
24
|
+
"benchmarks",
|
|
25
|
+
"supersededbenchmarks",
|
|
26
|
+
"currents",
|
|
27
|
+
"historiccurrents",
|
|
28
|
+
"surveycurrents",
|
|
29
|
+
"currentpredictions",
|
|
30
|
+
"cond",
|
|
31
|
+
"watertemp",
|
|
32
|
+
"physocean",
|
|
33
|
+
"tcoon",
|
|
34
|
+
"1minute",
|
|
35
|
+
"airgap",
|
|
36
|
+
"visibility",
|
|
37
|
+
"highwater",
|
|
38
|
+
"lowwater",
|
|
39
|
+
];
|
|
40
|
+
/** Cached fetch of the full station directory for one station type. */
|
|
41
|
+
export async function listStations(type, units) {
|
|
42
|
+
const key = `stations:${type ?? "all"}:${units}`;
|
|
43
|
+
return cache.getOrLoad(key, CACHE_TTL.stationList, () => fetchMetadataApi("/stations.json", { type, units }));
|
|
44
|
+
}
|
|
45
|
+
/** Client-side filtered station search over the cached directory. */
|
|
46
|
+
export async function searchStations(filters) {
|
|
47
|
+
const { stations } = await listStations(filters.type, filters.units);
|
|
48
|
+
const nameNeedle = filters.name?.toLowerCase();
|
|
49
|
+
const stateNeedle = filters.state?.toLowerCase();
|
|
50
|
+
return stations.filter((s) => {
|
|
51
|
+
if (nameNeedle && !s.name?.toLowerCase().includes(nameNeedle))
|
|
52
|
+
return false;
|
|
53
|
+
if (stateNeedle && (s.state ?? "").toLowerCase() !== stateNeedle)
|
|
54
|
+
return false;
|
|
55
|
+
return true;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const EARTH_RADIUS_KM = 6371;
|
|
59
|
+
export function haversineKm(lat1, lon1, lat2, lon2) {
|
|
60
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
61
|
+
const dLat = toRad(lat2 - lat1);
|
|
62
|
+
const dLon = toRad(lon2 - lon1);
|
|
63
|
+
const a = Math.sin(dLat / 2) ** 2 +
|
|
64
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
|
65
|
+
return 2 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(a));
|
|
66
|
+
}
|
|
67
|
+
/** Nearest stations to an arbitrary point (MDAPI has no native lat/lon search). */
|
|
68
|
+
export async function findNearestStations(lat, lon, type, limit, maxDistanceKm) {
|
|
69
|
+
const { stations } = await listStations(type, "english");
|
|
70
|
+
const ranked = stations
|
|
71
|
+
.filter((s) => typeof s.lat === "number" && typeof s.lng === "number")
|
|
72
|
+
.map((s) => {
|
|
73
|
+
const distance_km = haversineKm(lat, lon, s.lat, s.lng);
|
|
74
|
+
return { ...s, distance_km, distance_mi: distance_km * 0.621371 };
|
|
75
|
+
})
|
|
76
|
+
.filter((s) => maxDistanceKm === undefined || s.distance_km <= maxDistanceKm)
|
|
77
|
+
.sort((a, b) => a.distance_km - b.distance_km);
|
|
78
|
+
return ranked.slice(0, limit);
|
|
79
|
+
}
|
|
80
|
+
/** Single-station overview, optionally expanding sub-resources inline. */
|
|
81
|
+
export async function getStationInfo(stationId, expand, units) {
|
|
82
|
+
const key = `station:${stationId}:${units}:${expand?.slice().sort().join(",") ?? ""}`;
|
|
83
|
+
return cache.getOrLoad(key, CACHE_TTL.stationResource, () => fetchMetadataApi(`/stations/${encodeURIComponent(stationId)}.json`, {
|
|
84
|
+
units,
|
|
85
|
+
expand: expand?.length ? expand.join(",") : undefined,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
/** Generic station sub-resource fetch (datums, harcon, tidepredoffsets, ...). */
|
|
89
|
+
export async function getStationResource(stationId, resource, params = {}) {
|
|
90
|
+
const key = `resource:${stationId}:${resource}:${JSON.stringify(params)}`;
|
|
91
|
+
return cache.getOrLoad(key, CACHE_TTL.stationResource, () => fetchMetadataApi(`/stations/${encodeURIComponent(stationId)}/${resource}.json`, params));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Defensive array extraction: MDAPI's real key names differ from its docs
|
|
95
|
+
* (observed live: `datums`, `HarmonicConstituents`, `sensors`, `bins`,
|
|
96
|
+
* `notices`, `products` — docs claim `datumList`, `harconList`, ...).
|
|
97
|
+
*/
|
|
98
|
+
export function extractList(payload, ...candidateKeys) {
|
|
99
|
+
for (const key of candidateKeys) {
|
|
100
|
+
const value = payload[key];
|
|
101
|
+
if (Array.isArray(value))
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MoonPhaseParams, MoonPhasesRangeParams, NextMoonPhaseParams } from
|
|
2
|
-
import { MoonPhaseInfo } from
|
|
1
|
+
import { MoonPhaseParams, MoonPhasesRangeParams, NextMoonPhaseParams } from "../interfaces/moon.js";
|
|
2
|
+
import { MoonPhaseInfo } from "../types/moon.js";
|
|
3
3
|
/**
|
|
4
4
|
* Service for moon phase calculations
|
|
5
5
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import SunCalc from
|
|
2
|
-
import { MoonPhaseName } from
|
|
1
|
+
import SunCalc from "suncalc";
|
|
2
|
+
import { MoonPhaseName } from "../types/moon.js";
|
|
3
3
|
/**
|
|
4
4
|
* Service for moon phase calculations
|
|
5
5
|
*/
|
|
@@ -27,14 +27,14 @@ export class MoonPhaseService {
|
|
|
27
27
|
// Calculate apparent diameter (in degrees)
|
|
28
28
|
const diameter = 0.5181 * (384400 / positionData.distance);
|
|
29
29
|
return {
|
|
30
|
-
date: date.toISOString().split(
|
|
30
|
+
date: date.toISOString().split("T")[0],
|
|
31
31
|
phase: illuminationData.phase,
|
|
32
32
|
phaseName,
|
|
33
33
|
illumination: illuminationData.fraction,
|
|
34
34
|
age,
|
|
35
35
|
distance: positionData.distance,
|
|
36
36
|
diameter,
|
|
37
|
-
isWaxing
|
|
37
|
+
isWaxing,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
@@ -46,18 +46,18 @@ export class MoonPhaseService {
|
|
|
46
46
|
const startDate = new Date(params.start_date);
|
|
47
47
|
const endDate = new Date(params.end_date);
|
|
48
48
|
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
|
49
|
-
throw new Error(
|
|
49
|
+
throw new Error("Invalid date format. Please use YYYY-MM-DD format.");
|
|
50
50
|
}
|
|
51
51
|
if (startDate > endDate) {
|
|
52
|
-
throw new Error(
|
|
52
|
+
throw new Error("Start date must be before end date.");
|
|
53
53
|
}
|
|
54
54
|
const result = [];
|
|
55
55
|
const currentDate = new Date(startDate);
|
|
56
56
|
while (currentDate <= endDate) {
|
|
57
57
|
result.push(this.getMoonPhase({
|
|
58
|
-
date: currentDate.toISOString().split(
|
|
58
|
+
date: currentDate.toISOString().split("T")[0],
|
|
59
59
|
latitude: params.latitude,
|
|
60
|
-
longitude: params.longitude
|
|
60
|
+
longitude: params.longitude,
|
|
61
61
|
}));
|
|
62
62
|
// Move to next day
|
|
63
63
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
@@ -78,34 +78,32 @@ export class MoonPhaseService {
|
|
|
78
78
|
[MoonPhaseName.NEW_MOON]: 0,
|
|
79
79
|
[MoonPhaseName.FIRST_QUARTER]: 0.25,
|
|
80
80
|
[MoonPhaseName.FULL_MOON]: 0.5,
|
|
81
|
-
[MoonPhaseName.LAST_QUARTER]: 0.75
|
|
81
|
+
[MoonPhaseName.LAST_QUARTER]: 0.75,
|
|
82
82
|
};
|
|
83
83
|
const targetPhaseValue = phaseValues[targetPhase];
|
|
84
84
|
const results = [];
|
|
85
85
|
let currentDate = new Date(startDate);
|
|
86
|
-
//
|
|
86
|
+
// Signed circular offset of a phase from the target, in [-0.5, 0.5).
|
|
87
|
+
// The lunar phase is cyclic (0 and 1 are both new moon), so plain
|
|
88
|
+
// |phase - target| breaks at the wraparound.
|
|
89
|
+
const signedDelta = (phase) => ((phase - targetPhaseValue + 1.5) % 1) - 0.5;
|
|
90
|
+
// The phase advances ~0.034/day, so the target is hit on the day where
|
|
91
|
+
// the signed offset crosses from negative to non-negative.
|
|
87
92
|
while (results.length < count) {
|
|
88
|
-
// Check every day (could be optimized with better algorithms)
|
|
89
|
-
const illuminationData = SunCalc.getMoonIllumination(currentDate);
|
|
90
93
|
const prevDate = new Date(currentDate);
|
|
91
94
|
prevDate.setDate(prevDate.getDate() - 1);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const currentDiff = Math.abs(illuminationData.phase - targetPhaseValue);
|
|
96
|
-
// If we're getting closer to the target phase and then further away, we've passed it
|
|
97
|
-
// Or if we're very close to the target phase (within 0.01)
|
|
98
|
-
if ((prevDiff > currentDiff && currentDiff < 0.01) || currentDiff < 0.005) {
|
|
95
|
+
const prev = signedDelta(SunCalc.getMoonIllumination(prevDate).phase);
|
|
96
|
+
const curr = signedDelta(SunCalc.getMoonIllumination(currentDate).phase);
|
|
97
|
+
if (prev < 0 && curr >= 0) {
|
|
99
98
|
results.push({
|
|
100
|
-
date: currentDate.toISOString().split(
|
|
101
|
-
phase: targetPhase
|
|
99
|
+
date: currentDate.toISOString().split("T")[0],
|
|
100
|
+
phase: targetPhase,
|
|
102
101
|
});
|
|
103
102
|
}
|
|
104
|
-
// Move to next day
|
|
105
103
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
throw new Error(
|
|
104
|
+
if (currentDate.getTime() - startDate.getTime() >
|
|
105
|
+
(count + 1) * 31 * 24 * 60 * 60 * 1000) {
|
|
106
|
+
throw new Error("Could not find the specified moon phase within the expected window.");
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
return results;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SunTimesParams, SunTimesRangeParams, SunPositionParams, NextSunEventParams } from
|
|
2
|
-
import { SunTimesInfo, SunPositionInfo } from
|
|
1
|
+
import { SunTimesParams, SunTimesRangeParams, SunPositionParams, NextSunEventParams } from "../interfaces/sun.js";
|
|
2
|
+
import { SunTimesInfo, SunPositionInfo } from "../types/sun.js";
|
|
3
3
|
/**
|
|
4
4
|
* Service for sun calculations
|
|
5
5
|
*/
|