@jablum/weather-mcp 1.7.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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1319 -0
  3. package/dist/analytics/anonymizer.d.ts +37 -0
  4. package/dist/analytics/anonymizer.d.ts.map +1 -0
  5. package/dist/analytics/anonymizer.js +112 -0
  6. package/dist/analytics/anonymizer.js.map +1 -0
  7. package/dist/analytics/collector.d.ts +72 -0
  8. package/dist/analytics/collector.d.ts.map +1 -0
  9. package/dist/analytics/collector.js +282 -0
  10. package/dist/analytics/collector.js.map +1 -0
  11. package/dist/analytics/config.d.ts +15 -0
  12. package/dist/analytics/config.d.ts.map +1 -0
  13. package/dist/analytics/config.js +172 -0
  14. package/dist/analytics/config.js.map +1 -0
  15. package/dist/analytics/index.d.ts +8 -0
  16. package/dist/analytics/index.d.ts.map +1 -0
  17. package/dist/analytics/index.js +7 -0
  18. package/dist/analytics/index.js.map +1 -0
  19. package/dist/analytics/middleware.d.ts +33 -0
  20. package/dist/analytics/middleware.d.ts.map +1 -0
  21. package/dist/analytics/middleware.js +99 -0
  22. package/dist/analytics/middleware.js.map +1 -0
  23. package/dist/analytics/transport.d.ts +11 -0
  24. package/dist/analytics/transport.d.ts.map +1 -0
  25. package/dist/analytics/transport.js +92 -0
  26. package/dist/analytics/transport.js.map +1 -0
  27. package/dist/analytics/types.d.ts +74 -0
  28. package/dist/analytics/types.d.ts.map +1 -0
  29. package/dist/analytics/types.js +6 -0
  30. package/dist/analytics/types.js.map +1 -0
  31. package/dist/config/api.d.ts +30 -0
  32. package/dist/config/api.d.ts.map +1 -0
  33. package/dist/config/api.js +32 -0
  34. package/dist/config/api.js.map +1 -0
  35. package/dist/config/cache.d.ts +31 -0
  36. package/dist/config/cache.d.ts.map +1 -0
  37. package/dist/config/cache.js +108 -0
  38. package/dist/config/cache.js.map +1 -0
  39. package/dist/config/displayThresholds.d.ts +83 -0
  40. package/dist/config/displayThresholds.d.ts.map +1 -0
  41. package/dist/config/displayThresholds.js +83 -0
  42. package/dist/config/displayThresholds.js.map +1 -0
  43. package/dist/config/tools.d.ts +44 -0
  44. package/dist/config/tools.d.ts.map +1 -0
  45. package/dist/config/tools.js +269 -0
  46. package/dist/config/tools.js.map +1 -0
  47. package/dist/errors/ApiError.d.ts +62 -0
  48. package/dist/errors/ApiError.d.ts.map +1 -0
  49. package/dist/errors/ApiError.js +171 -0
  50. package/dist/errors/ApiError.js.map +1 -0
  51. package/dist/handlers/airQualityHandler.d.ts +11 -0
  52. package/dist/handlers/airQualityHandler.d.ts.map +1 -0
  53. package/dist/handlers/airQualityHandler.js +154 -0
  54. package/dist/handlers/airQualityHandler.js.map +1 -0
  55. package/dist/handlers/alertsHandler.d.ts +11 -0
  56. package/dist/handlers/alertsHandler.d.ts.map +1 -0
  57. package/dist/handlers/alertsHandler.js +98 -0
  58. package/dist/handlers/alertsHandler.js.map +1 -0
  59. package/dist/handlers/currentConditionsHandler.d.ts +13 -0
  60. package/dist/handlers/currentConditionsHandler.d.ts.map +1 -0
  61. package/dist/handlers/currentConditionsHandler.js +296 -0
  62. package/dist/handlers/currentConditionsHandler.js.map +1 -0
  63. package/dist/handlers/forecastHandler.d.ts +16 -0
  64. package/dist/handlers/forecastHandler.d.ts.map +1 -0
  65. package/dist/handlers/forecastHandler.js +454 -0
  66. package/dist/handlers/forecastHandler.js.map +1 -0
  67. package/dist/handlers/historicalWeatherHandler.d.ts +12 -0
  68. package/dist/handlers/historicalWeatherHandler.d.ts.map +1 -0
  69. package/dist/handlers/historicalWeatherHandler.js +188 -0
  70. package/dist/handlers/historicalWeatherHandler.js.map +1 -0
  71. package/dist/handlers/lightningHandler.d.ts +14 -0
  72. package/dist/handlers/lightningHandler.d.ts.map +1 -0
  73. package/dist/handlers/lightningHandler.js +258 -0
  74. package/dist/handlers/lightningHandler.js.map +1 -0
  75. package/dist/handlers/locationHandler.d.ts +12 -0
  76. package/dist/handlers/locationHandler.d.ts.map +1 -0
  77. package/dist/handlers/locationHandler.js +149 -0
  78. package/dist/handlers/locationHandler.js.map +1 -0
  79. package/dist/handlers/marineConditionsHandler.d.ts +13 -0
  80. package/dist/handlers/marineConditionsHandler.d.ts.map +1 -0
  81. package/dist/handlers/marineConditionsHandler.js +270 -0
  82. package/dist/handlers/marineConditionsHandler.js.map +1 -0
  83. package/dist/handlers/riverConditionsHandler.d.ts +11 -0
  84. package/dist/handlers/riverConditionsHandler.d.ts.map +1 -0
  85. package/dist/handlers/riverConditionsHandler.js +176 -0
  86. package/dist/handlers/riverConditionsHandler.js.map +1 -0
  87. package/dist/handlers/savedLocationsHandler.d.ts +50 -0
  88. package/dist/handlers/savedLocationsHandler.d.ts.map +1 -0
  89. package/dist/handlers/savedLocationsHandler.js +397 -0
  90. package/dist/handlers/savedLocationsHandler.js.map +1 -0
  91. package/dist/handlers/statusHandler.d.ts +12 -0
  92. package/dist/handlers/statusHandler.d.ts.map +1 -0
  93. package/dist/handlers/statusHandler.js +115 -0
  94. package/dist/handlers/statusHandler.js.map +1 -0
  95. package/dist/handlers/weatherImageryHandler.d.ts +14 -0
  96. package/dist/handlers/weatherImageryHandler.d.ts.map +1 -0
  97. package/dist/handlers/weatherImageryHandler.js +143 -0
  98. package/dist/handlers/weatherImageryHandler.js.map +1 -0
  99. package/dist/handlers/wildfireHandler.d.ts +11 -0
  100. package/dist/handlers/wildfireHandler.d.ts.map +1 -0
  101. package/dist/handlers/wildfireHandler.js +186 -0
  102. package/dist/handlers/wildfireHandler.js.map +1 -0
  103. package/dist/index.d.ts +7 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +735 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/services/blitzortung.d.ts +67 -0
  108. package/dist/services/blitzortung.d.ts.map +1 -0
  109. package/dist/services/blitzortung.js +475 -0
  110. package/dist/services/blitzortung.js.map +1 -0
  111. package/dist/services/geocoding.d.ts +57 -0
  112. package/dist/services/geocoding.d.ts.map +1 -0
  113. package/dist/services/geocoding.js +393 -0
  114. package/dist/services/geocoding.js.map +1 -0
  115. package/dist/services/locationStore.d.ts +62 -0
  116. package/dist/services/locationStore.d.ts.map +1 -0
  117. package/dist/services/locationStore.js +201 -0
  118. package/dist/services/locationStore.js.map +1 -0
  119. package/dist/services/ncei.d.ts +61 -0
  120. package/dist/services/ncei.d.ts.map +1 -0
  121. package/dist/services/ncei.js +126 -0
  122. package/dist/services/ncei.js.map +1 -0
  123. package/dist/services/nifc.d.ts +44 -0
  124. package/dist/services/nifc.d.ts.map +1 -0
  125. package/dist/services/nifc.js +159 -0
  126. package/dist/services/nifc.js.map +1 -0
  127. package/dist/services/noaa.d.ts +161 -0
  128. package/dist/services/noaa.d.ts.map +1 -0
  129. package/dist/services/noaa.js +681 -0
  130. package/dist/services/noaa.js.map +1 -0
  131. package/dist/services/nominatim.d.ts +62 -0
  132. package/dist/services/nominatim.d.ts.map +1 -0
  133. package/dist/services/nominatim.js +254 -0
  134. package/dist/services/nominatim.js.map +1 -0
  135. package/dist/services/openmeteo.d.ts +189 -0
  136. package/dist/services/openmeteo.d.ts.map +1 -0
  137. package/dist/services/openmeteo.js +936 -0
  138. package/dist/services/openmeteo.js.map +1 -0
  139. package/dist/services/rainviewer.d.ts +37 -0
  140. package/dist/services/rainviewer.d.ts.map +1 -0
  141. package/dist/services/rainviewer.js +115 -0
  142. package/dist/services/rainviewer.js.map +1 -0
  143. package/dist/types/imagery.d.ts +82 -0
  144. package/dist/types/imagery.d.ts.map +1 -0
  145. package/dist/types/imagery.js +6 -0
  146. package/dist/types/imagery.js.map +1 -0
  147. package/dist/types/lightning.d.ts +89 -0
  148. package/dist/types/lightning.d.ts.map +1 -0
  149. package/dist/types/lightning.js +6 -0
  150. package/dist/types/lightning.js.map +1 -0
  151. package/dist/types/noaa.d.ts +535 -0
  152. package/dist/types/noaa.d.ts.map +1 -0
  153. package/dist/types/noaa.js +5 -0
  154. package/dist/types/noaa.js.map +1 -0
  155. package/dist/types/nominatim.d.ts +72 -0
  156. package/dist/types/nominatim.d.ts.map +1 -0
  157. package/dist/types/nominatim.js +6 -0
  158. package/dist/types/nominatim.js.map +1 -0
  159. package/dist/types/openmeteo.d.ts +583 -0
  160. package/dist/types/openmeteo.d.ts.map +1 -0
  161. package/dist/types/openmeteo.js +6 -0
  162. package/dist/types/openmeteo.js.map +1 -0
  163. package/dist/types/savedLocations.d.ts +58 -0
  164. package/dist/types/savedLocations.d.ts.map +1 -0
  165. package/dist/types/savedLocations.js +5 -0
  166. package/dist/types/savedLocations.js.map +1 -0
  167. package/dist/types/wildfire.d.ts +83 -0
  168. package/dist/types/wildfire.d.ts.map +1 -0
  169. package/dist/types/wildfire.js +5 -0
  170. package/dist/types/wildfire.js.map +1 -0
  171. package/dist/utils/airQuality.d.ts +54 -0
  172. package/dist/utils/airQuality.d.ts.map +1 -0
  173. package/dist/utils/airQuality.js +251 -0
  174. package/dist/utils/airQuality.js.map +1 -0
  175. package/dist/utils/cache.d.ts +69 -0
  176. package/dist/utils/cache.d.ts.map +1 -0
  177. package/dist/utils/cache.js +164 -0
  178. package/dist/utils/cache.js.map +1 -0
  179. package/dist/utils/distance.d.ts +25 -0
  180. package/dist/utils/distance.d.ts.map +1 -0
  181. package/dist/utils/distance.js +40 -0
  182. package/dist/utils/distance.js.map +1 -0
  183. package/dist/utils/fireWeather.d.ts +76 -0
  184. package/dist/utils/fireWeather.d.ts.map +1 -0
  185. package/dist/utils/fireWeather.js +243 -0
  186. package/dist/utils/fireWeather.js.map +1 -0
  187. package/dist/utils/geography.d.ts +79 -0
  188. package/dist/utils/geography.d.ts.map +1 -0
  189. package/dist/utils/geography.js +266 -0
  190. package/dist/utils/geography.js.map +1 -0
  191. package/dist/utils/geohash.d.ts +62 -0
  192. package/dist/utils/geohash.d.ts.map +1 -0
  193. package/dist/utils/geohash.js +146 -0
  194. package/dist/utils/geohash.js.map +1 -0
  195. package/dist/utils/locationResolver.d.ts +34 -0
  196. package/dist/utils/locationResolver.d.ts.map +1 -0
  197. package/dist/utils/locationResolver.js +120 -0
  198. package/dist/utils/locationResolver.js.map +1 -0
  199. package/dist/utils/logger.d.ts +75 -0
  200. package/dist/utils/logger.d.ts.map +1 -0
  201. package/dist/utils/logger.js +153 -0
  202. package/dist/utils/logger.js.map +1 -0
  203. package/dist/utils/marine.d.ts +59 -0
  204. package/dist/utils/marine.d.ts.map +1 -0
  205. package/dist/utils/marine.js +215 -0
  206. package/dist/utils/marine.js.map +1 -0
  207. package/dist/utils/normals.d.ts +86 -0
  208. package/dist/utils/normals.d.ts.map +1 -0
  209. package/dist/utils/normals.js +223 -0
  210. package/dist/utils/normals.js.map +1 -0
  211. package/dist/utils/snow.d.ts +45 -0
  212. package/dist/utils/snow.d.ts.map +1 -0
  213. package/dist/utils/snow.js +144 -0
  214. package/dist/utils/snow.js.map +1 -0
  215. package/dist/utils/temperatureConversion.d.ts +12 -0
  216. package/dist/utils/temperatureConversion.d.ts.map +1 -0
  217. package/dist/utils/temperatureConversion.js +17 -0
  218. package/dist/utils/temperatureConversion.js.map +1 -0
  219. package/dist/utils/timezone.d.ts +56 -0
  220. package/dist/utils/timezone.d.ts.map +1 -0
  221. package/dist/utils/timezone.js +167 -0
  222. package/dist/utils/timezone.js.map +1 -0
  223. package/dist/utils/units.d.ts +69 -0
  224. package/dist/utils/units.d.ts.map +1 -0
  225. package/dist/utils/units.js +158 -0
  226. package/dist/utils/units.js.map +1 -0
  227. package/dist/utils/validation.d.ts +89 -0
  228. package/dist/utils/validation.d.ts.map +1 -0
  229. package/dist/utils/validation.js +177 -0
  230. package/dist/utils/validation.js.map +1 -0
  231. package/dist/utils/version.d.ts +15 -0
  232. package/dist/utils/version.d.ts.map +1 -0
  233. package/dist/utils/version.js +24 -0
  234. package/dist/utils/version.js.map +1 -0
  235. package/package.json +74 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Service for interacting with the NOAA Weather API
3
+ */
4
+ import axios from 'axios';
5
+ import { Cache } from '../utils/cache.js';
6
+ import { CacheConfig, getHistoricalDataTTL } from '../config/cache.js';
7
+ import { validateLatitude, validateLongitude } from '../utils/validation.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import { RateLimitError, ServiceUnavailableError, InvalidLocationError, DataNotFoundError, ApiError } from '../errors/ApiError.js';
10
+ export class NOAAService {
11
+ client;
12
+ nwpsClient; // For NOAA water/river data
13
+ usgsClient; // For USGS streamflow data
14
+ maxRetries;
15
+ cache;
16
+ constructor(config = {}) {
17
+ const { userAgent = '(weather-mcp, contact@example.com)', baseURL = 'https://api.weather.gov', nwpsBaseURL = 'https://api.water.noaa.gov/nwps/v1', usgsBaseURL = 'https://waterservices.usgs.gov', timeout = CacheConfig.apiTimeoutMs, maxRetries = 3 } = config;
18
+ this.maxRetries = maxRetries;
19
+ this.cache = new Cache(CacheConfig.maxSize);
20
+ // Main NOAA Weather API client
21
+ this.client = axios.create({
22
+ baseURL,
23
+ timeout,
24
+ headers: {
25
+ 'User-Agent': userAgent,
26
+ 'Accept': 'application/geo+json'
27
+ }
28
+ });
29
+ // NWPS (National Water Prediction Service) client for river gauges
30
+ this.nwpsClient = axios.create({
31
+ baseURL: nwpsBaseURL,
32
+ timeout,
33
+ headers: {
34
+ 'User-Agent': userAgent,
35
+ 'Accept': 'application/json'
36
+ }
37
+ });
38
+ // USGS Water Services client for streamflow data
39
+ this.usgsClient = axios.create({
40
+ baseURL: usgsBaseURL,
41
+ timeout,
42
+ headers: {
43
+ 'User-Agent': userAgent
44
+ }
45
+ });
46
+ // Add response interceptor for error handling
47
+ this.client.interceptors.response.use(response => response, error => this.handleError(error));
48
+ this.nwpsClient.interceptors.response.use(response => response, error => this.handleError(error));
49
+ this.usgsClient.interceptors.response.use(response => response, error => this.handleError(error));
50
+ }
51
+ /**
52
+ * Handle API errors with retry logic and helpful status information
53
+ */
54
+ async handleError(error) {
55
+ if (error.response) {
56
+ const status = error.response.status;
57
+ const data = error.response.data;
58
+ // Rate limit error - suggest retry
59
+ if (status === 429) {
60
+ logger.warn('Rate limit exceeded', {
61
+ service: 'NOAA',
62
+ securityEvent: true
63
+ });
64
+ throw new RateLimitError('NOAA');
65
+ }
66
+ // 404 errors - location not found
67
+ if (status === 404) {
68
+ throw new DataNotFoundError('NOAA', `${data.detail || data.title || 'Location not found'}\n\n` +
69
+ `This location may be outside NOAA's coverage area (US only).`);
70
+ }
71
+ // Other client errors
72
+ if (status >= 400 && status < 500) {
73
+ logger.warn('Invalid request parameters', {
74
+ service: 'NOAA',
75
+ status,
76
+ detail: data.detail || data.title,
77
+ securityEvent: true
78
+ });
79
+ throw new InvalidLocationError('NOAA', data.detail || data.title || 'Invalid request');
80
+ }
81
+ // Server errors
82
+ if (status >= 500) {
83
+ throw new ServiceUnavailableError('NOAA', error);
84
+ }
85
+ }
86
+ // Network errors
87
+ if (error.code === 'ECONNABORTED') {
88
+ throw new ServiceUnavailableError('NOAA', error);
89
+ }
90
+ if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
91
+ throw new ServiceUnavailableError('NOAA', error);
92
+ }
93
+ // Generic error
94
+ throw new ApiError(`NOAA API request failed: ${error.message}`, 500, 'NOAA', `Request failed: ${error.message}`, ['https://weather-gov.github.io/api/reporting-issues'], true);
95
+ }
96
+ /**
97
+ * Make request with retry logic
98
+ */
99
+ async makeRequest(url, retries = 0) {
100
+ try {
101
+ const response = await this.client.get(url);
102
+ return response.data;
103
+ }
104
+ catch (error) {
105
+ // Retry on rate limit or server errors
106
+ if (retries < this.maxRetries) {
107
+ const shouldRetry = error.message.includes('rate limit') ||
108
+ error.message.includes('server error') ||
109
+ error.message.includes('timed out');
110
+ if (shouldRetry) {
111
+ // Exponential backoff with jitter to prevent thundering herd
112
+ const baseDelay = Math.pow(2, retries) * 1000;
113
+ const delay = baseDelay * (0.5 + Math.random() * 0.5);
114
+ await new Promise(resolve => setTimeout(resolve, delay));
115
+ return this.makeRequest(url, retries + 1);
116
+ }
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+ /**
122
+ * Get cache statistics
123
+ */
124
+ getCacheStats() {
125
+ return this.cache.getStats();
126
+ }
127
+ /**
128
+ * Clear the cache
129
+ */
130
+ clearCache() {
131
+ this.cache.clear();
132
+ }
133
+ /**
134
+ * Check if the NOAA API is operational
135
+ * Performs a lightweight health check by requesting a well-known endpoint
136
+ * @returns Object with status information
137
+ */
138
+ async checkServiceStatus() {
139
+ try {
140
+ // Use a simple, well-known location (US mainland center) for health check
141
+ const response = await this.client.get('/points/39.8283,-98.5795', {
142
+ timeout: 10000 // Shorter timeout for health check
143
+ });
144
+ if (response.status === 200) {
145
+ return {
146
+ operational: true,
147
+ message: 'NOAA Weather API is operational',
148
+ statusPage: 'https://weather-gov.github.io/api/planned-outages',
149
+ timestamp: new Date().toISOString()
150
+ };
151
+ }
152
+ return {
153
+ operational: false,
154
+ message: `NOAA API returned unexpected status: ${response.status}`,
155
+ statusPage: 'https://weather-gov.github.io/api/planned-outages',
156
+ timestamp: new Date().toISOString()
157
+ };
158
+ }
159
+ catch (error) {
160
+ const axiosError = error;
161
+ let message = 'NOAA Weather API may be experiencing issues';
162
+ let operational = false;
163
+ if (axiosError.response) {
164
+ const status = axiosError.response.status;
165
+ if (status === 429) {
166
+ operational = true; // API is up, just rate limited
167
+ message = 'NOAA API is operational but rate limited';
168
+ }
169
+ else if (status >= 500) {
170
+ message = 'NOAA API is experiencing server errors (possible outage)';
171
+ }
172
+ else if (status === 404) {
173
+ operational = true; // 404 on this endpoint might just mean API change
174
+ message = 'NOAA API is responding (health check endpoint may have changed)';
175
+ }
176
+ }
177
+ else if (axiosError.code === 'ECONNABORTED') {
178
+ message = 'NOAA API is not responding (timeout)';
179
+ }
180
+ else if (axiosError.code === 'ENOTFOUND' || axiosError.code === 'ECONNREFUSED') {
181
+ message = 'Cannot connect to NOAA API (DNS or connection failure)';
182
+ }
183
+ return {
184
+ operational,
185
+ message,
186
+ statusPage: 'https://weather-gov.github.io/api/planned-outages',
187
+ timestamp: new Date().toISOString()
188
+ };
189
+ }
190
+ }
191
+ /**
192
+ * Convert lat/lon coordinates to NWS grid information
193
+ * This is the first step for getting forecast or observation data
194
+ */
195
+ async getPointData(latitude, longitude) {
196
+ // Validate coordinates (checks for NaN, Infinity, and range)
197
+ validateLatitude(latitude);
198
+ validateLongitude(longitude);
199
+ // Check cache first (if enabled)
200
+ if (CacheConfig.enabled) {
201
+ const cacheKey = Cache.generateKey('points', latitude.toFixed(4), longitude.toFixed(4));
202
+ const cached = this.cache.get(cacheKey);
203
+ if (cached) {
204
+ return cached;
205
+ }
206
+ const url = `/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
207
+ const result = await this.makeRequest(url);
208
+ // Cache with infinite TTL (grid coordinates never change)
209
+ this.cache.set(cacheKey, result, CacheConfig.ttl.gridCoordinates);
210
+ return result;
211
+ }
212
+ const url = `/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
213
+ return this.makeRequest(url);
214
+ }
215
+ /**
216
+ * Get forecast for a location using grid coordinates
217
+ */
218
+ async getForecast(office, gridX, gridY) {
219
+ // Check cache first (if enabled)
220
+ if (CacheConfig.enabled) {
221
+ const cacheKey = Cache.generateKey('forecast', office, gridX, gridY);
222
+ const cached = this.cache.get(cacheKey);
223
+ if (cached) {
224
+ return cached;
225
+ }
226
+ const url = `/gridpoints/${office}/${gridX},${gridY}/forecast`;
227
+ const result = await this.makeRequest(url);
228
+ // Cache with forecast TTL (2 hours)
229
+ this.cache.set(cacheKey, result, CacheConfig.ttl.forecast);
230
+ return result;
231
+ }
232
+ const url = `/gridpoints/${office}/${gridX},${gridY}/forecast`;
233
+ return this.makeRequest(url);
234
+ }
235
+ /**
236
+ * Get hourly forecast for a location using grid coordinates
237
+ */
238
+ async getHourlyForecast(office, gridX, gridY) {
239
+ // Check cache first (if enabled)
240
+ if (CacheConfig.enabled) {
241
+ const cacheKey = Cache.generateKey('hourly-forecast', office, gridX, gridY);
242
+ const cached = this.cache.get(cacheKey);
243
+ if (cached) {
244
+ return cached;
245
+ }
246
+ const url = `/gridpoints/${office}/${gridX},${gridY}/forecast/hourly`;
247
+ const result = await this.makeRequest(url);
248
+ // Cache with forecast TTL (2 hours) - hourly forecasts update at same rate as daily
249
+ this.cache.set(cacheKey, result, CacheConfig.ttl.forecast);
250
+ return result;
251
+ }
252
+ const url = `/gridpoints/${office}/${gridX},${gridY}/forecast/hourly`;
253
+ return this.makeRequest(url);
254
+ }
255
+ /**
256
+ * Get forecast for a location using lat/lon (convenience method)
257
+ * This combines getPointData and getForecast
258
+ */
259
+ async getForecastByCoordinates(latitude, longitude) {
260
+ const pointData = await this.getPointData(latitude, longitude);
261
+ const { gridId, gridX, gridY } = pointData.properties;
262
+ return this.getForecast(gridId, gridX, gridY);
263
+ }
264
+ /**
265
+ * Get hourly forecast for a location using lat/lon (convenience method)
266
+ * This combines getPointData and getHourlyForecast
267
+ */
268
+ async getHourlyForecastByCoordinates(latitude, longitude) {
269
+ const pointData = await this.getPointData(latitude, longitude);
270
+ const { gridId, gridX, gridY } = pointData.properties;
271
+ return this.getHourlyForecast(gridId, gridX, gridY);
272
+ }
273
+ /**
274
+ * Get gridpoint data for a location using grid coordinates
275
+ * Contains detailed forecast data including fire weather indices
276
+ */
277
+ async getGridpointData(office, gridX, gridY) {
278
+ // Check cache first (if enabled)
279
+ if (CacheConfig.enabled) {
280
+ const cacheKey = Cache.generateKey('gridpoint', office, gridX, gridY);
281
+ const cached = this.cache.get(cacheKey);
282
+ if (cached) {
283
+ return cached;
284
+ }
285
+ const url = `/gridpoints/${office}/${gridX},${gridY}`;
286
+ const result = await this.makeRequest(url);
287
+ // Cache with forecast TTL (2 hours)
288
+ this.cache.set(cacheKey, result, CacheConfig.ttl.forecast);
289
+ return result;
290
+ }
291
+ const url = `/gridpoints/${office}/${gridX},${gridY}`;
292
+ return this.makeRequest(url);
293
+ }
294
+ /**
295
+ * Get gridpoint data for a location using lat/lon (convenience method)
296
+ * This combines getPointData and getGridpointData
297
+ */
298
+ async getGridpointDataByCoordinates(latitude, longitude) {
299
+ const pointData = await this.getPointData(latitude, longitude);
300
+ const { gridId, gridX, gridY } = pointData.properties;
301
+ return this.getGridpointData(gridId, gridX, gridY);
302
+ }
303
+ /**
304
+ * Get nearest observation stations for a location
305
+ */
306
+ async getStations(latitude, longitude) {
307
+ // Check cache first (if enabled)
308
+ if (CacheConfig.enabled) {
309
+ const cacheKey = Cache.generateKey('stations', latitude.toFixed(4), longitude.toFixed(4));
310
+ const cached = this.cache.get(cacheKey);
311
+ if (cached) {
312
+ return cached;
313
+ }
314
+ const url = `/points/${latitude.toFixed(4)},${longitude.toFixed(4)}/stations`;
315
+ const result = await this.makeRequest(url);
316
+ // Cache with stations TTL (24 hours - stations rarely change)
317
+ this.cache.set(cacheKey, result, CacheConfig.ttl.stations);
318
+ return result;
319
+ }
320
+ const url = `/points/${latitude.toFixed(4)},${longitude.toFixed(4)}/stations`;
321
+ return this.makeRequest(url);
322
+ }
323
+ /**
324
+ * Get the latest observation from a station
325
+ */
326
+ async getLatestObservation(stationId) {
327
+ // Check cache first (if enabled)
328
+ if (CacheConfig.enabled) {
329
+ const cacheKey = Cache.generateKey('latest-observation', stationId);
330
+ const cached = this.cache.get(cacheKey);
331
+ if (cached) {
332
+ return cached;
333
+ }
334
+ const url = `/stations/${stationId}/observations/latest`;
335
+ const result = await this.makeRequest(url);
336
+ // Cache with current conditions TTL (15 minutes)
337
+ this.cache.set(cacheKey, result, CacheConfig.ttl.currentConditions);
338
+ return result;
339
+ }
340
+ const url = `/stations/${stationId}/observations/latest`;
341
+ return this.makeRequest(url);
342
+ }
343
+ /**
344
+ * Get observations from a station within a time range
345
+ */
346
+ async getObservations(stationId, startTime, endTime, limit) {
347
+ // Validate date range if both dates are provided
348
+ if (startTime && endTime) {
349
+ if (startTime > endTime) {
350
+ throw new Error(`Invalid date range: start date (${startTime.toISOString()}) must be before end date (${endTime.toISOString()})`);
351
+ }
352
+ }
353
+ // Validate dates are not in the future
354
+ const now = new Date();
355
+ if (startTime && startTime > now) {
356
+ throw new Error(`Start date (${startTime.toISOString()}) cannot be in the future`);
357
+ }
358
+ if (endTime && endTime > now) {
359
+ throw new Error(`End date (${endTime.toISOString()}) cannot be in the future`);
360
+ }
361
+ let url = `/stations/${stationId}/observations`;
362
+ const params = new URLSearchParams();
363
+ if (startTime) {
364
+ params.append('start', startTime.toISOString());
365
+ }
366
+ if (endTime) {
367
+ params.append('end', endTime.toISOString());
368
+ }
369
+ if (limit) {
370
+ // Ensure limit is between 1 and 500
371
+ const validLimit = Math.max(1, Math.min(limit, 500));
372
+ params.append('limit', validLimit.toString());
373
+ }
374
+ if (params.toString()) {
375
+ url += `?${params.toString()}`;
376
+ }
377
+ // Check cache first (if enabled)
378
+ if (CacheConfig.enabled) {
379
+ const cacheKey = Cache.generateKey('observations', stationId, startTime?.toISOString(), endTime?.toISOString(), limit);
380
+ const cached = this.cache.get(cacheKey);
381
+ if (cached) {
382
+ return cached;
383
+ }
384
+ const result = await this.makeRequest(url);
385
+ // Use smart TTL based on date range
386
+ const ttl = startTime ? getHistoricalDataTTL(startTime) : CacheConfig.ttl.recentHistorical;
387
+ this.cache.set(cacheKey, result, ttl);
388
+ return result;
389
+ }
390
+ return this.makeRequest(url);
391
+ }
392
+ /**
393
+ * Get current conditions for a location (convenience method)
394
+ * This combines getStations and getLatestObservation
395
+ */
396
+ async getCurrentConditions(latitude, longitude) {
397
+ const stations = await this.getStations(latitude, longitude);
398
+ if (!stations.features || stations.features.length === 0) {
399
+ throw new Error('No weather stations found near the specified location.');
400
+ }
401
+ // Try the first station, fallback to others if it fails
402
+ for (const station of stations.features) {
403
+ try {
404
+ const stationId = station.properties.stationIdentifier;
405
+ return await this.getLatestObservation(stationId);
406
+ }
407
+ catch (error) {
408
+ // Try next station
409
+ continue;
410
+ }
411
+ }
412
+ throw new Error('Unable to retrieve current conditions from nearby stations.');
413
+ }
414
+ /**
415
+ * Get historical observations for a location (convenience method)
416
+ */
417
+ async getHistoricalObservations(latitude, longitude, startTime, endTime, limit) {
418
+ // Validate date range
419
+ if (startTime > endTime) {
420
+ throw new Error(`Invalid date range: start date (${startTime.toISOString()}) must be before end date (${endTime.toISOString()})`);
421
+ }
422
+ // Validate dates are not in the future
423
+ const now = new Date();
424
+ if (startTime > now) {
425
+ throw new Error(`Start date (${startTime.toISOString()}) cannot be in the future`);
426
+ }
427
+ if (endTime > now) {
428
+ throw new Error(`End date (${endTime.toISOString()}) cannot be in the future`);
429
+ }
430
+ const stations = await this.getStations(latitude, longitude);
431
+ if (!stations.features || stations.features.length === 0) {
432
+ throw new Error('No weather stations found near the specified location.');
433
+ }
434
+ // Get observations from the nearest station
435
+ const stationId = stations.features[0].properties.stationIdentifier;
436
+ return this.getObservations(stationId, startTime, endTime, limit);
437
+ }
438
+ /**
439
+ * Get active weather alerts for a location
440
+ * @param latitude Latitude coordinate
441
+ * @param longitude Longitude coordinate
442
+ * @param activeOnly Whether to filter to only active alerts (default: true)
443
+ * @returns Collection of weather alerts
444
+ */
445
+ async getAlerts(latitude, longitude, activeOnly = true) {
446
+ // Validate coordinates (checks for NaN, Infinity, and range)
447
+ validateLatitude(latitude);
448
+ validateLongitude(longitude);
449
+ // Check cache first (if enabled)
450
+ if (CacheConfig.enabled) {
451
+ const cacheKey = Cache.generateKey('alerts', latitude.toFixed(4), longitude.toFixed(4), activeOnly ? 'active' : 'all');
452
+ const cached = this.cache.get(cacheKey);
453
+ if (cached) {
454
+ return cached;
455
+ }
456
+ // Query alerts using point parameter
457
+ const url = activeOnly
458
+ ? `/alerts/active?point=${latitude.toFixed(4)},${longitude.toFixed(4)}`
459
+ : `/alerts?point=${latitude.toFixed(4)},${longitude.toFixed(4)}`;
460
+ const result = await this.makeRequest(url);
461
+ // Cache with alerts TTL (5 minutes - alerts can change rapidly)
462
+ this.cache.set(cacheKey, result, CacheConfig.ttl.alerts);
463
+ return result;
464
+ }
465
+ // Query alerts using point parameter
466
+ const url = activeOnly
467
+ ? `/alerts/active?point=${latitude.toFixed(4)},${longitude.toFixed(4)}`
468
+ : `/alerts?point=${latitude.toFixed(4)},${longitude.toFixed(4)}`;
469
+ return this.makeRequest(url);
470
+ }
471
+ /**
472
+ * NWPS (National Water Prediction Service) Methods for River Gauges
473
+ */
474
+ /**
475
+ * Get a specific river gauge by its NWSLI identifier
476
+ * @param lid 5-character NWSLI identifier (e.g., "LOLT2")
477
+ * @returns River gauge data with current conditions and flood stages
478
+ */
479
+ async getNWPSGauge(lid) {
480
+ // Check cache first (if enabled)
481
+ if (CacheConfig.enabled) {
482
+ const cacheKey = Cache.generateKey('nwps-gauge', lid);
483
+ const cached = this.cache.get(cacheKey);
484
+ if (cached) {
485
+ return cached;
486
+ }
487
+ const response = await this.nwpsClient.get(`/gauges/${lid}`);
488
+ const result = response.data;
489
+ // Cache for 1 hour (river conditions update hourly)
490
+ this.cache.set(cacheKey, result, 3600000);
491
+ return result;
492
+ }
493
+ const response = await this.nwpsClient.get(`/gauges/${lid}`);
494
+ return response.data;
495
+ }
496
+ /**
497
+ * Get stage/flow time series data for a specific gauge
498
+ * @param lid 5-character NWSLI identifier
499
+ * @returns Time series of stage and flow data
500
+ */
501
+ async getNWPSStageFlow(lid) {
502
+ // Check cache first (if enabled)
503
+ if (CacheConfig.enabled) {
504
+ const cacheKey = Cache.generateKey('nwps-stageflow', lid);
505
+ const cached = this.cache.get(cacheKey);
506
+ if (cached) {
507
+ return cached;
508
+ }
509
+ const response = await this.nwpsClient.get(`/gauges/${lid}/stageflow`);
510
+ const result = response.data;
511
+ // Cache for 30 minutes (frequently updated)
512
+ this.cache.set(cacheKey, result, 1800000);
513
+ return result;
514
+ }
515
+ const response = await this.nwpsClient.get(`/gauges/${lid}/stageflow`);
516
+ return response.data;
517
+ }
518
+ /**
519
+ * Get all NWPS gauges (warning: large response, should be filtered)
520
+ * Note: This endpoint returns all gauges across the US. Consider using
521
+ * geographic filtering or querying by specific gauge IDs instead.
522
+ * @returns Array of all river gauges
523
+ * @deprecated Use getNWPSGaugesInBoundingBox instead to avoid downloading entire catalog
524
+ */
525
+ async getAllNWPSGauges() {
526
+ // Check cache first (if enabled) - cache for 24 hours (gauges rarely change)
527
+ if (CacheConfig.enabled) {
528
+ const cacheKey = Cache.generateKey('nwps-all-gauges');
529
+ const cached = this.cache.get(cacheKey);
530
+ if (cached) {
531
+ return cached;
532
+ }
533
+ const response = await this.nwpsClient.get('/gauges');
534
+ const result = response.data;
535
+ // Cache for 24 hours (gauge locations don't change often)
536
+ this.cache.set(cacheKey, result, 86400000);
537
+ return result;
538
+ }
539
+ const response = await this.nwpsClient.get('/gauges');
540
+ return response.data;
541
+ }
542
+ /**
543
+ * Get NWPS river gauges within a bounding box
544
+ * More efficient than getAllNWPSGauges() for location-specific queries
545
+ * @param west Western longitude boundary
546
+ * @param south Southern latitude boundary
547
+ * @param east Eastern longitude boundary
548
+ * @param north Northern latitude boundary
549
+ * @returns Array of gauges within the bounding box
550
+ */
551
+ async getNWPSGaugesInBoundingBox(west, south, east, north) {
552
+ // Validate bounding box
553
+ validateLongitude(west);
554
+ validateLongitude(east);
555
+ validateLatitude(south);
556
+ validateLatitude(north);
557
+ if (west >= east) {
558
+ throw new Error('Invalid bounding box: west longitude must be less than east longitude');
559
+ }
560
+ if (south >= north) {
561
+ throw new Error('Invalid bounding box: south latitude must be less than north latitude');
562
+ }
563
+ // Check cache first (if enabled)
564
+ const bboxKey = `${west.toFixed(2)},${south.toFixed(2)},${east.toFixed(2)},${north.toFixed(2)}`;
565
+ if (CacheConfig.enabled) {
566
+ const cacheKey = Cache.generateKey('nwps-gauges-bbox', bboxKey);
567
+ const cached = this.cache.get(cacheKey);
568
+ if (cached) {
569
+ return cached;
570
+ }
571
+ }
572
+ // NWPS API supports bounding box queries via query parameters
573
+ const params = {
574
+ west: west.toString(),
575
+ south: south.toString(),
576
+ east: east.toString(),
577
+ north: north.toString()
578
+ };
579
+ try {
580
+ const response = await this.nwpsClient.get('/gauges', { params });
581
+ const result = response.data;
582
+ // Cache for 24 hours
583
+ if (CacheConfig.enabled) {
584
+ const cacheKey = Cache.generateKey('nwps-gauges-bbox', bboxKey);
585
+ this.cache.set(cacheKey, result, 86400000);
586
+ }
587
+ return result;
588
+ }
589
+ catch (error) {
590
+ // If bounding box query fails, fall back to client-side filtering
591
+ // This provides compatibility if the API doesn't support bbox queries
592
+ logger.warn('NWPS bounding box query failed, falling back to client-side filtering', {
593
+ error: error instanceof Error ? error.message : String(error)
594
+ });
595
+ const allGauges = await this.getAllNWPSGauges();
596
+ const filtered = allGauges.filter(gauge => gauge.longitude >= west &&
597
+ gauge.longitude <= east &&
598
+ gauge.latitude >= south &&
599
+ gauge.latitude <= north);
600
+ // Cache the filtered result
601
+ if (CacheConfig.enabled) {
602
+ const cacheKey = Cache.generateKey('nwps-gauges-bbox', bboxKey);
603
+ this.cache.set(cacheKey, filtered, 86400000);
604
+ }
605
+ return filtered;
606
+ }
607
+ }
608
+ /**
609
+ * USGS Water Services Methods for Streamflow Data
610
+ */
611
+ /**
612
+ * Get real-time streamflow data for sites within a bounding box
613
+ * @param west Western longitude boundary
614
+ * @param south Southern latitude boundary
615
+ * @param east Eastern longitude boundary
616
+ * @param north Northern latitude boundary
617
+ * @returns USGS instantaneous values response with streamflow data
618
+ */
619
+ async getUSGSStreamflow(west, south, east, north) {
620
+ // Validate bounding box
621
+ validateLongitude(west);
622
+ validateLongitude(east);
623
+ validateLatitude(south);
624
+ validateLatitude(north);
625
+ if (west >= east) {
626
+ throw new Error('Invalid bounding box: west longitude must be less than east longitude');
627
+ }
628
+ if (south >= north) {
629
+ throw new Error('Invalid bounding box: south latitude must be less than north latitude');
630
+ }
631
+ // USGS API limits: product of lat/lon range cannot exceed 25 degrees
632
+ const latRange = north - south;
633
+ const lonRange = east - west;
634
+ if (latRange * lonRange > 25) {
635
+ throw new Error('Bounding box too large: product of latitude and longitude ranges cannot exceed 25 degrees');
636
+ }
637
+ // Check cache first (if enabled)
638
+ const bboxKey = `${west},${south},${east},${north}`;
639
+ if (CacheConfig.enabled) {
640
+ const cacheKey = Cache.generateKey('usgs-streamflow', bboxKey);
641
+ const cached = this.cache.get(cacheKey);
642
+ if (cached) {
643
+ return cached;
644
+ }
645
+ const url = `/nwis/iv/?format=json&bBox=${bboxKey}&parameterCd=00060&siteStatus=active`;
646
+ const response = await this.usgsClient.get(url);
647
+ const result = response.data;
648
+ // Cache for 15 minutes (current data)
649
+ this.cache.set(cacheKey, result, 900000);
650
+ return result;
651
+ }
652
+ const url = `/nwis/iv/?format=json&bBox=${bboxKey}&parameterCd=00060&siteStatus=active`;
653
+ const response = await this.usgsClient.get(url);
654
+ return response.data;
655
+ }
656
+ /**
657
+ * Get real-time streamflow data for a specific USGS site
658
+ * @param siteNumber USGS site number (e.g., "01646500")
659
+ * @returns USGS instantaneous values response with streamflow data
660
+ */
661
+ async getUSGSStreamflowForSite(siteNumber) {
662
+ // Check cache first (if enabled)
663
+ if (CacheConfig.enabled) {
664
+ const cacheKey = Cache.generateKey('usgs-site-streamflow', siteNumber);
665
+ const cached = this.cache.get(cacheKey);
666
+ if (cached) {
667
+ return cached;
668
+ }
669
+ const url = `/nwis/iv/?format=json&sites=${siteNumber}&parameterCd=00060&siteStatus=active`;
670
+ const response = await this.usgsClient.get(url);
671
+ const result = response.data;
672
+ // Cache for 15 minutes (current data)
673
+ this.cache.set(cacheKey, result, 900000);
674
+ return result;
675
+ }
676
+ const url = `/nwis/iv/?format=json&sites=${siteNumber}&parameterCd=00060&siteStatus=active`;
677
+ const response = await this.usgsClient.get(url);
678
+ return response.data;
679
+ }
680
+ }
681
+ //# sourceMappingURL=noaa.js.map