@ryancardin/noaa-tides-currents-mcp-server 1.0.0

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 (91) hide show
  1. package/.claude/settings.local.json +29 -0
  2. package/CLAUDE.md +71 -0
  3. package/Dockerfile +14 -0
  4. package/LICENSE +21 -0
  5. package/README.md +234 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +9 -0
  8. package/dist/interfaces/config.d.ts +6 -0
  9. package/dist/interfaces/config.js +1 -0
  10. package/dist/interfaces/moon.d.ts +65 -0
  11. package/dist/interfaces/moon.js +35 -0
  12. package/dist/interfaces/noaa.d.ts +334 -0
  13. package/dist/interfaces/noaa.js +98 -0
  14. package/dist/interfaces/parameters.d.ts +18 -0
  15. package/dist/interfaces/parameters.js +5 -0
  16. package/dist/interfaces/sun.d.ts +103 -0
  17. package/dist/interfaces/sun.js +45 -0
  18. package/dist/mcp-server.d.ts +12 -0
  19. package/dist/mcp-server.js +103 -0
  20. package/dist/moon-phase-service.d.ts +122 -0
  21. package/dist/moon-phase-service.js +187 -0
  22. package/dist/noaa-service.d.ts +60 -0
  23. package/dist/noaa-service.js +159 -0
  24. package/dist/schemas/common.d.ts +14 -0
  25. package/dist/schemas/common.js +18 -0
  26. package/dist/schemas/dpapi.d.ts +198 -0
  27. package/dist/schemas/dpapi.js +89 -0
  28. package/dist/server/config.d.ts +9 -0
  29. package/dist/server/config.js +40 -0
  30. package/dist/server/mcp-server.d.ts +12 -0
  31. package/dist/server/mcp-server.js +103 -0
  32. package/dist/services/dpapi-service.d.ts +72 -0
  33. package/dist/services/dpapi-service.js +164 -0
  34. package/dist/services/moon-phase-service.d.ts +34 -0
  35. package/dist/services/moon-phase-service.js +147 -0
  36. package/dist/services/noaa-parameters-service.d.ts +76 -0
  37. package/dist/services/noaa-parameters-service.js +128 -0
  38. package/dist/services/noaa-service.d.ts +52 -0
  39. package/dist/services/noaa-service.js +151 -0
  40. package/dist/services/sun-service.d.ts +60 -0
  41. package/dist/services/sun-service.js +234 -0
  42. package/dist/sun-service.d.ts +184 -0
  43. package/dist/sun-service.js +218 -0
  44. package/dist/tools/derived-product-tools.d.ts +6 -0
  45. package/dist/tools/derived-product-tools.js +168 -0
  46. package/dist/tools/index.d.ts +16 -0
  47. package/dist/tools/index.js +36 -0
  48. package/dist/tools/moon-tools.d.ts +6 -0
  49. package/dist/tools/moon-tools.js +69 -0
  50. package/dist/tools/parameter-tools.d.ts +6 -0
  51. package/dist/tools/parameter-tools.js +77 -0
  52. package/dist/tools/station-tools.d.ts +6 -0
  53. package/dist/tools/station-tools.js +51 -0
  54. package/dist/tools/sun-tools.d.ts +6 -0
  55. package/dist/tools/sun-tools.js +109 -0
  56. package/dist/tools/water-tools.d.ts +6 -0
  57. package/dist/tools/water-tools.js +150 -0
  58. package/dist/types/moon.d.ts +26 -0
  59. package/dist/types/moon.js +14 -0
  60. package/dist/types/sun.d.ts +49 -0
  61. package/dist/types/sun.js +19 -0
  62. package/dist/types.d.ts +337 -0
  63. package/dist/types.js +98 -0
  64. package/package.json +56 -0
  65. package/smithery.yaml +16 -0
  66. package/src/index.ts +13 -0
  67. package/src/interfaces/moon.ts +44 -0
  68. package/src/interfaces/noaa.ts +130 -0
  69. package/src/interfaces/parameters.ts +20 -0
  70. package/src/interfaces/sun.ts +57 -0
  71. package/src/schemas/common.ts +23 -0
  72. package/src/schemas/dpapi.ts +99 -0
  73. package/src/server/config.ts +43 -0
  74. package/src/server/mcp-server.ts +135 -0
  75. package/src/services/dpapi-service.ts +187 -0
  76. package/src/services/moon-phase-service.ts +167 -0
  77. package/src/services/noaa-parameters-service.ts +139 -0
  78. package/src/services/noaa-service.ts +171 -0
  79. package/src/services/sun-service.ts +275 -0
  80. package/src/tools/derived-product-tools.ts +180 -0
  81. package/src/tools/index.ts +40 -0
  82. package/src/tools/moon-tools.ts +79 -0
  83. package/src/tools/parameter-tools.ts +82 -0
  84. package/src/tools/station-tools.ts +57 -0
  85. package/src/tools/sun-tools.ts +120 -0
  86. package/src/tools/water-tools.ts +166 -0
  87. package/src/types/moon.ts +27 -0
  88. package/src/types/sun.ts +51 -0
  89. package/src/types/suncalc.d.ts +110 -0
  90. package/test-dpapi.js +0 -0
  91. package/tsconfig.json +15 -0
@@ -0,0 +1,187 @@
1
+ import axios from 'axios';
2
+
3
+ // Base URL for the NOAA Derived Product API
4
+ const DPAPI_BASE_URL = 'https://api.tidesandcurrents.noaa.gov/dpapi/prod';
5
+
6
+ /**
7
+ * Service for interacting with NOAA Derived Product API (DPAPI)
8
+ */
9
+ export class DpapiService {
10
+
11
+ /**
12
+ * Build parameters for the API request
13
+ * @param params Parameters for the request
14
+ * @returns URL-encoded parameters string
15
+ */
16
+ private buildParams(params: Record<string, any>): string {
17
+ // Remove undefined and null values
18
+ const filteredParams = Object.entries(params)
19
+ .filter(([_, value]) => value !== undefined && value !== null)
20
+ .reduce((acc, [key, value]) => {
21
+ acc[key] = value;
22
+ return acc;
23
+ }, {} as Record<string, any>);
24
+
25
+ // Convert to URL parameters
26
+ return new URLSearchParams(filteredParams as Record<string, string>).toString();
27
+ }
28
+
29
+ /**
30
+ * Make a request to the DPAPI
31
+ * @param endpoint Endpoint path
32
+ * @param params Parameters for the request
33
+ * @returns Response data
34
+ */
35
+ async fetchDpapi(endpoint: string, params: Record<string, any> = {}): Promise<any> {
36
+ try {
37
+ const queryParams = this.buildParams(params);
38
+ const url = `${DPAPI_BASE_URL}${endpoint}${queryParams ? '?' + queryParams : ''}`;
39
+
40
+ const response = await axios.get(url);
41
+ return response.data;
42
+ } catch (error) {
43
+ if (axios.isAxiosError(error) && error.response) {
44
+ throw new Error(`NOAA DPAPI Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get sea level trends for a station
52
+ * @param params Parameters including station ID and affiliation
53
+ * @returns Sea level trend data
54
+ */
55
+ async getSeaLevelTrends(params: Record<string, any>): Promise<any> {
56
+ const { station, affil = 'Global', format = 'json', ...rest } = params;
57
+
58
+ return this.fetchDpapi('/sltrends', {
59
+ station,
60
+ affil,
61
+ format,
62
+ ...rest
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Get extreme water levels for a station
68
+ * @param params Parameters including station ID and units
69
+ * @returns Extreme water level data
70
+ */
71
+ async getExtremeWaterLevels(params: Record<string, any>): Promise<any> {
72
+ const { station, units = 'english', format = 'json', ...rest } = params;
73
+
74
+ return this.fetchDpapi('/ewl', {
75
+ station,
76
+ units,
77
+ format,
78
+ ...rest
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Get high tide flooding daily count data
84
+ * @param params Parameters including station ID, date range, and thresholds
85
+ * @returns Daily flood count data
86
+ */
87
+ async getHighTideFloodingDaily(params: Record<string, any>): Promise<any> {
88
+ const { station, format = 'json', ...rest } = params;
89
+
90
+ return this.fetchDpapi('/htf/daily', {
91
+ station,
92
+ format,
93
+ ...rest
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Get high tide flooding monthly count data
99
+ * @param params Parameters including station ID, date range, and thresholds
100
+ * @returns Monthly flood count data
101
+ */
102
+ async getHighTideFloodingMonthly(params: Record<string, any>): Promise<any> {
103
+ const { station, format = 'json', ...rest } = params;
104
+
105
+ return this.fetchDpapi('/htf/monthly', {
106
+ station,
107
+ format,
108
+ ...rest
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Get high tide flooding seasonal count data
114
+ * @param params Parameters including station ID, seasons, and thresholds
115
+ * @returns Seasonal flood count data
116
+ */
117
+ async getHighTideFloodingSeasonal(params: Record<string, any>): Promise<any> {
118
+ const { station, format = 'json', ...rest } = params;
119
+
120
+ return this.fetchDpapi('/htf/seasonal', {
121
+ station,
122
+ format,
123
+ ...rest
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Get high tide flooding annual count data
129
+ * @param params Parameters including station ID, year range, and thresholds
130
+ * @returns Annual flood count data
131
+ */
132
+ async getHighTideFloodingAnnual(params: Record<string, any>): Promise<any> {
133
+ const { station, format = 'json', ...rest } = params;
134
+
135
+ return this.fetchDpapi('/htf/annual', {
136
+ station,
137
+ format,
138
+ ...rest
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Get high tide flooding decadal projections
144
+ * @param params Parameters including station ID, scenario, and decade
145
+ * @returns Decadal projection data
146
+ */
147
+ async getHighTideFloodingProjections(params: Record<string, any>): Promise<any> {
148
+ const { station, scenario = 'all', format = 'json', ...rest } = params;
149
+
150
+ return this.fetchDpapi('/htf/projections', {
151
+ station,
152
+ scenario,
153
+ format,
154
+ ...rest
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Get high tide flooding daily likelihoods
160
+ * @param params Parameters including station ID and date
161
+ * @returns Daily likelihood data
162
+ */
163
+ async getHighTideFloodingLikelihoods(params: Record<string, any>): Promise<any> {
164
+ const { station, format = 'json', ...rest } = params;
165
+
166
+ return this.fetchDpapi('/htf/likelihoods', {
167
+ station,
168
+ format,
169
+ ...rest
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Get top ten water levels for a station
175
+ * @param params Parameters including station ID and analysis type
176
+ * @returns Top ten water level data
177
+ */
178
+ async getTopTenWaterLevels(params: Record<string, any>): Promise<any> {
179
+ const { station, format = 'json', ...rest } = params;
180
+
181
+ return this.fetchDpapi('/topten', {
182
+ station,
183
+ format,
184
+ ...rest
185
+ });
186
+ }
187
+ }
@@ -0,0 +1,167 @@
1
+ import SunCalc from 'suncalc';
2
+ import { MoonPhaseParams, MoonPhasesRangeParams, NextMoonPhaseParams } from '../interfaces/moon.js';
3
+ import { MoonPhaseName, MoonPhaseInfo } from '../types/moon.js';
4
+
5
+ /**
6
+ * Service for moon phase calculations
7
+ */
8
+ export class MoonPhaseService {
9
+ /**
10
+ * Get the moon phase for a specific date
11
+ * @param params Parameters for the request
12
+ * @returns Moon phase information
13
+ */
14
+ getMoonPhase(params: MoonPhaseParams): MoonPhaseInfo {
15
+ const date = params.date ? new Date(params.date) : new Date();
16
+
17
+ // Get moon illumination data
18
+ const illuminationData = SunCalc.getMoonIllumination(date);
19
+
20
+ // Get moon position data (requires location)
21
+ const latitude = params.latitude ?? 0;
22
+ const longitude = params.longitude ?? 0;
23
+ const positionData = SunCalc.getMoonPosition(date, latitude, longitude);
24
+
25
+ // Calculate moon phase name
26
+ const phaseName = this.getMoonPhaseName(illuminationData.phase);
27
+
28
+ // Calculate if the moon is waxing (increasing illumination)
29
+ const isWaxing = illuminationData.phase < 0.5;
30
+
31
+ // Calculate approximate moon age (0-29.53 days)
32
+ const lunarMonth = 29.53; // days
33
+ const age = illuminationData.phase * lunarMonth;
34
+
35
+ // Calculate apparent diameter (in degrees)
36
+ const diameter = 0.5181 * (384400 / positionData.distance);
37
+
38
+ return {
39
+ date: date.toISOString().split('T')[0],
40
+ phase: illuminationData.phase,
41
+ phaseName,
42
+ illumination: illuminationData.fraction,
43
+ age,
44
+ distance: positionData.distance,
45
+ diameter,
46
+ isWaxing
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Get moon phases for a date range
52
+ * @param params Parameters for the request
53
+ * @returns Array of moon phase information
54
+ */
55
+ getMoonPhasesRange(params: MoonPhasesRangeParams): MoonPhaseInfo[] {
56
+ const startDate = new Date(params.start_date);
57
+ const endDate = new Date(params.end_date);
58
+
59
+ if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
60
+ throw new Error('Invalid date format. Please use YYYY-MM-DD format.');
61
+ }
62
+
63
+ if (startDate > endDate) {
64
+ throw new Error('Start date must be before end date.');
65
+ }
66
+
67
+ const result: MoonPhaseInfo[] = [];
68
+ const currentDate = new Date(startDate);
69
+
70
+ while (currentDate <= endDate) {
71
+ result.push(this.getMoonPhase({
72
+ date: currentDate.toISOString().split('T')[0],
73
+ latitude: params.latitude,
74
+ longitude: params.longitude
75
+ }));
76
+
77
+ // Move to next day
78
+ currentDate.setDate(currentDate.getDate() + 1);
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * Get the next occurrence(s) of a specific moon phase
86
+ * @param params Parameters for the request
87
+ * @returns Array of dates for the next occurrences of the specified phase
88
+ */
89
+ getNextMoonPhase(params: NextMoonPhaseParams): { date: string, phase: string }[] {
90
+ const startDate = params.date ? new Date(params.date) : new Date();
91
+ const count = params.count || 1;
92
+ const targetPhase = params.phase;
93
+
94
+ // Map phase names to their approximate values
95
+ const phaseValues: Record<string, number> = {
96
+ [MoonPhaseName.NEW_MOON]: 0,
97
+ [MoonPhaseName.FIRST_QUARTER]: 0.25,
98
+ [MoonPhaseName.FULL_MOON]: 0.5,
99
+ [MoonPhaseName.LAST_QUARTER]: 0.75
100
+ };
101
+
102
+ const targetPhaseValue = phaseValues[targetPhase];
103
+ const results: { date: string, phase: string }[] = [];
104
+ let currentDate = new Date(startDate);
105
+
106
+ // Find the next occurrences
107
+ while (results.length < count) {
108
+ // Check every day (could be optimized with better algorithms)
109
+ const illuminationData = SunCalc.getMoonIllumination(currentDate);
110
+ const prevDate = new Date(currentDate);
111
+ prevDate.setDate(prevDate.getDate() - 1);
112
+ const prevIlluminationData = SunCalc.getMoonIllumination(prevDate);
113
+
114
+ // Check if we've passed the target phase between yesterday and today
115
+ const prevDiff = Math.abs(prevIlluminationData.phase - targetPhaseValue);
116
+ const currentDiff = Math.abs(illuminationData.phase - targetPhaseValue);
117
+
118
+ // If we're getting closer to the target phase and then further away, we've passed it
119
+ // Or if we're very close to the target phase (within 0.01)
120
+ if ((prevDiff > currentDiff && currentDiff < 0.01) || currentDiff < 0.005) {
121
+ results.push({
122
+ date: currentDate.toISOString().split('T')[0],
123
+ phase: targetPhase
124
+ });
125
+ }
126
+
127
+ // Move to next day
128
+ currentDate.setDate(currentDate.getDate() + 1);
129
+
130
+ // Safety check to prevent infinite loops
131
+ if (results.length === 0 && currentDate.getTime() - startDate.getTime() > 366 * 24 * 60 * 60 * 1000) {
132
+ throw new Error('Could not find the specified moon phase within a year.');
133
+ }
134
+ }
135
+
136
+ return results;
137
+ }
138
+
139
+ /**
140
+ * Get the moon phase name based on the phase value (0-1)
141
+ * @param phase Phase value (0-1)
142
+ * @returns Moon phase name
143
+ */
144
+ private getMoonPhaseName(phase: number): MoonPhaseName {
145
+ // Normalize phase to 0-1 range
146
+ const normalizedPhase = phase < 0 ? phase + 1 : phase > 1 ? phase - 1 : phase;
147
+
148
+ // Determine moon phase based on the value
149
+ if (normalizedPhase < 0.0625 || normalizedPhase >= 0.9375) {
150
+ return MoonPhaseName.NEW_MOON;
151
+ } else if (normalizedPhase < 0.1875) {
152
+ return MoonPhaseName.WAXING_CRESCENT;
153
+ } else if (normalizedPhase < 0.3125) {
154
+ return MoonPhaseName.FIRST_QUARTER;
155
+ } else if (normalizedPhase < 0.4375) {
156
+ return MoonPhaseName.WAXING_GIBBOUS;
157
+ } else if (normalizedPhase < 0.5625) {
158
+ return MoonPhaseName.FULL_MOON;
159
+ } else if (normalizedPhase < 0.6875) {
160
+ return MoonPhaseName.WANING_GIBBOUS;
161
+ } else if (normalizedPhase < 0.8125) {
162
+ return MoonPhaseName.LAST_QUARTER;
163
+ } else {
164
+ return MoonPhaseName.WANING_CRESCENT;
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,139 @@
1
+ import axios from 'axios';
2
+
3
+ /**
4
+ * Provides information about valid NOAA Tides and Currents API parameters
5
+ */
6
+ export class NoaaParametersService {
7
+ /**
8
+ * Get valid time zone values
9
+ */
10
+ getTimeZones(): { id: string, description: string }[] {
11
+ return [
12
+ { id: 'gmt', description: 'Greenwich Mean Time' },
13
+ { id: 'lst', description: 'Local Standard Time' },
14
+ { id: 'lst_ldt', description: 'Local Standard/Local Daylight Time' }
15
+ ];
16
+ }
17
+
18
+ /**
19
+ * Get valid datum values
20
+ */
21
+ getDatums(): { id: string, description: string }[] {
22
+ return [
23
+ { id: 'MHHW', description: 'Mean Higher High Water' },
24
+ { id: 'MHW', description: 'Mean High Water' },
25
+ { id: 'MTL', description: 'Mean Tide Level' },
26
+ { id: 'MSL', description: 'Mean Sea Level' },
27
+ { id: 'MLW', description: 'Mean Low Water' },
28
+ { id: 'MLLW', description: 'Mean Lower Low Water' },
29
+ { id: 'NAVD', description: 'North American Vertical Datum' },
30
+ { id: 'STND', description: 'Station Datum' }
31
+ ];
32
+ }
33
+
34
+ /**
35
+ * Get valid units
36
+ */
37
+ getUnits(): { id: string, description: string }[] {
38
+ return [
39
+ { id: 'english', description: 'English units (feet, mph, etc.)' },
40
+ { id: 'metric', description: 'Metric units (meters, kph, etc.)' }
41
+ ];
42
+ }
43
+
44
+ /**
45
+ * Get valid intervals for tide predictions
46
+ */
47
+ getTidePredictionIntervals(): { id: string, description: string }[] {
48
+ return [
49
+ { id: 'hilo', description: 'High/low tide predictions only' },
50
+ { id: 'h', description: 'Hourly predictions' },
51
+ { id: '6', description: '6-minute predictions' },
52
+ { id: '30', description: '30-minute predictions' },
53
+ { id: '60', description: '60-minute predictions' }
54
+ ];
55
+ }
56
+
57
+ /**
58
+ * Get valid intervals for current predictions
59
+ */
60
+ getCurrentPredictionIntervals(): { id: string, description: string }[] {
61
+ return [
62
+ { id: 'MAX_SLACK', description: 'Maximum flood/ebb and slack predictions only' },
63
+ { id: '6', description: '6-minute predictions' },
64
+ { id: '30', description: '30-minute predictions' },
65
+ { id: '60', description: '60-minute predictions' }
66
+ ];
67
+ }
68
+
69
+ /**
70
+ * Get valid velocity types for current predictions
71
+ */
72
+ getVelocityTypes(): { id: string, description: string }[] {
73
+ return [
74
+ { id: 'default', description: 'Default velocity reporting (flood/ebb direction)' },
75
+ { id: 'speed_dir', description: 'Speed and direction format' }
76
+ ];
77
+ }
78
+
79
+ /**
80
+ * Get valid meteorological products
81
+ */
82
+ getMeteorologicalProducts(): { id: string, description: string }[] {
83
+ return [
84
+ { id: 'air_temperature', description: 'Air temperature' },
85
+ { id: 'water_temperature', description: 'Water temperature' },
86
+ { id: 'wind', description: 'Wind speed and direction' },
87
+ { id: 'air_pressure', description: 'Barometric pressure' },
88
+ { id: 'air_gap', description: 'Air gap (distance between bridge and water surface)' },
89
+ { id: 'conductivity', description: 'Conductivity' },
90
+ { id: 'visibility', description: 'Visibility' },
91
+ { id: 'humidity', description: 'Relative humidity' },
92
+ { id: 'salinity', description: 'Salinity' },
93
+ { id: 'hourly_height', description: 'Verified hourly height water level' },
94
+ { id: 'high_low', description: 'Verified high/low water level' },
95
+ { id: 'daily_mean', description: 'Verified daily mean water level' },
96
+ { id: 'monthly_mean', description: 'Verified monthly mean water level' },
97
+ { id: 'one_minute_water_level', description: 'One-minute water level data' },
98
+ { id: 'datums', description: 'Datums' }
99
+ ];
100
+ }
101
+
102
+ /**
103
+ * Get valid station types
104
+ */
105
+ getStationTypes(): { id: string, description: string }[] {
106
+ return [
107
+ { id: 'waterlevels', description: 'Water level stations' },
108
+ { id: 'currentpredictions', description: 'Current prediction stations' },
109
+ { id: 'currents', description: 'Current observation stations' },
110
+ { id: 'tidepredictions', description: 'Tide prediction stations' },
111
+ { id: 'weather', description: 'Weather stations' },
112
+ { id: 'ports', description: 'Physical Oceanographic Real-Time System (PORTS) stations' }
113
+ ];
114
+ }
115
+
116
+ /**
117
+ * Get valid date formats
118
+ */
119
+ getDateFormats(): { format: string, description: string, example: string }[] {
120
+ return [
121
+ { format: 'YYYYMMDD', description: 'Year, month, day without separators', example: '20230401' },
122
+ { format: 'MM/DD/YYYY', description: 'Month/day/year with separators', example: '04/01/2023' },
123
+ { format: 'today', description: 'Current date', example: 'today' },
124
+ { format: 'latest', description: 'Latest available data', example: 'latest' },
125
+ { format: 'recent', description: 'Most recent data', example: 'recent' }
126
+ ];
127
+ }
128
+
129
+ /**
130
+ * Get valid output formats
131
+ */
132
+ getOutputFormats(): { id: string, description: string }[] {
133
+ return [
134
+ { id: 'json', description: 'JSON format' },
135
+ { id: 'xml', description: 'XML format' },
136
+ { id: 'csv', description: 'CSV format (not available for all endpoints)' }
137
+ ];
138
+ }
139
+ }
@@ -0,0 +1,171 @@
1
+ import axios from 'axios';
2
+
3
+ // Base URLs for the different NOAA APIs
4
+ const DATA_API_BASE_URL = 'https://api.tidesandcurrents.noaa.gov/api/prod/datagetter';
5
+ const METADATA_API_BASE_URL = 'https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi';
6
+
7
+ /**
8
+ * Service for interacting with NOAA Tides and Currents APIs
9
+ */
10
+ export class NoaaService {
11
+
12
+ /**
13
+ * Build parameters for the API request
14
+ * @param params Parameters for the request
15
+ * @returns URL-encoded parameters string
16
+ */
17
+ private buildParams(params: Record<string, any>): string {
18
+ // Remove undefined and null values
19
+ const filteredParams = Object.entries(params)
20
+ .filter(([_, value]) => value !== undefined && value !== null)
21
+ .reduce((acc, [key, value]) => {
22
+ acc[key] = value;
23
+ return acc;
24
+ }, {} as Record<string, any>);
25
+
26
+ // Convert to URL parameters
27
+ return new URLSearchParams(filteredParams as Record<string, string>).toString();
28
+ }
29
+
30
+ /**
31
+ * Make a request to the Data API
32
+ * @param params Parameters for the request
33
+ * @returns Response data
34
+ */
35
+ async fetchDataApi(params: Record<string, any>): Promise<any> {
36
+ try {
37
+ const queryParams = this.buildParams(params);
38
+ const url = `${DATA_API_BASE_URL}?${queryParams}`;
39
+
40
+ const response = await axios.get(url);
41
+ return response.data;
42
+ } catch (error) {
43
+ if (axios.isAxiosError(error) && error.response) {
44
+ throw new Error(`NOAA API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Make a request to the Metadata API
52
+ * @param endpoint Endpoint path
53
+ * @param params Parameters for the request
54
+ * @returns Response data
55
+ */
56
+ async fetchMetadataApi(endpoint: string, params: Record<string, any> = {}): Promise<any> {
57
+ try {
58
+ const queryParams = this.buildParams(params);
59
+ const url = `${METADATA_API_BASE_URL}${endpoint}${queryParams ? '?' + queryParams : ''}`;
60
+
61
+ const response = await axios.get(url);
62
+ return response.data;
63
+ } catch (error) {
64
+ if (axios.isAxiosError(error) && error.response) {
65
+ throw new Error(`NOAA API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get water level data
73
+ */
74
+ async getWaterLevels(params: Record<string, any>): Promise<any> {
75
+ return this.fetchDataApi({
76
+ ...params,
77
+ product: 'water_level'
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Get tide predictions
83
+ */
84
+ async getTidePredictions(params: Record<string, any>): Promise<any> {
85
+ return this.fetchDataApi({
86
+ ...params,
87
+ product: 'predictions'
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Get currents data
93
+ */
94
+ async getCurrents(params: Record<string, any>): Promise<any> {
95
+ return this.fetchDataApi({
96
+ ...params,
97
+ product: 'currents'
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Get current predictions
103
+ */
104
+ async getCurrentPredictions(params: Record<string, any>): Promise<any> {
105
+ return this.fetchDataApi({
106
+ ...params,
107
+ product: 'currents_predictions'
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Get meteorological data (air_temperature, wind, etc.)
113
+ */
114
+ async getMeteorologicalData(params: Record<string, any>): Promise<any> {
115
+ const { product, ...rest } = params;
116
+ return this.fetchDataApi({
117
+ ...rest,
118
+ product
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Get list of stations
124
+ */
125
+ async getStations(params: Record<string, any>): Promise<any> {
126
+ const {
127
+ type,
128
+ name,
129
+ lat_min,
130
+ lat_max,
131
+ lon_min,
132
+ lon_max,
133
+ state,
134
+ limit,
135
+ offset,
136
+ sort_by,
137
+ sort_order,
138
+ ...rest
139
+ } = params;
140
+
141
+ const endpoint = '/stations.' + (rest.format || 'json');
142
+
143
+ // Build query parameters with all the filters
144
+ const queryParams: Record<string, any> = { ...rest };
145
+
146
+ // Add filters only if they are defined
147
+ if (type) queryParams.type = type;
148
+ if (name) queryParams.name = name;
149
+ if (lat_min !== undefined) queryParams.lat_min = lat_min;
150
+ if (lat_max !== undefined) queryParams.lat_max = lat_max;
151
+ if (lon_min !== undefined) queryParams.lon_min = lon_min;
152
+ if (lon_max !== undefined) queryParams.lon_max = lon_max;
153
+ if (state) queryParams.state = state;
154
+ if (limit !== undefined) queryParams.limit = limit;
155
+ if (offset !== undefined) queryParams.offset = offset;
156
+ if (sort_by) queryParams.sort_by = sort_by;
157
+ if (sort_order) queryParams.sort_order = sort_order;
158
+
159
+ return this.fetchMetadataApi(endpoint, queryParams);
160
+ }
161
+
162
+ /**
163
+ * Get station details
164
+ */
165
+ async getStationDetails(params: Record<string, any>): Promise<any> {
166
+ const { station, ...rest } = params;
167
+ const endpoint = `/stations/${station}/details.` + (rest.format || 'json');
168
+
169
+ return this.fetchMetadataApi(endpoint, rest);
170
+ }
171
+ }