@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.
- package/LICENSE +21 -0
- package/README.md +1319 -0
- package/dist/analytics/anonymizer.d.ts +37 -0
- package/dist/analytics/anonymizer.d.ts.map +1 -0
- package/dist/analytics/anonymizer.js +112 -0
- package/dist/analytics/anonymizer.js.map +1 -0
- package/dist/analytics/collector.d.ts +72 -0
- package/dist/analytics/collector.d.ts.map +1 -0
- package/dist/analytics/collector.js +282 -0
- package/dist/analytics/collector.js.map +1 -0
- package/dist/analytics/config.d.ts +15 -0
- package/dist/analytics/config.d.ts.map +1 -0
- package/dist/analytics/config.js +172 -0
- package/dist/analytics/config.js.map +1 -0
- package/dist/analytics/index.d.ts +8 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/index.js +7 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/middleware.d.ts +33 -0
- package/dist/analytics/middleware.d.ts.map +1 -0
- package/dist/analytics/middleware.js +99 -0
- package/dist/analytics/middleware.js.map +1 -0
- package/dist/analytics/transport.d.ts +11 -0
- package/dist/analytics/transport.d.ts.map +1 -0
- package/dist/analytics/transport.js +92 -0
- package/dist/analytics/transport.js.map +1 -0
- package/dist/analytics/types.d.ts +74 -0
- package/dist/analytics/types.d.ts.map +1 -0
- package/dist/analytics/types.js +6 -0
- package/dist/analytics/types.js.map +1 -0
- package/dist/config/api.d.ts +30 -0
- package/dist/config/api.d.ts.map +1 -0
- package/dist/config/api.js +32 -0
- package/dist/config/api.js.map +1 -0
- package/dist/config/cache.d.ts +31 -0
- package/dist/config/cache.d.ts.map +1 -0
- package/dist/config/cache.js +108 -0
- package/dist/config/cache.js.map +1 -0
- package/dist/config/displayThresholds.d.ts +83 -0
- package/dist/config/displayThresholds.d.ts.map +1 -0
- package/dist/config/displayThresholds.js +83 -0
- package/dist/config/displayThresholds.js.map +1 -0
- package/dist/config/tools.d.ts +44 -0
- package/dist/config/tools.d.ts.map +1 -0
- package/dist/config/tools.js +269 -0
- package/dist/config/tools.js.map +1 -0
- package/dist/errors/ApiError.d.ts +62 -0
- package/dist/errors/ApiError.d.ts.map +1 -0
- package/dist/errors/ApiError.js +171 -0
- package/dist/errors/ApiError.js.map +1 -0
- package/dist/handlers/airQualityHandler.d.ts +11 -0
- package/dist/handlers/airQualityHandler.d.ts.map +1 -0
- package/dist/handlers/airQualityHandler.js +154 -0
- package/dist/handlers/airQualityHandler.js.map +1 -0
- package/dist/handlers/alertsHandler.d.ts +11 -0
- package/dist/handlers/alertsHandler.d.ts.map +1 -0
- package/dist/handlers/alertsHandler.js +98 -0
- package/dist/handlers/alertsHandler.js.map +1 -0
- package/dist/handlers/currentConditionsHandler.d.ts +13 -0
- package/dist/handlers/currentConditionsHandler.d.ts.map +1 -0
- package/dist/handlers/currentConditionsHandler.js +296 -0
- package/dist/handlers/currentConditionsHandler.js.map +1 -0
- package/dist/handlers/forecastHandler.d.ts +16 -0
- package/dist/handlers/forecastHandler.d.ts.map +1 -0
- package/dist/handlers/forecastHandler.js +454 -0
- package/dist/handlers/forecastHandler.js.map +1 -0
- package/dist/handlers/historicalWeatherHandler.d.ts +12 -0
- package/dist/handlers/historicalWeatherHandler.d.ts.map +1 -0
- package/dist/handlers/historicalWeatherHandler.js +188 -0
- package/dist/handlers/historicalWeatherHandler.js.map +1 -0
- package/dist/handlers/lightningHandler.d.ts +14 -0
- package/dist/handlers/lightningHandler.d.ts.map +1 -0
- package/dist/handlers/lightningHandler.js +258 -0
- package/dist/handlers/lightningHandler.js.map +1 -0
- package/dist/handlers/locationHandler.d.ts +12 -0
- package/dist/handlers/locationHandler.d.ts.map +1 -0
- package/dist/handlers/locationHandler.js +149 -0
- package/dist/handlers/locationHandler.js.map +1 -0
- package/dist/handlers/marineConditionsHandler.d.ts +13 -0
- package/dist/handlers/marineConditionsHandler.d.ts.map +1 -0
- package/dist/handlers/marineConditionsHandler.js +270 -0
- package/dist/handlers/marineConditionsHandler.js.map +1 -0
- package/dist/handlers/riverConditionsHandler.d.ts +11 -0
- package/dist/handlers/riverConditionsHandler.d.ts.map +1 -0
- package/dist/handlers/riverConditionsHandler.js +176 -0
- package/dist/handlers/riverConditionsHandler.js.map +1 -0
- package/dist/handlers/savedLocationsHandler.d.ts +50 -0
- package/dist/handlers/savedLocationsHandler.d.ts.map +1 -0
- package/dist/handlers/savedLocationsHandler.js +397 -0
- package/dist/handlers/savedLocationsHandler.js.map +1 -0
- package/dist/handlers/statusHandler.d.ts +12 -0
- package/dist/handlers/statusHandler.d.ts.map +1 -0
- package/dist/handlers/statusHandler.js +115 -0
- package/dist/handlers/statusHandler.js.map +1 -0
- package/dist/handlers/weatherImageryHandler.d.ts +14 -0
- package/dist/handlers/weatherImageryHandler.d.ts.map +1 -0
- package/dist/handlers/weatherImageryHandler.js +143 -0
- package/dist/handlers/weatherImageryHandler.js.map +1 -0
- package/dist/handlers/wildfireHandler.d.ts +11 -0
- package/dist/handlers/wildfireHandler.d.ts.map +1 -0
- package/dist/handlers/wildfireHandler.js +186 -0
- package/dist/handlers/wildfireHandler.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +735 -0
- package/dist/index.js.map +1 -0
- package/dist/services/blitzortung.d.ts +67 -0
- package/dist/services/blitzortung.d.ts.map +1 -0
- package/dist/services/blitzortung.js +475 -0
- package/dist/services/blitzortung.js.map +1 -0
- package/dist/services/geocoding.d.ts +57 -0
- package/dist/services/geocoding.d.ts.map +1 -0
- package/dist/services/geocoding.js +393 -0
- package/dist/services/geocoding.js.map +1 -0
- package/dist/services/locationStore.d.ts +62 -0
- package/dist/services/locationStore.d.ts.map +1 -0
- package/dist/services/locationStore.js +201 -0
- package/dist/services/locationStore.js.map +1 -0
- package/dist/services/ncei.d.ts +61 -0
- package/dist/services/ncei.d.ts.map +1 -0
- package/dist/services/ncei.js +126 -0
- package/dist/services/ncei.js.map +1 -0
- package/dist/services/nifc.d.ts +44 -0
- package/dist/services/nifc.d.ts.map +1 -0
- package/dist/services/nifc.js +159 -0
- package/dist/services/nifc.js.map +1 -0
- package/dist/services/noaa.d.ts +161 -0
- package/dist/services/noaa.d.ts.map +1 -0
- package/dist/services/noaa.js +681 -0
- package/dist/services/noaa.js.map +1 -0
- package/dist/services/nominatim.d.ts +62 -0
- package/dist/services/nominatim.d.ts.map +1 -0
- package/dist/services/nominatim.js +254 -0
- package/dist/services/nominatim.js.map +1 -0
- package/dist/services/openmeteo.d.ts +189 -0
- package/dist/services/openmeteo.d.ts.map +1 -0
- package/dist/services/openmeteo.js +936 -0
- package/dist/services/openmeteo.js.map +1 -0
- package/dist/services/rainviewer.d.ts +37 -0
- package/dist/services/rainviewer.d.ts.map +1 -0
- package/dist/services/rainviewer.js +115 -0
- package/dist/services/rainviewer.js.map +1 -0
- package/dist/types/imagery.d.ts +82 -0
- package/dist/types/imagery.d.ts.map +1 -0
- package/dist/types/imagery.js +6 -0
- package/dist/types/imagery.js.map +1 -0
- package/dist/types/lightning.d.ts +89 -0
- package/dist/types/lightning.d.ts.map +1 -0
- package/dist/types/lightning.js +6 -0
- package/dist/types/lightning.js.map +1 -0
- package/dist/types/noaa.d.ts +535 -0
- package/dist/types/noaa.d.ts.map +1 -0
- package/dist/types/noaa.js +5 -0
- package/dist/types/noaa.js.map +1 -0
- package/dist/types/nominatim.d.ts +72 -0
- package/dist/types/nominatim.d.ts.map +1 -0
- package/dist/types/nominatim.js +6 -0
- package/dist/types/nominatim.js.map +1 -0
- package/dist/types/openmeteo.d.ts +583 -0
- package/dist/types/openmeteo.d.ts.map +1 -0
- package/dist/types/openmeteo.js +6 -0
- package/dist/types/openmeteo.js.map +1 -0
- package/dist/types/savedLocations.d.ts +58 -0
- package/dist/types/savedLocations.d.ts.map +1 -0
- package/dist/types/savedLocations.js +5 -0
- package/dist/types/savedLocations.js.map +1 -0
- package/dist/types/wildfire.d.ts +83 -0
- package/dist/types/wildfire.d.ts.map +1 -0
- package/dist/types/wildfire.js +5 -0
- package/dist/types/wildfire.js.map +1 -0
- package/dist/utils/airQuality.d.ts +54 -0
- package/dist/utils/airQuality.d.ts.map +1 -0
- package/dist/utils/airQuality.js +251 -0
- package/dist/utils/airQuality.js.map +1 -0
- package/dist/utils/cache.d.ts +69 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +164 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/distance.d.ts +25 -0
- package/dist/utils/distance.d.ts.map +1 -0
- package/dist/utils/distance.js +40 -0
- package/dist/utils/distance.js.map +1 -0
- package/dist/utils/fireWeather.d.ts +76 -0
- package/dist/utils/fireWeather.d.ts.map +1 -0
- package/dist/utils/fireWeather.js +243 -0
- package/dist/utils/fireWeather.js.map +1 -0
- package/dist/utils/geography.d.ts +79 -0
- package/dist/utils/geography.d.ts.map +1 -0
- package/dist/utils/geography.js +266 -0
- package/dist/utils/geography.js.map +1 -0
- package/dist/utils/geohash.d.ts +62 -0
- package/dist/utils/geohash.d.ts.map +1 -0
- package/dist/utils/geohash.js +146 -0
- package/dist/utils/geohash.js.map +1 -0
- package/dist/utils/locationResolver.d.ts +34 -0
- package/dist/utils/locationResolver.d.ts.map +1 -0
- package/dist/utils/locationResolver.js +120 -0
- package/dist/utils/locationResolver.js.map +1 -0
- package/dist/utils/logger.d.ts +75 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +153 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/marine.d.ts +59 -0
- package/dist/utils/marine.d.ts.map +1 -0
- package/dist/utils/marine.js +215 -0
- package/dist/utils/marine.js.map +1 -0
- package/dist/utils/normals.d.ts +86 -0
- package/dist/utils/normals.d.ts.map +1 -0
- package/dist/utils/normals.js +223 -0
- package/dist/utils/normals.js.map +1 -0
- package/dist/utils/snow.d.ts +45 -0
- package/dist/utils/snow.d.ts.map +1 -0
- package/dist/utils/snow.js +144 -0
- package/dist/utils/snow.js.map +1 -0
- package/dist/utils/temperatureConversion.d.ts +12 -0
- package/dist/utils/temperatureConversion.d.ts.map +1 -0
- package/dist/utils/temperatureConversion.js +17 -0
- package/dist/utils/temperatureConversion.js.map +1 -0
- package/dist/utils/timezone.d.ts +56 -0
- package/dist/utils/timezone.d.ts.map +1 -0
- package/dist/utils/timezone.js +167 -0
- package/dist/utils/timezone.js.map +1 -0
- package/dist/utils/units.d.ts +69 -0
- package/dist/utils/units.d.ts.map +1 -0
- package/dist/utils/units.js +158 -0
- package/dist/utils/units.js.map +1 -0
- package/dist/utils/validation.d.ts +89 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +177 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/version.d.ts +15 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +24 -0
- package/dist/utils/version.js.map +1 -0
- 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}¶meterCd=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}¶meterCd=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}¶meterCd=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}¶meterCd=00060&siteStatus=active`;
|
|
677
|
+
const response = await this.usgsClient.get(url);
|
|
678
|
+
return response.data;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=noaa.js.map
|