hydroanomaly 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- hydroanomaly/__init__.py +55 -3
- hydroanomaly/sentinel_data.py +516 -0
- hydroanomaly/usgs_data.py +78 -0
- {hydroanomaly-0.3.0.dist-info → hydroanomaly-0.5.0.dist-info}/METADATA +7 -2
- hydroanomaly-0.5.0.dist-info/RECORD +11 -0
- hydroanomaly-0.3.0.dist-info/RECORD +0 -10
- {hydroanomaly-0.3.0.dist-info → hydroanomaly-0.5.0.dist-info}/WHEEL +0 -0
- {hydroanomaly-0.3.0.dist-info → hydroanomaly-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {hydroanomaly-0.3.0.dist-info → hydroanomaly-0.5.0.dist-info}/top_level.txt +0 -0
hydroanomaly/__init__.py
CHANGED
@@ -1,19 +1,71 @@
|
|
1
1
|
"""
|
2
2
|
HydroAnomaly
|
3
3
|
|
4
|
-
A Python package for hydro anomaly detection
|
4
|
+
A Python package for hydro anomaly detection, USGS data retrieval,
|
5
|
+
time series visualization, and Sentinel satellite data analysis.
|
5
6
|
"""
|
6
7
|
|
7
|
-
__version__ = "0.
|
8
|
+
__version__ = "0.5.0"
|
8
9
|
__author__ = "Your Name"
|
9
10
|
__email__ = "your.email@example.com"
|
10
11
|
|
11
12
|
# Import main modules for easy access
|
12
13
|
from .hello import greet
|
13
14
|
from .math_utils import add, multiply
|
14
|
-
from .usgs_data import get_usgs_data, USGSDataRetriever
|
15
|
+
from .usgs_data import get_usgs_data, get_usgs_simple, USGSDataRetriever
|
15
16
|
from .plotting import plot_usgs_data, plot_multiple_gages, quick_plot, WaterDataPlotter
|
16
17
|
|
18
|
+
# Base exports
|
19
|
+
__all__ = [
|
20
|
+
'greet',
|
21
|
+
'add', 'multiply',
|
22
|
+
'get_usgs_data', 'get_usgs_simple', 'USGSDataRetriever',
|
23
|
+
'plot_usgs_data', 'plot_multiple_gages', 'quick_plot', 'WaterDataPlotter',
|
24
|
+
'get_discharge', 'get_temperature', 'get_water_level'
|
25
|
+
]
|
26
|
+
|
27
|
+
# Try to import Sentinel functionality (optional GEE dependency)
|
28
|
+
try:
|
29
|
+
from .sentinel_data import (
|
30
|
+
SentinelDataRetriever,
|
31
|
+
SentinelConfig,
|
32
|
+
setup_gee_authentication,
|
33
|
+
initialize_gee,
|
34
|
+
get_water_area_time_series,
|
35
|
+
detect_water_changes
|
36
|
+
)
|
37
|
+
_SENTINEL_AVAILABLE = True
|
38
|
+
|
39
|
+
# Add Sentinel functions to exports
|
40
|
+
__all__.extend([
|
41
|
+
'SentinelDataRetriever',
|
42
|
+
'SentinelConfig',
|
43
|
+
'setup_gee_authentication',
|
44
|
+
'initialize_gee',
|
45
|
+
'get_water_area_time_series',
|
46
|
+
'detect_water_changes'
|
47
|
+
])
|
48
|
+
|
49
|
+
except ImportError as e:
|
50
|
+
print("⚠️ Sentinel data functionality not available.")
|
51
|
+
print("💡 To use Google Earth Engine features, install:")
|
52
|
+
print(" pip install earthengine-api")
|
53
|
+
print(" Then authenticate: earthengine authenticate")
|
54
|
+
_SENTINEL_AVAILABLE = False
|
55
|
+
|
56
|
+
# Create placeholder functions for better error messages
|
57
|
+
def setup_gee_authentication(*args, **kwargs):
|
58
|
+
raise ImportError("Google Earth Engine not available. Install with: pip install earthengine-api")
|
59
|
+
|
60
|
+
def initialize_gee(*args, **kwargs):
|
61
|
+
raise ImportError("Google Earth Engine not available. Install with: pip install earthengine-api")
|
62
|
+
|
63
|
+
def get_water_area_time_series(*args, **kwargs):
|
64
|
+
raise ImportError("Google Earth Engine not available. Install with: pip install earthengine-api")
|
65
|
+
|
66
|
+
def detect_water_changes(*args, **kwargs):
|
67
|
+
raise ImportError("Google Earth Engine not available. Install with: pip install earthengine-api")
|
68
|
+
|
17
69
|
# Convenience functions for common use cases
|
18
70
|
def get_discharge(gage_number, start_date, end_date, save_file=None):
|
19
71
|
"""
|
@@ -0,0 +1,516 @@
|
|
1
|
+
"""
|
2
|
+
Google Earth Engine Sentinel Data Retrieval Module
|
3
|
+
|
4
|
+
This module provides functionality to retrieve and process Sentinel satellite data
|
5
|
+
from Google Earth Engine for water-related analysis and monitoring.
|
6
|
+
|
7
|
+
Features:
|
8
|
+
- User-configurable authentication and project setup
|
9
|
+
- Sentinel-2 surface reflectance data retrieval with custom masking
|
10
|
+
- Water body analysis using spectral indices
|
11
|
+
- Time series extraction for specific locations
|
12
|
+
- Anomaly detection capabilities
|
13
|
+
- Interactive mapping and visualization
|
14
|
+
|
15
|
+
Requirements:
|
16
|
+
- Google Earth Engine Python API (earthengine-api)
|
17
|
+
- geemap for visualization
|
18
|
+
- Valid GEE account and authentication
|
19
|
+
"""
|
20
|
+
|
21
|
+
import ee
|
22
|
+
import pandas as pd
|
23
|
+
import numpy as np
|
24
|
+
import geemap
|
25
|
+
import warnings
|
26
|
+
from datetime import datetime, timedelta
|
27
|
+
from typing import Dict, List, Tuple, Optional, Union
|
28
|
+
from tqdm import tqdm
|
29
|
+
import matplotlib.pyplot as plt
|
30
|
+
import seaborn as sns
|
31
|
+
|
32
|
+
warnings.filterwarnings('ignore')
|
33
|
+
|
34
|
+
|
35
|
+
class SentinelConfig:
|
36
|
+
"""
|
37
|
+
Configuration class for Sentinel data retrieval.
|
38
|
+
Users can customize these settings for their specific needs.
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(self):
|
42
|
+
# Authentication settings (user must configure)
|
43
|
+
self.gee_project = None # User must set their GEE project ID
|
44
|
+
|
45
|
+
# Default masks to apply
|
46
|
+
self.masks_to_apply = [
|
47
|
+
"water", # Keep only SCL = 6 (water)
|
48
|
+
"no_cloud_shadow", # Remove cloud shadow (SCL = 3)
|
49
|
+
"no_clouds", # Remove clouds (SCL = 8, 9, 10)
|
50
|
+
"no_snow_ice", # Remove snow/ice (SCL = 11)
|
51
|
+
"no_saturated", # Remove saturated/defective pixels (SCL = 1)
|
52
|
+
]
|
53
|
+
|
54
|
+
# Processing parameters
|
55
|
+
self.cloudy_pixel_percentage = 20 # Max allowed cloud percentage
|
56
|
+
self.buffer_meters = 20 # Buffer size around point, in meters
|
57
|
+
self.scale = 20 # Processing scale in meters
|
58
|
+
|
59
|
+
# Sentinel-2 bands to retrieve
|
60
|
+
self.bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B9', 'B11', 'B12', 'SCL']
|
61
|
+
|
62
|
+
# Visualization parameters
|
63
|
+
self.ndwi_vis = {
|
64
|
+
'min': 0,
|
65
|
+
'max': 1,
|
66
|
+
'palette': ['white', 'cyan', 'blue']
|
67
|
+
}
|
68
|
+
|
69
|
+
def set_project(self, project_id: str):
|
70
|
+
"""Set the Google Earth Engine project ID."""
|
71
|
+
self.gee_project = project_id
|
72
|
+
|
73
|
+
def set_masks(self, masks: List[str]):
|
74
|
+
"""Set the masks to apply during processing."""
|
75
|
+
self.masks_to_apply = masks
|
76
|
+
|
77
|
+
def set_buffer(self, buffer_meters: int):
|
78
|
+
"""Set the buffer size around points."""
|
79
|
+
self.buffer_meters = buffer_meters
|
80
|
+
|
81
|
+
def set_cloud_threshold(self, threshold: int):
|
82
|
+
"""Set the maximum cloud percentage."""
|
83
|
+
self.cloudy_pixel_percentage = threshold
|
84
|
+
|
85
|
+
|
86
|
+
class SentinelDataRetriever:
|
87
|
+
"""
|
88
|
+
A class to retrieve and process Sentinel satellite data from Google Earth Engine.
|
89
|
+
"""
|
90
|
+
|
91
|
+
def __init__(self, config: Optional[SentinelConfig] = None):
|
92
|
+
"""
|
93
|
+
Initialize the Sentinel Data Retriever.
|
94
|
+
|
95
|
+
Parameters:
|
96
|
+
-----------
|
97
|
+
config : SentinelConfig, optional
|
98
|
+
Configuration object with user settings
|
99
|
+
"""
|
100
|
+
self.config = config if config else SentinelConfig()
|
101
|
+
self.initialized = False
|
102
|
+
|
103
|
+
def authenticate_and_initialize(self, project_id: str, force_auth: bool = False):
|
104
|
+
"""
|
105
|
+
Authenticate with Google Earth Engine and initialize.
|
106
|
+
|
107
|
+
Parameters:
|
108
|
+
-----------
|
109
|
+
project_id : str
|
110
|
+
Your Google Earth Engine project ID
|
111
|
+
force_auth : bool, default False
|
112
|
+
Whether to force re-authentication
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
--------
|
116
|
+
bool : True if successful, False otherwise
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
if force_auth:
|
120
|
+
print("🔐 Starting authentication process...")
|
121
|
+
ee.Authenticate()
|
122
|
+
|
123
|
+
print(f"🚀 Initializing Google Earth Engine with project: {project_id}")
|
124
|
+
ee.Initialize(project=project_id)
|
125
|
+
|
126
|
+
self.config.set_project(project_id)
|
127
|
+
self.initialized = True
|
128
|
+
|
129
|
+
print("✅ Google Earth Engine initialized successfully!")
|
130
|
+
return True
|
131
|
+
|
132
|
+
except Exception as e:
|
133
|
+
print(f"❌ Failed to initialize Google Earth Engine: {e}")
|
134
|
+
print("💡 Make sure you have:")
|
135
|
+
print(" 1. Valid GEE account access")
|
136
|
+
print(" 2. Correct project ID")
|
137
|
+
print(" 3. Run: earthengine authenticate (if needed)")
|
138
|
+
return False
|
139
|
+
|
140
|
+
def create_point_geometry(self, longitude: float, latitude: float) -> ee.Geometry.Point:
|
141
|
+
"""
|
142
|
+
Create a point geometry for data extraction.
|
143
|
+
|
144
|
+
Parameters:
|
145
|
+
-----------
|
146
|
+
longitude : float
|
147
|
+
Longitude coordinate
|
148
|
+
latitude : float
|
149
|
+
Latitude coordinate
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
--------
|
153
|
+
ee.Geometry.Point : Earth Engine point geometry
|
154
|
+
"""
|
155
|
+
if not self.initialized:
|
156
|
+
raise RuntimeError("GEE not initialized. Call authenticate_and_initialize() first.")
|
157
|
+
|
158
|
+
return ee.Geometry.Point([longitude, latitude])
|
159
|
+
|
160
|
+
def get_available_dates(self, point: ee.Geometry.Point, start_date: str, end_date: str) -> List[str]:
|
161
|
+
"""
|
162
|
+
Get available Sentinel-2 dates for a location.
|
163
|
+
|
164
|
+
Parameters:
|
165
|
+
-----------
|
166
|
+
point : ee.Geometry.Point
|
167
|
+
Location point
|
168
|
+
start_date : str
|
169
|
+
Start date in 'YYYY-MM-DD' format
|
170
|
+
end_date : str
|
171
|
+
End date in 'YYYY-MM-DD' format
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
--------
|
175
|
+
List[str] : List of available dates
|
176
|
+
"""
|
177
|
+
buffered_point = point.buffer(self.config.buffer_meters)
|
178
|
+
|
179
|
+
s2 = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
|
180
|
+
.filterBounds(buffered_point)
|
181
|
+
.filterDate(start_date, end_date))
|
182
|
+
|
183
|
+
def get_date(image):
|
184
|
+
return ee.Feature(None, {'date': image.date().format('YYYY-MM-dd HH:mm:ss')})
|
185
|
+
|
186
|
+
dates_fc = ee.FeatureCollection(s2.map(get_date))
|
187
|
+
date_list = dates_fc.aggregate_array('date').getInfo()
|
188
|
+
|
189
|
+
print(f'Available Sentinel-2 dates at this site:')
|
190
|
+
for d in date_list:
|
191
|
+
print(d)
|
192
|
+
print(f"Total: {len(date_list)} images from {start_date} to {end_date}")
|
193
|
+
|
194
|
+
return date_list
|
195
|
+
|
196
|
+
def dynamic_scl_mask(self, image):
|
197
|
+
"""
|
198
|
+
Apply dynamic Scene Classification Layer (SCL) masks based on user configuration.
|
199
|
+
|
200
|
+
Parameters:
|
201
|
+
-----------
|
202
|
+
image : ee.Image
|
203
|
+
Sentinel-2 image
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
--------
|
207
|
+
ee.Image : Masked image
|
208
|
+
"""
|
209
|
+
scl = image.select('SCL')
|
210
|
+
mask = ee.Image.constant(1)
|
211
|
+
|
212
|
+
if "water" in self.config.masks_to_apply:
|
213
|
+
mask = mask.And(scl.eq(6))
|
214
|
+
if "no_cloud_shadow" in self.config.masks_to_apply:
|
215
|
+
mask = mask.And(scl.neq(3))
|
216
|
+
if "no_clouds" in self.config.masks_to_apply:
|
217
|
+
cloud_mask = scl.neq(8).And(scl.neq(9)).And(scl.neq(10))
|
218
|
+
mask = mask.And(cloud_mask)
|
219
|
+
if "no_snow_ice" in self.config.masks_to_apply:
|
220
|
+
mask = mask.And(scl.neq(11))
|
221
|
+
if "no_saturated" in self.config.masks_to_apply:
|
222
|
+
mask = mask.And(scl.neq(1))
|
223
|
+
|
224
|
+
return image.updateMask(mask)
|
225
|
+
|
226
|
+
def extract_time_series(self,
|
227
|
+
longitude: float,
|
228
|
+
latitude: float,
|
229
|
+
start_date: str,
|
230
|
+
end_date: str) -> pd.DataFrame:
|
231
|
+
"""
|
232
|
+
Extract time series data for a specific location.
|
233
|
+
|
234
|
+
Parameters:
|
235
|
+
-----------
|
236
|
+
longitude : float
|
237
|
+
Longitude coordinate
|
238
|
+
latitude : float
|
239
|
+
Latitude coordinate
|
240
|
+
start_date : str
|
241
|
+
Start date in 'YYYY-MM-DD' format
|
242
|
+
end_date : str
|
243
|
+
End date in 'YYYY-MM-DD' format
|
244
|
+
|
245
|
+
Returns:
|
246
|
+
--------
|
247
|
+
pd.DataFrame : Time series data with spectral bands
|
248
|
+
"""
|
249
|
+
if not self.initialized:
|
250
|
+
raise RuntimeError("GEE not initialized. Call authenticate_and_initialize() first.")
|
251
|
+
|
252
|
+
print(f"🛰️ Extracting Sentinel-2 time series...")
|
253
|
+
print(f"📍 Location: {latitude:.6f}°N, {longitude:.6f}°W")
|
254
|
+
print(f"📅 Period: {start_date} to {end_date}")
|
255
|
+
print(f"🎭 Masks: {self.config.masks_to_apply}")
|
256
|
+
|
257
|
+
point = self.create_point_geometry(longitude, latitude)
|
258
|
+
buffered_point = point.buffer(self.config.buffer_meters)
|
259
|
+
|
260
|
+
# Load and filter image collection
|
261
|
+
s2 = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
|
262
|
+
.filterBounds(buffered_point)
|
263
|
+
.filterDate(start_date, end_date)
|
264
|
+
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', self.config.cloudy_pixel_percentage))
|
265
|
+
.select(self.config.bands)
|
266
|
+
.map(self.dynamic_scl_mask))
|
267
|
+
|
268
|
+
def extract_features(image):
|
269
|
+
date = image.date().format('YYYY-MM-dd HH:mm:ss')
|
270
|
+
values = image.reduceRegion(
|
271
|
+
reducer=ee.Reducer.mean(),
|
272
|
+
geometry=buffered_point,
|
273
|
+
scale=self.config.scale,
|
274
|
+
maxPixels=1e8
|
275
|
+
)
|
276
|
+
return ee.Feature(None, values.set('date', date))
|
277
|
+
|
278
|
+
print("🔄 Processing images and extracting features...")
|
279
|
+
features = s2.map(extract_features)
|
280
|
+
fc = ee.FeatureCollection(features).filter(ee.Filter.notNull(['B2']))
|
281
|
+
|
282
|
+
# Convert to pandas DataFrame
|
283
|
+
data = fc.getInfo()
|
284
|
+
print(f"Number of features: {len(data['features'])}")
|
285
|
+
|
286
|
+
rows = [f['properties'] for f in data['features']]
|
287
|
+
df = pd.DataFrame(rows)
|
288
|
+
|
289
|
+
if len(df) > 0:
|
290
|
+
df['date'] = pd.to_datetime(df['date'])
|
291
|
+
df = df.sort_values('date').reset_index(drop=True)
|
292
|
+
|
293
|
+
print(f"✅ Retrieved {len(df)} records")
|
294
|
+
print(f"📊 Date range: {df['date'].min()} to {df['date'].max()}")
|
295
|
+
else:
|
296
|
+
print("⚠️ No data retrieved. Try adjusting date range or masks.")
|
297
|
+
|
298
|
+
return df
|
299
|
+
|
300
|
+
def create_interactive_map(self,
|
301
|
+
longitude: float,
|
302
|
+
latitude: float,
|
303
|
+
start_date: str,
|
304
|
+
end_date: str,
|
305
|
+
zoom: int = 15) -> geemap.Map:
|
306
|
+
"""
|
307
|
+
Create an interactive map showing the location and NDWI visualization.
|
308
|
+
|
309
|
+
Parameters:
|
310
|
+
-----------
|
311
|
+
longitude : float
|
312
|
+
Longitude coordinate
|
313
|
+
latitude : float
|
314
|
+
Latitude coordinate
|
315
|
+
start_date : str
|
316
|
+
Start date for image composite
|
317
|
+
end_date : str
|
318
|
+
End date for image composite
|
319
|
+
zoom : int, default 15
|
320
|
+
Map zoom level
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
--------
|
324
|
+
geemap.Map : Interactive map object
|
325
|
+
"""
|
326
|
+
if not self.initialized:
|
327
|
+
raise RuntimeError("GEE not initialized. Call authenticate_and_initialize() first.")
|
328
|
+
|
329
|
+
print("🗺️ Creating interactive map...")
|
330
|
+
|
331
|
+
point = self.create_point_geometry(longitude, latitude)
|
332
|
+
buffer = point.buffer(self.config.buffer_meters)
|
333
|
+
|
334
|
+
# Create median composite
|
335
|
+
image = (ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
336
|
+
.filterBounds(buffer)
|
337
|
+
.filterDate(start_date, end_date)
|
338
|
+
.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", self.config.cloudy_pixel_percentage))
|
339
|
+
.median())
|
340
|
+
|
341
|
+
# Calculate NDWI
|
342
|
+
ndwi = image.normalizedDifference(["B3", "B8"]).rename("NDWI")
|
343
|
+
|
344
|
+
# Create map
|
345
|
+
Map = geemap.Map()
|
346
|
+
Map.centerObject(buffer, zoom=zoom)
|
347
|
+
Map.addLayer(ndwi, self.config.ndwi_vis, "NDWI (Water)")
|
348
|
+
Map.addLayer(point, {'color': 'yellow'}, 'Point')
|
349
|
+
Map.addLayer(buffer, {'color': 'red'}, f'Buffer ({self.config.buffer_meters}m)')
|
350
|
+
|
351
|
+
print(f"✅ Map created with NDWI visualization")
|
352
|
+
print(f"🎯 Centered on: {latitude:.6f}°N, {longitude:.6f}°W")
|
353
|
+
|
354
|
+
return Map
|
355
|
+
|
356
|
+
def calculate_water_indices(self, df: pd.DataFrame) -> pd.DataFrame:
|
357
|
+
"""
|
358
|
+
Calculate water-related spectral indices from the DataFrame.
|
359
|
+
|
360
|
+
Parameters:
|
361
|
+
-----------
|
362
|
+
df : pd.DataFrame
|
363
|
+
DataFrame with Sentinel-2 band data
|
364
|
+
|
365
|
+
Returns:
|
366
|
+
--------
|
367
|
+
pd.DataFrame : DataFrame with added indices
|
368
|
+
"""
|
369
|
+
if len(df) == 0:
|
370
|
+
return df
|
371
|
+
|
372
|
+
df_copy = df.copy()
|
373
|
+
|
374
|
+
# NDWI (Normalized Difference Water Index)
|
375
|
+
if 'B3' in df_copy.columns and 'B8' in df_copy.columns:
|
376
|
+
df_copy['NDWI'] = (df_copy['B3'] - df_copy['B8']) / (df_copy['B3'] + df_copy['B8'])
|
377
|
+
|
378
|
+
# MNDWI (Modified NDWI)
|
379
|
+
if 'B3' in df_copy.columns and 'B11' in df_copy.columns:
|
380
|
+
df_copy['MNDWI'] = (df_copy['B3'] - df_copy['B11']) / (df_copy['B3'] + df_copy['B11'])
|
381
|
+
|
382
|
+
# NDVI (Normalized Difference Vegetation Index)
|
383
|
+
if 'B4' in df_copy.columns and 'B8' in df_copy.columns:
|
384
|
+
df_copy['NDVI'] = (df_copy['B8'] - df_copy['B4']) / (df_copy['B8'] + df_copy['B4'])
|
385
|
+
|
386
|
+
# Water turbidity proxy
|
387
|
+
if 'B4' in df_copy.columns and 'B2' in df_copy.columns:
|
388
|
+
df_copy['TURBIDITY'] = df_copy['B4'] / df_copy['B2']
|
389
|
+
|
390
|
+
print("✅ Water indices calculated: NDWI, MNDWI, NDVI, TURBIDITY")
|
391
|
+
return df_copy
|
392
|
+
|
393
|
+
def plot_time_series(self, df: pd.DataFrame, indices: List[str] = None, save_path: str = None):
|
394
|
+
"""
|
395
|
+
Plot time series of spectral indices.
|
396
|
+
|
397
|
+
Parameters:
|
398
|
+
-----------
|
399
|
+
df : pd.DataFrame
|
400
|
+
DataFrame with time series data
|
401
|
+
indices : List[str], optional
|
402
|
+
List of indices to plot (default: ['NDWI', 'MNDWI', 'NDVI'])
|
403
|
+
save_path : str, optional
|
404
|
+
Path to save the plot
|
405
|
+
"""
|
406
|
+
if len(df) == 0:
|
407
|
+
print("⚠️ No data to plot")
|
408
|
+
return
|
409
|
+
|
410
|
+
if indices is None:
|
411
|
+
indices = ['NDWI', 'MNDWI', 'NDVI']
|
412
|
+
|
413
|
+
# Filter available indices
|
414
|
+
available_indices = [idx for idx in indices if idx in df.columns]
|
415
|
+
|
416
|
+
if not available_indices:
|
417
|
+
print("⚠️ No indices available to plot")
|
418
|
+
return
|
419
|
+
|
420
|
+
fig, axes = plt.subplots(len(available_indices), 1, figsize=(12, 4*len(available_indices)))
|
421
|
+
if len(available_indices) == 1:
|
422
|
+
axes = [axes]
|
423
|
+
|
424
|
+
for i, idx in enumerate(available_indices):
|
425
|
+
axes[i].plot(df['date'], df[idx], 'o-', markersize=4, linewidth=1.5)
|
426
|
+
axes[i].set_ylabel(idx)
|
427
|
+
axes[i].set_title(f'{idx} Time Series')
|
428
|
+
axes[i].grid(True, alpha=0.3)
|
429
|
+
axes[i].tick_params(axis='x', rotation=45)
|
430
|
+
|
431
|
+
plt.tight_layout()
|
432
|
+
|
433
|
+
if save_path:
|
434
|
+
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
435
|
+
print(f"📊 Plot saved: {save_path}")
|
436
|
+
|
437
|
+
plt.show()
|
438
|
+
|
439
|
+
|
440
|
+
# Convenience functions for easy access
|
441
|
+
def setup_gee_authentication(project_id: str, force_auth: bool = False) -> SentinelDataRetriever:
|
442
|
+
"""
|
443
|
+
Set up Google Earth Engine authentication and return a configured retriever.
|
444
|
+
|
445
|
+
Parameters:
|
446
|
+
-----------
|
447
|
+
project_id : str
|
448
|
+
Your Google Earth Engine project ID
|
449
|
+
force_auth : bool, default False
|
450
|
+
Whether to force re-authentication
|
451
|
+
|
452
|
+
Returns:
|
453
|
+
--------
|
454
|
+
SentinelDataRetriever : Configured and initialized retriever
|
455
|
+
|
456
|
+
Example:
|
457
|
+
--------
|
458
|
+
>>> retriever = setup_gee_authentication('your-project-id')
|
459
|
+
"""
|
460
|
+
config = SentinelConfig()
|
461
|
+
retriever = SentinelDataRetriever(config)
|
462
|
+
|
463
|
+
if retriever.authenticate_and_initialize(project_id, force_auth):
|
464
|
+
return retriever
|
465
|
+
else:
|
466
|
+
raise RuntimeError("Failed to initialize Google Earth Engine")
|
467
|
+
|
468
|
+
|
469
|
+
def get_sentinel_time_series(project_id: str,
|
470
|
+
longitude: float,
|
471
|
+
latitude: float,
|
472
|
+
start_date: str,
|
473
|
+
end_date: str,
|
474
|
+
masks: List[str] = None,
|
475
|
+
buffer_meters: int = 20) -> pd.DataFrame:
|
476
|
+
"""
|
477
|
+
Quick function to get Sentinel-2 time series data.
|
478
|
+
|
479
|
+
Parameters:
|
480
|
+
-----------
|
481
|
+
project_id : str
|
482
|
+
Your Google Earth Engine project ID
|
483
|
+
longitude : float
|
484
|
+
Longitude coordinate
|
485
|
+
latitude : float
|
486
|
+
Latitude coordinate
|
487
|
+
start_date : str
|
488
|
+
Start date in 'YYYY-MM-DD' format
|
489
|
+
end_date : str
|
490
|
+
End date in 'YYYY-MM-DD' format
|
491
|
+
masks : List[str], optional
|
492
|
+
Custom masks to apply
|
493
|
+
buffer_meters : int, default 20
|
494
|
+
Buffer size around point
|
495
|
+
|
496
|
+
Returns:
|
497
|
+
--------
|
498
|
+
pd.DataFrame : Time series data
|
499
|
+
|
500
|
+
Example:
|
501
|
+
--------
|
502
|
+
>>> df = get_sentinel_time_series(
|
503
|
+
... 'your-project-id',
|
504
|
+
... -95.0644278, 29.7785861,
|
505
|
+
... '2023-01-01', '2023-12-31'
|
506
|
+
... )
|
507
|
+
"""
|
508
|
+
retriever = setup_gee_authentication(project_id)
|
509
|
+
|
510
|
+
if masks:
|
511
|
+
retriever.config.set_masks(masks)
|
512
|
+
if buffer_meters != 20:
|
513
|
+
retriever.config.set_buffer(buffer_meters)
|
514
|
+
|
515
|
+
df = retriever.extract_time_series(longitude, latitude, start_date, end_date)
|
516
|
+
return retriever.calculate_water_indices(df)
|
hydroanomaly/usgs_data.py
CHANGED
@@ -309,3 +309,81 @@ def get_usgs_data(
|
|
309
309
|
retriever.save_data(data, save_to_file, parameter_name)
|
310
310
|
|
311
311
|
return data
|
312
|
+
|
313
|
+
|
314
|
+
def get_usgs_simple(site_number: str, parameter_code: str, start_date: str, end_date: str, plot: bool = True) -> pd.DataFrame:
|
315
|
+
"""
|
316
|
+
🚀 SIMPLE USGS DATA RETRIEVAL WITH AUTOMATIC PLOTTING
|
317
|
+
|
318
|
+
This is the main function users should use for easy USGS data retrieval.
|
319
|
+
Just provide site, parameter, dates and get data + plot automatically!
|
320
|
+
|
321
|
+
Args:
|
322
|
+
site_number (str): USGS site number (e.g., "294643095035200")
|
323
|
+
parameter_code (str): Parameter code (e.g., "63680" for turbidity)
|
324
|
+
start_date (str): Start date as "YYYY-MM-DD"
|
325
|
+
end_date (str): End date as "YYYY-MM-DD"
|
326
|
+
plot (bool): Whether to automatically create a plot (default: True)
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
pd.DataFrame: Time series data with datetime index and values
|
330
|
+
|
331
|
+
Example:
|
332
|
+
>>> data = get_usgs_simple("294643095035200", "63680", "2020-01-01", "2024-12-30")
|
333
|
+
>>> # Automatically retrieves turbidity data and shows a plot!
|
334
|
+
"""
|
335
|
+
import matplotlib.pyplot as plt
|
336
|
+
import matplotlib.dates as mdates
|
337
|
+
from datetime import datetime
|
338
|
+
|
339
|
+
print("🌊 USGS Data Retrieval - Simple Mode")
|
340
|
+
print("=" * 50)
|
341
|
+
print(f"📍 Site: {site_number}")
|
342
|
+
print(f"📊 Parameter: {parameter_code}")
|
343
|
+
print(f"📅 Date Range: {start_date} to {end_date}")
|
344
|
+
print()
|
345
|
+
|
346
|
+
try:
|
347
|
+
# Get the data using existing function
|
348
|
+
retriever = USGSDataRetriever()
|
349
|
+
data = retriever.retrieve_data(site_number, parameter_code, start_date, end_date)
|
350
|
+
|
351
|
+
if data is None or len(data) == 0:
|
352
|
+
print("❌ No data retrieved")
|
353
|
+
return pd.DataFrame()
|
354
|
+
|
355
|
+
# Data summary
|
356
|
+
print(f"✅ Success! Retrieved {len(data)} data points")
|
357
|
+
print(f"📈 Data range: {data.index.min()} to {data.index.max()}")
|
358
|
+
print(f"📊 Values: {data.iloc[:, 0].min():.2f} to {data.iloc[:, 0].max():.2f}")
|
359
|
+
print()
|
360
|
+
|
361
|
+
# Create automatic plot if requested
|
362
|
+
if plot:
|
363
|
+
plt.figure(figsize=(12, 6))
|
364
|
+
plt.plot(data.index, data.iloc[:, 0], linewidth=1, alpha=0.8)
|
365
|
+
|
366
|
+
# Format the plot
|
367
|
+
plt.title(f'USGS Time Series Data\nSite: {site_number} | Parameter: {parameter_code}',
|
368
|
+
fontsize=14, fontweight='bold')
|
369
|
+
plt.xlabel('Date', fontsize=12)
|
370
|
+
plt.ylabel(f'Parameter {parameter_code}', fontsize=12)
|
371
|
+
plt.grid(True, alpha=0.3)
|
372
|
+
|
373
|
+
# Format x-axis dates
|
374
|
+
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
|
375
|
+
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6))
|
376
|
+
plt.xticks(rotation=45)
|
377
|
+
|
378
|
+
# Improve layout
|
379
|
+
plt.tight_layout()
|
380
|
+
plt.show()
|
381
|
+
|
382
|
+
print("📊 Plot created successfully!")
|
383
|
+
|
384
|
+
return data
|
385
|
+
|
386
|
+
except Exception as e:
|
387
|
+
print(f"❌ Error: {str(e)}")
|
388
|
+
print("💡 Please check your inputs and try again")
|
389
|
+
return pd.DataFrame()
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydroanomaly
|
3
|
-
Version: 0.
|
4
|
-
Summary: A Python package for hydro anomaly detection
|
3
|
+
Version: 0.5.0
|
4
|
+
Summary: A Python package for hydro anomaly detection with simple USGS data retrieval
|
5
5
|
Home-page: https://github.com/yourusername/hydroanomaly
|
6
6
|
Author: Your Name
|
7
7
|
Author-email: Your Name <your.email@example.com>
|
@@ -20,6 +20,11 @@ Requires-Dist: numpy>=1.20.0
|
|
20
20
|
Requires-Dist: requests>=2.25.1
|
21
21
|
Requires-Dist: matplotlib>=3.3.0
|
22
22
|
Requires-Dist: seaborn>=0.11.0
|
23
|
+
Requires-Dist: earthengine-api>=0.1.300
|
24
|
+
Requires-Dist: geemap>=0.20.0
|
25
|
+
Requires-Dist: tqdm>=4.60.0
|
26
|
+
Requires-Dist: scipy>=1.7.0
|
27
|
+
Requires-Dist: scikit-learn>=1.0.0
|
23
28
|
Provides-Extra: dev
|
24
29
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
25
30
|
Requires-Dist: black>=21.0; extra == "dev"
|
@@ -0,0 +1,11 @@
|
|
1
|
+
hydroanomaly/__init__.py,sha256=082rQhjdrRTN4uSSsdQB6emJa9OFS8U2O8omxFgGa9Q,4920
|
2
|
+
hydroanomaly/hello.py,sha256=AhK7UKF_3TyZcWL4IDlZq_BXdKQzUP-is-jv59fgqk4,566
|
3
|
+
hydroanomaly/math_utils.py,sha256=CDOGWAiRlb2PK5SNFysumnzp7_LbZ9aleHLR_3lsGrs,856
|
4
|
+
hydroanomaly/plotting.py,sha256=YZW6-Sb_IrhbHKFeoh1d86Ef4Ev5Gpq55lEv8XX0v20,13504
|
5
|
+
hydroanomaly/sentinel_data.py,sha256=C5T1ycyTcAGvR6KEukDHJe2kEDbFXgh0yVXi8QrjFXs,17870
|
6
|
+
hydroanomaly/usgs_data.py,sha256=ig7Diht106-gEiGFNfKzbj-mUn1RiC4GYoOKacz1Uaw,14674
|
7
|
+
hydroanomaly-0.5.0.dist-info/licenses/LICENSE,sha256=OphKV48tcMv6ep-7j-8T6nycykPT0g8ZlMJ9zbGvdPs,1066
|
8
|
+
hydroanomaly-0.5.0.dist-info/METADATA,sha256=1CD8vliaRj34Jr8bT1gRXd_iyIhKCSWh7_Ji6dQkoQg,11873
|
9
|
+
hydroanomaly-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
+
hydroanomaly-0.5.0.dist-info/top_level.txt,sha256=t-5Lc-eTLlkxIhR_N1Cpp6_YZafKS3xLLk9D2CtbE7o,13
|
11
|
+
hydroanomaly-0.5.0.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
hydroanomaly/__init__.py,sha256=orCkJ2g57zR9o5RUekkh0eOUvhalQVbRro5Oxm-iYZk,2997
|
2
|
-
hydroanomaly/hello.py,sha256=AhK7UKF_3TyZcWL4IDlZq_BXdKQzUP-is-jv59fgqk4,566
|
3
|
-
hydroanomaly/math_utils.py,sha256=CDOGWAiRlb2PK5SNFysumnzp7_LbZ9aleHLR_3lsGrs,856
|
4
|
-
hydroanomaly/plotting.py,sha256=YZW6-Sb_IrhbHKFeoh1d86Ef4Ev5Gpq55lEv8XX0v20,13504
|
5
|
-
hydroanomaly/usgs_data.py,sha256=zUvfu3go-7cQuFtD8Hbm7pABpw_RPWuJxE66NhxYmIU,11631
|
6
|
-
hydroanomaly-0.3.0.dist-info/licenses/LICENSE,sha256=OphKV48tcMv6ep-7j-8T6nycykPT0g8ZlMJ9zbGvdPs,1066
|
7
|
-
hydroanomaly-0.3.0.dist-info/METADATA,sha256=3NDnQzVH84RCiRpHf1SyQf5OHDIGMsMTEa-tlI822p4,11680
|
8
|
-
hydroanomaly-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
9
|
-
hydroanomaly-0.3.0.dist-info/top_level.txt,sha256=t-5Lc-eTLlkxIhR_N1Cpp6_YZafKS3xLLk9D2CtbE7o,13
|
10
|
-
hydroanomaly-0.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|