RadGEEToolbox 1.7.0__tar.gz → 1.7.1__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.1}/PKG-INFO +8 -6
  2. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/README.md +7 -5
  3. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox/GenericCollection.py +119 -31
  4. radgeetoolbox-1.7.1/RadGEEToolbox/GetPalette.py +169 -0
  5. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox/LandsatCollection.py +461 -36
  6. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox/Sentinel1Collection.py +603 -18
  7. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox/Sentinel2Collection.py +464 -30
  8. radgeetoolbox-1.7.1/RadGEEToolbox/VisParams.py +139 -0
  9. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox/__init__.py +1 -1
  10. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox.egg-info/PKG-INFO +8 -6
  11. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/pyproject.toml +1 -1
  12. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/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.1}/LICENSE.txt +0 -0
  16. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox/CollectionStitch.py +0 -0
  17. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox.egg-info/SOURCES.txt +0 -0
  18. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox.egg-info/dependency_links.txt +0 -0
  19. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox.egg-info/requires.txt +0 -0
  20. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/RadGEEToolbox.egg-info/top_level.txt +0 -0
  21. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/setup.cfg +0 -0
  22. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/tests/test_landsat_collection.py +0 -0
  23. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/tests/test_sentinel1_collection.py +0 -0
  24. {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.1}/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.1
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.1`, `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.1 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.1
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.1 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.1` 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.1`, `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.1 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.1
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.1 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.1` 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:
@@ -358,15 +367,41 @@ class GenericCollection:
358
367
 
359
368
  # If an export path is provided, the area data will be exported to a CSV file
360
369
  if area_data_export_path:
361
- GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
370
+ GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name], file_path=area_data_export_path+'.csv')
362
371
 
363
372
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
364
- if output_type == 'ImageCollection':
373
+ if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
365
374
  return self._PixelAreaSumCollection
366
375
  elif output_type == 'GenericCollection':
367
376
  return GenericCollection(collection=self._PixelAreaSumCollection)
377
+ elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
378
+ return GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name])
368
379
  else:
369
- raise ValueError("output_type must be 'ImageCollection' or 'GenericCollection'")
380
+ 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'.")
381
+
382
+ @staticmethod
383
+ def add_month_property_fn(image):
384
+ """
385
+ Adds a numeric 'month' property to the image based on its date.
386
+
387
+ Args:
388
+ image (ee.Image): Input image.
389
+
390
+ Returns:
391
+ ee.Image: Image with the 'month' property added.
392
+ """
393
+ return image.set('month', image.date().get('month'))
394
+
395
+ @property
396
+ def add_month_property(self):
397
+ """
398
+ Adds a numeric 'month' property to each image in the collection.
399
+
400
+ Returns:
401
+ GenericCollection: A GenericCollection image collection with the 'month' property added to each image.
402
+ """
403
+ col = self.collection.map(GenericCollection.add_month_property_fn)
404
+ return GenericCollection(collection=col)
370
405
 
371
406
  def combine(self, other):
372
407
  """
@@ -480,6 +515,28 @@ class GenericCollection:
480
515
  dates = self._dates_list.getInfo()
481
516
  self._dates = dates
482
517
  return self._dates
518
+
519
+ def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
520
+ """
521
+ Removes duplicate images that share the same date, keeping only the first one encountered.
522
+ Useful for handling duplicate acquisitions or overlapping path/rows.
523
+
524
+ Args:
525
+ sort_by (str): Property to sort by before filtering distinct dates. Defaults to 'system:time_start' which is a global property.
526
+ Take care to provide a property that exists in all images if using a custom property.
527
+ ascending (bool): Sort order. Defaults to True.
528
+
529
+ Returns:
530
+ GenericCollection: A new GenericCollection object with distinct dates.
531
+ """
532
+
533
+ # Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
534
+ sorted_col = self.collection.sort(sort_by, ascending)
535
+
536
+ # distinct() retains the first image for each unique value of the specified property
537
+ distinct_col = sorted_col.distinct('Date_Filter')
538
+
539
+ return GenericCollection(collection=distinct_col)
483
540
 
484
541
  def ExportProperties(self, property_names, file_path=None):
485
542
  """
@@ -1565,24 +1622,24 @@ class GenericCollection:
1565
1622
  if classify_above_threshold:
1566
1623
  if mask_zeros:
1567
1624
  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)
1625
+ 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
1626
  )
1570
1627
  else:
1571
1628
  col = self.collection.map(
1572
- lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
1629
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
1573
1630
  )
1574
1631
  else:
1575
1632
  if mask_zeros:
1576
1633
  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)
1634
+ 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
1635
  )
1579
1636
  else:
1580
1637
  col = self.collection.map(
1581
- lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
1638
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
1582
1639
  )
1583
1640
  return GenericCollection(collection=col)
1584
1641
 
1585
- def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
1642
+ def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=None):
1586
1643
  """
1587
1644
  Calculates the anomaly of each image in a collection compared to the mean of each image.
1588
1645
 
@@ -1596,6 +1653,7 @@ class GenericCollection:
1596
1653
  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
1654
  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
1655
  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.
1656
+ 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
1657
 
1600
1658
  Returns:
1601
1659
  GenericCollection: A GenericCollection where each image represents the anomaly (deviation from
@@ -1614,7 +1672,7 @@ class GenericCollection:
1614
1672
  else:
1615
1673
  band_name = band_names.get(0).getInfo()
1616
1674
 
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))
1675
+ 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
1676
  return GenericCollection(collection=col)
1619
1677
 
1620
1678
  def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
@@ -2434,24 +2492,30 @@ class GenericCollection:
2434
2492
  ValueError: If input parameters are invalid.
2435
2493
  TypeError: If geometries input type is unsupported.
2436
2494
  """
2495
+ # Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
2437
2496
  img_collection_obj = self
2497
+
2498
+ # If a specific band is requested, select only that band
2438
2499
  if band:
2439
2500
  img_collection_obj = GenericCollection(collection=img_collection_obj.collection.select(band))
2440
2501
  else:
2502
+ # If no band is specified, default to using the first band of the first image in the collection
2441
2503
  first_image = img_collection_obj.image_grab(0)
2442
2504
  first_band = first_image.bandNames().get(0)
2443
2505
  img_collection_obj = GenericCollection(collection=img_collection_obj.collection.select([first_band]))
2444
- # Filter collection by dates if provided
2506
+
2507
+ # If a list of dates is provided, filter the collection to include only images matching those dates
2445
2508
  if dates:
2446
2509
  img_collection_obj = GenericCollection(
2447
2510
  collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
2448
2511
  )
2449
2512
 
2450
- # Initialize variables
2513
+ # Initialize variables to hold the standardized feature collection and coordinates
2451
2514
  features = None
2452
2515
  validated_coordinates = []
2453
2516
 
2454
- # Function to standardize feature names if no names are provided
2517
+ # Define a helper function to ensure every feature has a standardized 'geo_name' property
2518
+ # This handles features that might have different existing name properties or none at all
2455
2519
  def set_standard_name(feature):
2456
2520
  has_geo_name = feature.get('geo_name')
2457
2521
  has_name = feature.get('name')
@@ -2462,33 +2526,38 @@ class GenericCollection:
2462
2526
  ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
2463
2527
  return feature.set({'geo_name': new_name})
2464
2528
 
2529
+ # Handle input: FeatureCollection or single Feature
2465
2530
  if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
2466
2531
  features = ee.FeatureCollection(geometries)
2467
2532
  if geometry_names:
2468
2533
  print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
2469
2534
 
2535
+ # Handle input: Single ee.Geometry
2470
2536
  elif isinstance(geometries, ee.Geometry):
2471
2537
  name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
2472
2538
  features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
2473
2539
 
2540
+ # Handle input: List (could be coordinates or ee.Geometry objects)
2474
2541
  elif isinstance(geometries, list):
2475
2542
  if not geometries: # Handle empty list case
2476
2543
  raise ValueError("'geometries' list cannot be empty.")
2477
2544
 
2478
- # Case: List of coordinates
2545
+ # Case: List of tuples (coordinates)
2479
2546
  if all(isinstance(i, tuple) for i in geometries):
2480
2547
  validated_coordinates = geometries
2548
+ # Generate default names if none provided
2481
2549
  if geometry_names is None:
2482
2550
  geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
2483
2551
  elif len(geometry_names) != len(validated_coordinates):
2484
2552
  raise ValueError("geometry_names must have the same length as the coordinates list.")
2553
+ # Create features with buffers around the coordinates
2485
2554
  points = [
2486
2555
  ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
2487
2556
  for coord, name in zip(validated_coordinates, geometry_names)
2488
2557
  ]
2489
2558
  features = ee.FeatureCollection(points)
2490
2559
 
2491
- # Case: List of Geometries
2560
+ # Case: List of ee.Geometry objects
2492
2561
  elif all(isinstance(i, ee.Geometry) for i in geometries):
2493
2562
  if geometry_names is None:
2494
2563
  geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
@@ -2503,6 +2572,7 @@ class GenericCollection:
2503
2572
  else:
2504
2573
  raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
2505
2574
 
2575
+ # Handle input: Single tuple (coordinate)
2506
2576
  elif isinstance(geometries, tuple) and len(geometries) == 2:
2507
2577
  name = geometry_names[0] if geometry_names else 'Location_1'
2508
2578
  features = ee.FeatureCollection([
@@ -2511,39 +2581,48 @@ class GenericCollection:
2511
2581
  else:
2512
2582
  raise TypeError("Unsupported type for 'geometries'.")
2513
2583
 
2584
+ # Apply the naming standardization to the created FeatureCollection
2514
2585
  features = features.map(set_standard_name)
2515
2586
 
2587
+ # Dynamically retrieve the Earth Engine reducer based on the string name provided
2516
2588
  try:
2517
2589
  reducer = getattr(ee.Reducer, reducer_type)()
2518
2590
  except AttributeError:
2519
2591
  raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
2520
2592
 
2593
+ # Define the function to map over the image collection
2521
2594
  def calculate_stats_for_image(image):
2522
2595
  image_date = image.get('Date_Filter')
2596
+ # Calculate statistics for all geometries in 'features' for this specific image
2523
2597
  stats_fc = image.reduceRegions(
2524
2598
  collection=features, reducer=reducer, scale=scale, tileScale=tileScale
2525
2599
  )
2526
2600
 
2601
+ # Helper to ensure the result has the reducer property, even if masked
2602
+ # If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
2527
2603
  def guarantee_reducer_property(f):
2528
2604
  has_property = f.propertyNames().contains(reducer_type)
2529
2605
  return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
2606
+
2607
+ # Apply the guarantee check
2530
2608
  fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
2531
2609
 
2610
+ # Attach the image date to every feature in the result so we know which image it came from
2532
2611
  return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
2533
2612
 
2613
+ # Map the calculation over the image collection and flatten the resulting FeatureCollections into one
2534
2614
  results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
2615
+
2616
+ # Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
2535
2617
  df = GenericCollection.ee_to_df(results_fc, remove_geom=True)
2536
2618
 
2537
- # Checking for issues
2619
+ # Check for empty results or missing columns
2538
2620
  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
2621
  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
2622
  if reducer_type not in df.columns:
2543
2623
  print(f"Warning: Reducer '{reducer_type}' not found in results.")
2544
- # return df
2545
2624
 
2546
- # Get the number of rows before dropping nulls for a helpful message
2625
+ # Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
2547
2626
  initial_rows = len(df)
2548
2627
  df.dropna(subset=[reducer_type], inplace=True)
2549
2628
  df = df[df[reducer_type] != -9999]
@@ -2551,9 +2630,18 @@ class GenericCollection:
2551
2630
  if dropped_rows > 0:
2552
2631
  print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
2553
2632
 
2554
- # Reshape DataFrame to have dates as index and geometry names as columns
2633
+ # Pivot the DataFrame so that each row represents a date and each column represents a geometry location
2555
2634
  pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
2635
+ # Rename the column headers (geometry names) to include the reducer type
2636
+ pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
2637
+ # Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
2556
2638
  pivot_df.index.name = 'Date'
2639
+ # 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
2640
+ pivot_df.columns.name = None
2641
+ # Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
2642
+ pivot_df = pivot_df.reset_index(drop=False)
2643
+
2644
+ # If a file path is provided, save the resulting DataFrame to CSV
2557
2645
  if file_path:
2558
2646
  # Check if file_path ends with .csv and remove it if so for consistency
2559
2647
  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)