RadGEEToolbox 1.7.0__tar.gz → 1.7.2__tar.gz

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 (24) hide show
  1. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/PKG-INFO +8 -6
  2. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/README.md +7 -5
  3. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/GenericCollection.py +123 -32
  4. radgeetoolbox-1.7.2/RadGEEToolbox/GetPalette.py +169 -0
  5. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/LandsatCollection.py +465 -37
  6. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/Sentinel1Collection.py +607 -19
  7. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/Sentinel2Collection.py +468 -31
  8. radgeetoolbox-1.7.2/RadGEEToolbox/VisParams.py +139 -0
  9. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/__init__.py +1 -1
  10. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/PKG-INFO +8 -6
  11. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/pyproject.toml +1 -1
  12. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/setup.py +1 -1
  13. radgeetoolbox-1.7.0/RadGEEToolbox/GetPalette.py +0 -120
  14. radgeetoolbox-1.7.0/RadGEEToolbox/VisParams.py +0 -221
  15. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/LICENSE.txt +0 -0
  16. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/CollectionStitch.py +0 -0
  17. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/SOURCES.txt +0 -0
  18. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/dependency_links.txt +0 -0
  19. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/requires.txt +0 -0
  20. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/top_level.txt +0 -0
  21. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/setup.cfg +0 -0
  22. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/tests/test_landsat_collection.py +0 -0
  23. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/tests/test_sentinel1_collection.py +0 -0
  24. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/tests/test_sentinel2_collection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: RadGEEToolbox
3
- Version: 1.7.0
3
+ Version: 1.7.2
4
4
  Summary: Streamlined Multispectral & SAR Analysis for Google Earth Engine Python API
5
5
  Home-page: https://github.com/radwinskis/RadGEEToolbox
6
6
  Author: Mark Radwin
@@ -43,7 +43,7 @@ Designed for both new and advanced users of Google Earth Engine, RadGEEToolbox m
43
43
 
44
44
  Although similar packages exist (eemont, geetools, etc.), `RadGEEToolbox` extends functionality and provides cohesive, chainable methods for research oriented projects working with Landsat TM & OLI, Sentinel-1 SAR, and/or Sentinel-2 MSI datasets (Table 1). The ultimate goal of `RadGEEToolbox` is to make satellite image processing easier and faster for real world applications relying on the most commonly utilized remote sensing platforms.
45
45
 
46
- As of version `1.7.0`, `RadGEEToolbox` supports any generic image collection via the `GenericCollection` module which allows for utilization of the same data management, temporal reduction, zonal statistics, and data export tools available for the `LandsatCollection`, `Sentinel1Collection`, and `Sentinel2Collection` modules. This allows users to provide their own image collection of choice, such as PRISM or MODIS data, to benefit from the tools available with `RadGEEToolbox`.
46
+ As of version `1.7.2`, `RadGEEToolbox` supports any generic image collection via the `GenericCollection` module which allows for utilization of the same data management, temporal reduction, zonal statistics, and data export tools available for the `LandsatCollection`, `Sentinel1Collection`, and `Sentinel2Collection` modules. This allows users to provide their own image collection of choice, such as PRISM or MODIS data, to benefit from the tools available with `RadGEEToolbox`.
47
47
 
48
48
  ***Table 1.*** *Comparison of functionality between RadGEEToolbox, eemont, and geetools.*
49
49
 
@@ -181,15 +181,15 @@ _____________
181
181
 
182
182
  ### Installing via pip
183
183
 
184
- To install `RadGEEToolbox` version 1.7.0 using pip (NOTE: it is recommended to create a new virtual environment):
184
+ To install `RadGEEToolbox` version 1.7.2 using pip (NOTE: it is recommended to create a new virtual environment):
185
185
 
186
186
  ```bash
187
- pip install RadGEEToolbox==1.7.0
187
+ pip install RadGEEToolbox==1.7.2
188
188
  ```
189
189
 
190
190
  ### Installing via Conda
191
191
 
192
- To install `RadGEEToolbox` version 1.7.0 using conda-forge (NOTE: it is recommended to create a new virtual environment):
192
+ To install `RadGEEToolbox` version 1.7.2 using conda-forge (NOTE: it is recommended to create a new virtual environment):
193
193
 
194
194
  ```bash
195
195
  conda install conda-forge::radgeetoolbox
@@ -220,7 +220,7 @@ To verify that `RadGEEToolbox` was installed correctly:
220
220
  python -c "import RadGEEToolbox; print(RadGEEToolbox.__version__)"
221
221
  ```
222
222
 
223
- You should see `1.7.0` printed as the version number.
223
+ You should see `1.7.2` printed as the version number.
224
224
 
225
225
  ### Want to Visualize Data? Install These Too
226
226
 
@@ -322,6 +322,8 @@ Then, directly plot `water_area_time_series` using Matplotlib as shown below
322
322
 
323
323
  For details about Sentinel-1 SAR and Sentinel-2 MSI modules, and all other available Landsat or cross-module functions, please refer to the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/). You can also explore [`/Example Notebooks`](https://github.com/radwinskis/RadGEEToolbox/tree/main/Example%20Notebooks) for more usage examples.
324
324
 
325
+ > NOTE for those newer to Python: when you see ***`property`*** next to the function name in the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/), you do not need to add parentheses at the end of the function when calling the function. For example: ```my_image_collection.add_date_property``` calls the `add_date_property` property function upon the `my_image_collection` image collection - no parentheses needed. All other non-property functions require one or more arguments to be specified by the user, and will require the use of parentheses to define the function arguments.
326
+
325
327
  ________
326
328
 
327
329
 
@@ -14,7 +14,7 @@ Designed for both new and advanced users of Google Earth Engine, RadGEEToolbox m
14
14
 
15
15
  Although similar packages exist (eemont, geetools, etc.), `RadGEEToolbox` extends functionality and provides cohesive, chainable methods for research oriented projects working with Landsat TM & OLI, Sentinel-1 SAR, and/or Sentinel-2 MSI datasets (Table 1). The ultimate goal of `RadGEEToolbox` is to make satellite image processing easier and faster for real world applications relying on the most commonly utilized remote sensing platforms.
16
16
 
17
- As of version `1.7.0`, `RadGEEToolbox` supports any generic image collection via the `GenericCollection` module which allows for utilization of the same data management, temporal reduction, zonal statistics, and data export tools available for the `LandsatCollection`, `Sentinel1Collection`, and `Sentinel2Collection` modules. This allows users to provide their own image collection of choice, such as PRISM or MODIS data, to benefit from the tools available with `RadGEEToolbox`.
17
+ As of version `1.7.2`, `RadGEEToolbox` supports any generic image collection via the `GenericCollection` module which allows for utilization of the same data management, temporal reduction, zonal statistics, and data export tools available for the `LandsatCollection`, `Sentinel1Collection`, and `Sentinel2Collection` modules. This allows users to provide their own image collection of choice, such as PRISM or MODIS data, to benefit from the tools available with `RadGEEToolbox`.
18
18
 
19
19
  ***Table 1.*** *Comparison of functionality between RadGEEToolbox, eemont, and geetools.*
20
20
 
@@ -152,15 +152,15 @@ _____________
152
152
 
153
153
  ### Installing via pip
154
154
 
155
- To install `RadGEEToolbox` version 1.7.0 using pip (NOTE: it is recommended to create a new virtual environment):
155
+ To install `RadGEEToolbox` version 1.7.2 using pip (NOTE: it is recommended to create a new virtual environment):
156
156
 
157
157
  ```bash
158
- pip install RadGEEToolbox==1.7.0
158
+ pip install RadGEEToolbox==1.7.2
159
159
  ```
160
160
 
161
161
  ### Installing via Conda
162
162
 
163
- To install `RadGEEToolbox` version 1.7.0 using conda-forge (NOTE: it is recommended to create a new virtual environment):
163
+ To install `RadGEEToolbox` version 1.7.2 using conda-forge (NOTE: it is recommended to create a new virtual environment):
164
164
 
165
165
  ```bash
166
166
  conda install conda-forge::radgeetoolbox
@@ -191,7 +191,7 @@ To verify that `RadGEEToolbox` was installed correctly:
191
191
  python -c "import RadGEEToolbox; print(RadGEEToolbox.__version__)"
192
192
  ```
193
193
 
194
- You should see `1.7.0` printed as the version number.
194
+ You should see `1.7.2` printed as the version number.
195
195
 
196
196
  ### Want to Visualize Data? Install These Too
197
197
 
@@ -293,6 +293,8 @@ Then, directly plot `water_area_time_series` using Matplotlib as shown below
293
293
 
294
294
  For details about Sentinel-1 SAR and Sentinel-2 MSI modules, and all other available Landsat or cross-module functions, please refer to the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/). You can also explore [`/Example Notebooks`](https://github.com/radwinskis/RadGEEToolbox/tree/main/Example%20Notebooks) for more usage examples.
295
295
 
296
+ > NOTE for those newer to Python: when you see ***`property`*** next to the function name in the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/), you do not need to add parentheses at the end of the function when calling the function. For example: ```my_image_collection.add_date_property``` calls the `add_date_property` property function upon the `my_image_collection` image collection - no parentheses needed. All other non-property functions require one or more arguments to be specified by the user, and will require the use of parentheses to define the function arguments.
297
+
296
298
  ________
297
299
 
298
300
 
@@ -104,7 +104,7 @@ class GenericCollection:
104
104
 
105
105
 
106
106
  @staticmethod
107
- def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
107
+ def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=None):
108
108
  """
109
109
  Calculates the anomaly of a singleband image compared to the mean of the singleband image.
110
110
 
@@ -132,16 +132,25 @@ class GenericCollection:
132
132
 
133
133
  image_to_process = image.select([band_name])
134
134
 
135
- # Calculate the mean image of the provided collection.
135
+ image_scale = image_to_process.projection().nominalScale()
136
+
137
+ # If the user supplies a numeric scale (int/float), keep it; otherwise default to image projection scale.
138
+ scale_value = scale if scale is not None else image_scale # Can be Python number or ee.Number
139
+
140
+ # Compute mean over geometry at chosen scale (scale_value may be ee.Number).
136
141
  mean_image = image_to_process.reduceRegion(
137
142
  reducer=ee.Reducer.mean(),
138
143
  geometry=geometry,
139
- scale=30,
144
+ scale=scale_value,
140
145
  maxPixels=1e13
141
146
  ).toImage()
142
147
 
143
148
  # Compute the anomaly by subtracting the mean image from the input image.
144
- anomaly_image = image_to_process.subtract(mean_image)
149
+ if scale is None:
150
+ anomaly_image = image_to_process.subtract(mean_image)
151
+ else:
152
+ anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale_value).subtract(mean_image)
153
+
145
154
  if anomaly_band_name is None:
146
155
  if band_name:
147
156
  anomaly_image = anomaly_image.rename(band_name)
@@ -152,9 +161,9 @@ class GenericCollection:
152
161
  anomaly_image = anomaly_image.rename(anomaly_band_name)
153
162
  # return anomaly_image
154
163
  if replace:
155
- return anomaly_image.copyProperties(image)
164
+ return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
156
165
  else:
157
- return image.addBands(anomaly_image, overwrite=True)
166
+ return image.addBands(anomaly_image, overwrite=True).copyProperties(image)
158
167
 
159
168
  @staticmethod
160
169
  def mask_via_band_fn(image, band_to_mask, band_for_mask, threshold, mask_above=False, add_band_to_original_image=False):
@@ -315,7 +324,7 @@ class GenericCollection:
315
324
 
316
325
  # Call to iterate the calculate_and_set_area function over the list of bands, starting with the original image
317
326
  final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
318
- return final_image
327
+ return final_image #.set('system:time_start', image.get('system:time_start'))
319
328
 
320
329
  def PixelAreaSumCollection(
321
330
  self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
@@ -329,11 +338,11 @@ class GenericCollection:
329
338
 
330
339
  Args:
331
340
  band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
332
- geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
341
+ geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
333
342
  threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
334
- scale (int): integer scale of image resolution (meters) (defaults to 30)
335
- maxPixels (int): integer denoting maximum number of pixels for calculations
336
- output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'GenericCollection' to return a GenericCollection object (defaults to 'ImageCollection')
343
+ scale (int): integer scale of image resolution (meters) (defaults to 30).
344
+ maxPixels (int): integer denoting maximum number of pixels for calculations.
345
+ output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'GenericCollection' to return a GenericCollection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
337
346
  area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
338
347
 
339
348
  Returns:
@@ -356,17 +365,44 @@ class GenericCollection:
356
365
  # Storing the result in the instance variable to avoid redundant calculations
357
366
  self._PixelAreaSumCollection = AreaCollection
358
367
 
368
+ prop_names = band_name if isinstance(band_name, list) else [band_name]
369
+
359
370
  # If an export path is provided, the area data will be exported to a CSV file
360
371
  if area_data_export_path:
361
- GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
362
-
372
+ GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
363
373
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
364
- if output_type == 'ImageCollection':
374
+ if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
365
375
  return self._PixelAreaSumCollection
366
376
  elif output_type == 'GenericCollection':
367
377
  return GenericCollection(collection=self._PixelAreaSumCollection)
378
+ elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
379
+ return GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
368
380
  else:
369
- raise ValueError("output_type must be 'ImageCollection' or 'GenericCollection'")
381
+ raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'GenericCollection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
382
+
383
+ @staticmethod
384
+ def add_month_property_fn(image):
385
+ """
386
+ Adds a numeric 'month' property to the image based on its date.
387
+
388
+ Args:
389
+ image (ee.Image): Input image.
390
+
391
+ Returns:
392
+ ee.Image: Image with the 'month' property added.
393
+ """
394
+ return image.set('month', image.date().get('month'))
395
+
396
+ @property
397
+ def add_month_property(self):
398
+ """
399
+ Adds a numeric 'month' property to each image in the collection.
400
+
401
+ Returns:
402
+ GenericCollection: A GenericCollection image collection with the 'month' property added to each image.
403
+ """
404
+ col = self.collection.map(GenericCollection.add_month_property_fn)
405
+ return GenericCollection(collection=col)
370
406
 
371
407
  def combine(self, other):
372
408
  """
@@ -480,6 +516,28 @@ class GenericCollection:
480
516
  dates = self._dates_list.getInfo()
481
517
  self._dates = dates
482
518
  return self._dates
519
+
520
+ def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
521
+ """
522
+ Removes duplicate images that share the same date, keeping only the first one encountered.
523
+ Useful for handling duplicate acquisitions or overlapping path/rows.
524
+
525
+ Args:
526
+ sort_by (str): Property to sort by before filtering distinct dates. Defaults to 'system:time_start' which is a global property.
527
+ Take care to provide a property that exists in all images if using a custom property.
528
+ ascending (bool): Sort order. Defaults to True.
529
+
530
+ Returns:
531
+ GenericCollection: A new GenericCollection object with distinct dates.
532
+ """
533
+
534
+ # Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
535
+ sorted_col = self.collection.sort(sort_by, ascending)
536
+
537
+ # distinct() retains the first image for each unique value of the specified property
538
+ distinct_col = sorted_col.distinct('Date_Filter')
539
+
540
+ return GenericCollection(collection=distinct_col)
483
541
 
484
542
  def ExportProperties(self, property_names, file_path=None):
485
543
  """
@@ -495,6 +553,8 @@ class GenericCollection:
495
553
  # Ensure property_names is a list for consistent processing
496
554
  if isinstance(property_names, str):
497
555
  property_names = [property_names]
556
+ elif isinstance(property_names, list):
557
+ property_names = property_names
498
558
 
499
559
  # Ensure properties are included without duplication, including 'Date_Filter'
500
560
  all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
@@ -1565,24 +1625,24 @@ class GenericCollection:
1565
1625
  if classify_above_threshold:
1566
1626
  if mask_zeros:
1567
1627
  col = self.collection.map(
1568
- lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
1628
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
1569
1629
  )
1570
1630
  else:
1571
1631
  col = self.collection.map(
1572
- lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
1632
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
1573
1633
  )
1574
1634
  else:
1575
1635
  if mask_zeros:
1576
1636
  col = self.collection.map(
1577
- lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
1637
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
1578
1638
  )
1579
1639
  else:
1580
1640
  col = self.collection.map(
1581
- lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
1641
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
1582
1642
  )
1583
1643
  return GenericCollection(collection=col)
1584
1644
 
1585
- def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
1645
+ def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=None):
1586
1646
  """
1587
1647
  Calculates the anomaly of each image in a collection compared to the mean of each image.
1588
1648
 
@@ -1596,6 +1656,7 @@ class GenericCollection:
1596
1656
  band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
1597
1657
  anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
1598
1658
  replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band. If True, the output image will only contain the anomaly band. If False, the output image will retain all original bands and add the anomaly band. Default is True.
1659
+ scale (int, optional): The scale (in meters) to use for the image reduction. If not provided, the nominal scale of the image will be used.
1599
1660
 
1600
1661
  Returns:
1601
1662
  GenericCollection: A GenericCollection where each image represents the anomaly (deviation from
@@ -1614,7 +1675,7 @@ class GenericCollection:
1614
1675
  else:
1615
1676
  band_name = band_names.get(0).getInfo()
1616
1677
 
1617
- col = self.collection.map(lambda image: GenericCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
1678
+ col = self.collection.map(lambda image: GenericCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
1618
1679
  return GenericCollection(collection=col)
1619
1680
 
1620
1681
  def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
@@ -2434,24 +2495,30 @@ class GenericCollection:
2434
2495
  ValueError: If input parameters are invalid.
2435
2496
  TypeError: If geometries input type is unsupported.
2436
2497
  """
2498
+ # Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
2437
2499
  img_collection_obj = self
2500
+
2501
+ # If a specific band is requested, select only that band
2438
2502
  if band:
2439
2503
  img_collection_obj = GenericCollection(collection=img_collection_obj.collection.select(band))
2440
2504
  else:
2505
+ # If no band is specified, default to using the first band of the first image in the collection
2441
2506
  first_image = img_collection_obj.image_grab(0)
2442
2507
  first_band = first_image.bandNames().get(0)
2443
2508
  img_collection_obj = GenericCollection(collection=img_collection_obj.collection.select([first_band]))
2444
- # Filter collection by dates if provided
2509
+
2510
+ # If a list of dates is provided, filter the collection to include only images matching those dates
2445
2511
  if dates:
2446
2512
  img_collection_obj = GenericCollection(
2447
2513
  collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
2448
2514
  )
2449
2515
 
2450
- # Initialize variables
2516
+ # Initialize variables to hold the standardized feature collection and coordinates
2451
2517
  features = None
2452
2518
  validated_coordinates = []
2453
2519
 
2454
- # Function to standardize feature names if no names are provided
2520
+ # Define a helper function to ensure every feature has a standardized 'geo_name' property
2521
+ # This handles features that might have different existing name properties or none at all
2455
2522
  def set_standard_name(feature):
2456
2523
  has_geo_name = feature.get('geo_name')
2457
2524
  has_name = feature.get('name')
@@ -2462,33 +2529,38 @@ class GenericCollection:
2462
2529
  ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
2463
2530
  return feature.set({'geo_name': new_name})
2464
2531
 
2532
+ # Handle input: FeatureCollection or single Feature
2465
2533
  if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
2466
2534
  features = ee.FeatureCollection(geometries)
2467
2535
  if geometry_names:
2468
2536
  print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
2469
2537
 
2538
+ # Handle input: Single ee.Geometry
2470
2539
  elif isinstance(geometries, ee.Geometry):
2471
2540
  name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
2472
2541
  features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
2473
2542
 
2543
+ # Handle input: List (could be coordinates or ee.Geometry objects)
2474
2544
  elif isinstance(geometries, list):
2475
2545
  if not geometries: # Handle empty list case
2476
2546
  raise ValueError("'geometries' list cannot be empty.")
2477
2547
 
2478
- # Case: List of coordinates
2548
+ # Case: List of tuples (coordinates)
2479
2549
  if all(isinstance(i, tuple) for i in geometries):
2480
2550
  validated_coordinates = geometries
2551
+ # Generate default names if none provided
2481
2552
  if geometry_names is None:
2482
2553
  geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
2483
2554
  elif len(geometry_names) != len(validated_coordinates):
2484
2555
  raise ValueError("geometry_names must have the same length as the coordinates list.")
2556
+ # Create features with buffers around the coordinates
2485
2557
  points = [
2486
2558
  ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
2487
2559
  for coord, name in zip(validated_coordinates, geometry_names)
2488
2560
  ]
2489
2561
  features = ee.FeatureCollection(points)
2490
2562
 
2491
- # Case: List of Geometries
2563
+ # Case: List of ee.Geometry objects
2492
2564
  elif all(isinstance(i, ee.Geometry) for i in geometries):
2493
2565
  if geometry_names is None:
2494
2566
  geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
@@ -2503,6 +2575,7 @@ class GenericCollection:
2503
2575
  else:
2504
2576
  raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
2505
2577
 
2578
+ # Handle input: Single tuple (coordinate)
2506
2579
  elif isinstance(geometries, tuple) and len(geometries) == 2:
2507
2580
  name = geometry_names[0] if geometry_names else 'Location_1'
2508
2581
  features = ee.FeatureCollection([
@@ -2511,39 +2584,48 @@ class GenericCollection:
2511
2584
  else:
2512
2585
  raise TypeError("Unsupported type for 'geometries'.")
2513
2586
 
2587
+ # Apply the naming standardization to the created FeatureCollection
2514
2588
  features = features.map(set_standard_name)
2515
2589
 
2590
+ # Dynamically retrieve the Earth Engine reducer based on the string name provided
2516
2591
  try:
2517
2592
  reducer = getattr(ee.Reducer, reducer_type)()
2518
2593
  except AttributeError:
2519
2594
  raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
2520
2595
 
2596
+ # Define the function to map over the image collection
2521
2597
  def calculate_stats_for_image(image):
2522
2598
  image_date = image.get('Date_Filter')
2599
+ # Calculate statistics for all geometries in 'features' for this specific image
2523
2600
  stats_fc = image.reduceRegions(
2524
2601
  collection=features, reducer=reducer, scale=scale, tileScale=tileScale
2525
2602
  )
2526
2603
 
2604
+ # Helper to ensure the result has the reducer property, even if masked
2605
+ # If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
2527
2606
  def guarantee_reducer_property(f):
2528
2607
  has_property = f.propertyNames().contains(reducer_type)
2529
2608
  return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
2609
+
2610
+ # Apply the guarantee check
2530
2611
  fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
2531
2612
 
2613
+ # Attach the image date to every feature in the result so we know which image it came from
2532
2614
  return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
2533
2615
 
2616
+ # Map the calculation over the image collection and flatten the resulting FeatureCollections into one
2534
2617
  results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
2618
+
2619
+ # Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
2535
2620
  df = GenericCollection.ee_to_df(results_fc, remove_geom=True)
2536
2621
 
2537
- # Checking for issues
2622
+ # Check for empty results or missing columns
2538
2623
  if df.empty:
2539
- # print("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
2540
- # return df
2541
2624
  raise ValueError("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
2542
2625
  if reducer_type not in df.columns:
2543
2626
  print(f"Warning: Reducer '{reducer_type}' not found in results.")
2544
- # return df
2545
2627
 
2546
- # Get the number of rows before dropping nulls for a helpful message
2628
+ # Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
2547
2629
  initial_rows = len(df)
2548
2630
  df.dropna(subset=[reducer_type], inplace=True)
2549
2631
  df = df[df[reducer_type] != -9999]
@@ -2551,9 +2633,18 @@ class GenericCollection:
2551
2633
  if dropped_rows > 0:
2552
2634
  print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
2553
2635
 
2554
- # Reshape DataFrame to have dates as index and geometry names as columns
2636
+ # Pivot the DataFrame so that each row represents a date and each column represents a geometry location
2555
2637
  pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
2638
+ # Rename the column headers (geometry names) to include the reducer type
2639
+ pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
2640
+ # Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
2556
2641
  pivot_df.index.name = 'Date'
2642
+ # Remove the name of the columns axis (which defaults to 'geo_name') so it doesn't appear as a confusing label in the final output
2643
+ pivot_df.columns.name = None
2644
+ # Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
2645
+ pivot_df = pivot_df.reset_index(drop=False)
2646
+
2647
+ # If a file path is provided, save the resulting DataFrame to CSV
2557
2648
  if file_path:
2558
2649
  # Check if file_path ends with .csv and remove it if so for consistency
2559
2650
  if file_path.endswith('.csv'):
@@ -0,0 +1,169 @@
1
+ def get_palette(name):
2
+ """
3
+ Returns the color palette associated with the given name.
4
+
5
+ Args:
6
+ name (str): Options include:
7
+ - Scientific: 'viridis', 'plasma', 'inferno', 'magma', 'cividis', 'turbo', 'coolwarm', 'spectral'
8
+ - Sequential: 'blues', 'greens', 'reds', 'greys', 'oranges', 'purples'
9
+ - Diverging: 'rdylgn' (Red-Yellow-Green), 'rdylbu' (Red-Yellow-Blue), 'rdbu' (Red-White-Blue), 'brbg' (Brown-Blue-Green), 'piyg' (Pink-Yellow-Green)
10
+ - Domain Specific: 'dem' (Elevation), 'terrain' (Topography), 'ndvi' (Vegetation), 'ndwi' (Water), 'precipitation' (Rain/Snow), 'thermal' (Temperature), 'evapotranspiration' (ET)
11
+ - Custom/Legacy: 'algae', 'dense', 'haline', 'jet', 'matter', 'pubu', 'soft_blue_green_red', 'turbid', 'ylord', 'ocean'
12
+
13
+ Returns:
14
+ list: list of colors to be used for image visualization in GEE vis params
15
+
16
+ """
17
+ palettes = {
18
+ # --- Scientific / Perceptually Uniform ---
19
+ "viridis": [
20
+ "#440154", "#482475", "#414487", "#355f8d", "#2a788e",
21
+ "#21918c", "#22a884", "#44bf70", "#7ad151", "#bddf26", "#fde725"
22
+ ],
23
+ "plasma": [
24
+ "#0d0887", "#46039f", "#7201a8", "#9c179e", "#bd3786",
25
+ "#d8576b", "#ed7953", "#fb9f3a", "#fdc924", "#f0f921"
26
+ ],
27
+ "inferno": [
28
+ "#000004", "#160b39", "#420a68", "#6a176e", "#932667",
29
+ "#bc3754", "#dd513a", "#f37819", "#fca50a", "#f6d746", "#fcffa4"
30
+ ],
31
+ "magma": [
32
+ "#000004", "#140e36", "#3b0f70", "#641a80", "#8c2981",
33
+ "#b73779", "#de4968", "#f7705c", "#fe9f6d", "#fecf92", "#fcfdbf"
34
+ ],
35
+ "cividis": [
36
+ "#00204d", "#002c69", "#003989", "#184a8c", "#3f5b8a",
37
+ "#5d6d85", "#78807f", "#969576", "#b4ab6a", "#d4c359", "#fdea45"
38
+ ],
39
+ "turbo": [
40
+ "#30123b", "#466be3", "#28bbec", "#32f298", "#a2fc3c",
41
+ "#f2ea33", "#fe9b2d", "#e4460a", "#7a0403"
42
+ ],
43
+ "coolwarm": [
44
+ "#3d4c8a", "#6282ea", "#99baff", "#cdd9ec", "#eaf0e4",
45
+ "#f4dcb8", "#e8ac80", "#d4654d", "#b2182b"
46
+ ],
47
+
48
+ # --- Sequential (ColorBrewer & Standard) ---
49
+ "blues": [
50
+ "#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6",
51
+ "#4292c6", "#2171b5", "#08519c", "#08306b"
52
+ ],
53
+ "greens": [
54
+ "#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476",
55
+ "#41ab5d", "#238b45", "#006d2c", "#00441b"
56
+ ],
57
+ "reds": [
58
+ "#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a",
59
+ "#ef3b2c", "#cb181d", "#a50f15", "#67000d"
60
+ ],
61
+ "greys": [
62
+ "#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696",
63
+ "#737373", "#525252", "#252525", "#000000"
64
+ ],
65
+ "oranges": [
66
+ "#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c",
67
+ "#f16913", "#d94801", "#a63603", "#7f2704"
68
+ ],
69
+ "purples": [
70
+ "#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8",
71
+ "#807dba", "#6a51a3", "#54278f", "#3f007d"
72
+ ],
73
+
74
+ # --- Diverging ---
75
+ "spectral": [
76
+ "#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b",
77
+ "#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"
78
+ ],
79
+ "rdylgn": [
80
+ "#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b",
81
+ "#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"
82
+ ],
83
+ "rdylbu": [
84
+ "#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b",
85
+ "#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"
86
+ ],
87
+ "rdbu": [
88
+ "#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7",
89
+ "#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"
90
+ ],
91
+ "brbg": [
92
+ "#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3",
93
+ "#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"
94
+ ],
95
+ "piyg": [
96
+ "#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef",
97
+ "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"
98
+ ],
99
+
100
+ # --- Domain Specific ---
101
+ "dem": [
102
+ "#006600", "#002200", "#fff700", "#ab7634", "#c4d0ff", "#ffffff"
103
+ ], # Classic Green-Brown-White Elevation
104
+ "terrain": [
105
+ "#00A600", "#63C600", "#E6E600", "#E9BD3A", "#ECB176",
106
+ "#EFC2B3", "#F2F2F2"
107
+ ], # Alternative Terrain
108
+ "ndvi": [
109
+ "#FFFFFF", "#CE7E45", "#DF923D", "#F1B555", "#FCD163", "#99B718",
110
+ "#74A901", "#66A000", "#529400", "#3E8601", "#207401", "#056201",
111
+ "#004C00", "#023B01", "#012E01", "#011D01", "#011301"
112
+ ], # Standard MODIS/Landsat NDVI ramp
113
+ "ndwi": [
114
+ "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0",
115
+ "#0570b0", "#045a8d", "#023858"
116
+ ], # Blue ramp for water
117
+ "precipitation": [
118
+ "#ffffff", "#00ffff", "#0000ff", "#00ff00", "#ffff00",
119
+ "#ff0000", "#ff00ff"
120
+ ], # Classic Precip: White-Blue-Green-Yellow-Red-Purple
121
+ "evapotranspiration": [
122
+ "#ffffff", "#fcd163", "#99b718", "#74a901", "#66a000",
123
+ "#529400", "#3e8601", "#207401", "#056201", "#004c00"
124
+ ], # Modeled on NDVI/Vegetation water use
125
+ "thermal": [
126
+ "#042333", "#2c3395", "#744992", "#b15f82", "#eb7958",
127
+ "#fbb43d", "#e8fa5b"
128
+ ],
129
+
130
+ # --- Custom / Legacy from Original ---
131
+ "jet": [
132
+ "#00007F", "#002AFF", "#00D4FF", "#7FFF7F", "#FFD400",
133
+ "#FF2A00", "#7F0000"
134
+ ],
135
+ "soft_blue_green_red": ["#deeaee", "#b1cbbb", "#eea29a", "#c94c4c"],
136
+ "algae": [
137
+ "#d7f9d0", "#a2d595", "#64b463", "#129450", "#126e45",
138
+ "#1a482f", "#122414"
139
+ ],
140
+ "turbid": [
141
+ "#e9f6ab", "#d3c671", "#bf9747", "#a1703b", "#795338",
142
+ "#4d392d", "#221f1b"
143
+ ],
144
+ "dense": [
145
+ "#e6f1f1", "#a2cee2", "#76a4e5", "#7871d5", "#7642a5",
146
+ "#621d62", "#360e24"
147
+ ],
148
+ "matter": [
149
+ "#feedb0", "#f7b37c", "#eb7858", "#ce4356", "#9f2462",
150
+ "#66185c", "#2f0f3e"
151
+ ],
152
+ "haline": [
153
+ "#2a186c", "14439c", "#206e8b", "#3c9387", "#5ab978",
154
+ "#aad85c", "#fdef9a"
155
+ ],
156
+ "ylord": [
157
+ "#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c",
158
+ "#fc4e2a", "#e31a1c", "#bd0026", "#800026"
159
+ ],
160
+ "pubu": [
161
+ "#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf",
162
+ "#3690c0", "#0570b0", "#045a8d", "#023858"
163
+ ][::-1],
164
+ "ocean": [
165
+ "#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4",
166
+ "#1d91c0", "#225ea8", "#253494", "#081d58"
167
+ ],
168
+ }
169
+ return palettes.get(name, None)