RadGEEToolbox 1.7.2__py3-none-any.whl → 1.7.4__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.
@@ -1,6 +1,7 @@
1
1
  import ee
2
2
  import pandas as pd
3
3
  import numpy as np
4
+ import warnings
4
5
 
5
6
 
6
7
  # ---- Reflectance scaling for Landsat Collection 2 SR ----
@@ -159,6 +160,11 @@ class LandsatCollection:
159
160
  self._monthly_max = None
160
161
  self._monthly_min = None
161
162
  self._monthly_sum = None
163
+ self._yearly_median = None
164
+ self._yearly_mean = None
165
+ self._yearly_max = None
166
+ self._yearly_min = None
167
+ self._yearly_sum = None
162
168
  self._mean = None
163
169
  self._max = None
164
170
  self._min = None
@@ -181,6 +187,14 @@ class LandsatCollection:
181
187
  self._PixelAreaSumCollection = None
182
188
  self._Reflectance = None
183
189
 
190
+ def __call__(self):
191
+ """
192
+ Allows the object to be called as a function, returning itself.
193
+ This enables property-like methods to be accessed with or without parentheses
194
+ (e.g., .mosaicByDate or .mosaicByDate()).
195
+ """
196
+ return self
197
+
184
198
  @staticmethod
185
199
  def image_dater(image):
186
200
  """
@@ -239,17 +253,17 @@ class LandsatCollection:
239
253
  ndwi_calc.updateMask(ndwi_calc.gte(threshold))
240
254
  .rename("ndwi")
241
255
  .copyProperties(image)
242
- .set("threshold", threshold),
256
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
243
257
  ndwi_calc.updateMask(ndwi_calc.gte(ng_threshold))
244
258
  .rename("ndwi")
245
259
  .copyProperties(image)
246
- .set("threshold", ng_threshold),
260
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
247
261
  )
248
262
  else:
249
263
  water = (
250
264
  ndwi_calc.updateMask(ndwi_calc.gte(threshold))
251
265
  .rename("ndwi")
252
- .copyProperties(image)
266
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
253
267
  )
254
268
  return water
255
269
 
@@ -282,17 +296,17 @@ class LandsatCollection:
282
296
  mndwi_calc.updateMask(mndwi_calc.gte(threshold))
283
297
  .rename("mndwi")
284
298
  .copyProperties(image)
285
- .set("threshold", threshold),
299
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
286
300
  mndwi_calc.updateMask(mndwi_calc.gte(ng_threshold))
287
301
  .rename("mndwi")
288
302
  .copyProperties(image)
289
- .set("threshold", ng_threshold),
303
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
290
304
  )
291
305
  else:
292
306
  water = (
293
307
  mndwi_calc.updateMask(mndwi_calc.gte(threshold))
294
308
  .rename("mndwi")
295
- .copyProperties(image)
309
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
296
310
  )
297
311
  return water
298
312
 
@@ -320,17 +334,17 @@ class LandsatCollection:
320
334
  ndvi_calc.updateMask(ndvi_calc.gte(threshold))
321
335
  .rename("ndvi")
322
336
  .copyProperties(image)
323
- .set("threshold", threshold),
337
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
324
338
  ndvi_calc.updateMask(ndvi_calc.gte(ng_threshold))
325
339
  .rename("ndvi")
326
340
  .copyProperties(image)
327
- .set("threshold", ng_threshold),
341
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
328
342
  )
329
343
  else:
330
344
  vegetation = (
331
345
  ndvi_calc.updateMask(ndvi_calc.gte(threshold))
332
346
  .rename("ndvi")
333
- .copyProperties(image)
347
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
334
348
  )
335
349
  return vegetation
336
350
 
@@ -356,17 +370,17 @@ class LandsatCollection:
356
370
  halite_index.updateMask(halite_index.gte(threshold))
357
371
  .rename("halite")
358
372
  .copyProperties(image)
359
- .set("threshold", threshold),
373
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
360
374
  halite_index.updateMask(halite_index.gte(ng_threshold))
361
375
  .rename("halite")
362
376
  .copyProperties(image)
363
- .set("threshold", ng_threshold),
377
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
364
378
  )
365
379
  else:
366
380
  halite = (
367
381
  halite_index.updateMask(halite_index.gte(threshold))
368
382
  .rename("halite")
369
- .copyProperties(image)
383
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
370
384
  )
371
385
  return halite
372
386
 
@@ -392,17 +406,17 @@ class LandsatCollection:
392
406
  gypsum_index.updateMask(gypsum_index.gte(threshold))
393
407
  .rename("gypsum")
394
408
  .copyProperties(image)
395
- .set("threshold", threshold),
409
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
396
410
  gypsum_index.updateMask(gypsum_index.gte(ng_threshold))
397
411
  .rename("gypsum")
398
412
  .copyProperties(image)
399
- .set("threshold", ng_threshold),
413
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
400
414
  )
401
415
  else:
402
416
  gypsum = (
403
417
  gypsum_index.updateMask(gypsum_index.gte(threshold))
404
418
  .rename("gypsum")
405
- .copyProperties(image)
419
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
406
420
  )
407
421
  return gypsum
408
422
 
@@ -427,11 +441,11 @@ class LandsatCollection:
427
441
  NDTI.updateMask(NDTI.gte(threshold))
428
442
  .rename("ndti")
429
443
  .copyProperties(image)
430
- .set("threshold", threshold),
444
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
431
445
  NDTI.updateMask(NDTI.gte(ng_threshold))
432
446
  .rename("ndti")
433
447
  .copyProperties(image)
434
- .set("threshold", ng_threshold),
448
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
435
449
  )
436
450
  else:
437
451
  turbidity = (
@@ -471,17 +485,17 @@ class LandsatCollection:
471
485
  KIVU.updateMask(KIVU.gte(threshold))
472
486
  .rename("kivu")
473
487
  .copyProperties(image)
474
- .set("threshold", threshold),
488
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
475
489
  KIVU.updateMask(KIVU.gte(ng_threshold))
476
490
  .rename("kivu")
477
491
  .copyProperties(image)
478
- .set("threshold", ng_threshold),
492
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
479
493
  )
480
494
  else:
481
495
  chlorophyll = (
482
496
  KIVU.updateMask(KIVU.gte(threshold))
483
497
  .rename("kivu")
484
- .copyProperties(image)
498
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
485
499
  )
486
500
  return chlorophyll
487
501
 
@@ -505,8 +519,8 @@ class LandsatCollection:
505
519
  # otherwise treat as OLI and copy properties after renaming band to "albedo"
506
520
  albedo = ee.Algorithms.If(
507
521
  ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
508
- image.expression(TM_expression).rename("albedo").copyProperties(image),
509
- image.expression(OLI_expression).rename("albedo").copyProperties(image))
522
+ image.expression(TM_expression).rename("albedo").copyProperties(image).set('system:time_start', image.get('system:time_start')),
523
+ image.expression(OLI_expression).rename("albedo").copyProperties(image).set('system:time_start', image.get('system:time_start')))
510
524
  return albedo
511
525
 
512
526
  @staticmethod
@@ -530,14 +544,14 @@ class LandsatCollection:
530
544
  ndsi_calc.updateMask(ndsi_calc.gte(threshold))
531
545
  .rename("ndsi")
532
546
  .copyProperties(image)
533
- .set("threshold", threshold),
547
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
534
548
  ndsi_calc.updateMask(ndsi_calc.gte(ng_threshold))
535
549
  .rename("ndsi")
536
550
  .copyProperties(image)
537
- .set("threshold", ng_threshold),
551
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
538
552
  )
539
553
  else:
540
- ndsi = ndsi_calc.updateMask(ndsi_calc.gte(threshold)).rename("ndsi").copyProperties(image).set("threshold", threshold)
554
+ ndsi = ndsi_calc.updateMask(ndsi_calc.gte(threshold)).rename("ndsi").copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start'))
541
555
  return ndsi
542
556
 
543
557
  @staticmethod
@@ -567,14 +581,14 @@ class LandsatCollection:
567
581
  evi_calc.updateMask(evi_calc.gte(threshold))
568
582
  .rename("evi")
569
583
  .copyProperties(image)
570
- .set("threshold", threshold),
584
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
571
585
  evi_calc.updateMask(evi_calc.gte(ng_threshold))
572
586
  .rename("evi")
573
587
  .copyProperties(image)
574
- .set("threshold", ng_threshold),
588
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
575
589
  )
576
590
  else:
577
- evi = evi_calc.updateMask(evi_calc.gte(threshold)).rename("evi").copyProperties(image).set("threshold", threshold)
591
+ evi = evi_calc.updateMask(evi_calc.gte(threshold)).rename("evi").copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start'))
578
592
  return evi
579
593
 
580
594
  @staticmethod
@@ -600,14 +614,14 @@ class LandsatCollection:
600
614
  savi_calc.updateMask(savi_calc.gte(threshold))
601
615
  .rename("savi")
602
616
  .copyProperties(image)
603
- .set("threshold", threshold),
617
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
604
618
  savi_calc.updateMask(savi_calc.gte(ng_threshold))
605
619
  .rename("savi")
606
620
  .copyProperties(image)
607
- .set("threshold", ng_threshold),
621
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
608
622
  )
609
623
  else:
610
- savi = savi_calc.updateMask(savi_calc.gte(threshold)).rename("savi").copyProperties(image).set("threshold", threshold)
624
+ savi = savi_calc.updateMask(savi_calc.gte(threshold)).rename("savi").copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start'))
611
625
  return savi
612
626
 
613
627
  @staticmethod
@@ -633,14 +647,14 @@ class LandsatCollection:
633
647
  msavi_calc.updateMask(msavi_calc.gte(threshold))
634
648
  .rename("msavi")
635
649
  .copyProperties(image)
636
- .set("threshold", threshold),
650
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
637
651
  msavi_calc.updateMask(msavi_calc.gte(ng_threshold))
638
652
  .rename("msavi")
639
653
  .copyProperties(image)
640
- .set("threshold", ng_threshold),
654
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
641
655
  )
642
656
  else:
643
- msavi = msavi_calc.updateMask(msavi_calc.gte(threshold)).rename("msavi").copyProperties(image).set("threshold", threshold)
657
+ msavi = msavi_calc.updateMask(msavi_calc.gte(threshold)).rename("msavi").copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start'))
644
658
  return msavi
645
659
 
646
660
  @staticmethod
@@ -666,14 +680,14 @@ class LandsatCollection:
666
680
  ndmi_calc.updateMask(ndmi_calc.gte(threshold))
667
681
  .rename("ndmi")
668
682
  .copyProperties(image)
669
- .set("threshold", threshold),
683
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
670
684
  ndmi_calc.updateMask(ndmi_calc.gte(ng_threshold))
671
685
  .rename("ndmi")
672
686
  .copyProperties(image)
673
- .set("threshold", ng_threshold),
687
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
674
688
  )
675
689
  else:
676
- ndmi = ndmi_calc.updateMask(ndmi_calc.gte(threshold)).rename("ndmi").copyProperties(image).set("threshold", threshold)
690
+ ndmi = ndmi_calc.updateMask(ndmi_calc.gte(threshold)).rename("ndmi").copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start'))
677
691
  return ndmi
678
692
 
679
693
  @staticmethod
@@ -698,14 +712,14 @@ class LandsatCollection:
698
712
  nbr_calc.updateMask(nbr_calc.gte(threshold))
699
713
  .rename("nbr")
700
714
  .copyProperties(image)
701
- .set("threshold", threshold),
715
+ .set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
702
716
  nbr_calc.updateMask(nbr_calc.gte(ng_threshold))
703
717
  .rename("nbr")
704
718
  .copyProperties(image)
705
- .set("threshold", ng_threshold),
719
+ .set("threshold", ng_threshold, 'system:time_start', image.get('system:time_start')),
706
720
  )
707
721
  else:
708
- nbr = nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr").copyProperties(image).set("threshold", threshold)
722
+ nbr = nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr").copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start'))
709
723
  return nbr
710
724
 
711
725
  @staticmethod
@@ -767,7 +781,7 @@ class LandsatCollection:
767
781
  return image.addBands(anomaly_image, overwrite=True).copyProperties(image)
768
782
 
769
783
  @staticmethod
770
- def MaskWaterLandsat(image):
784
+ def maskWater(image):
771
785
  """
772
786
  Masks water pixels based on Landsat image QA band.
773
787
 
@@ -780,11 +794,21 @@ class LandsatCollection:
780
794
  WaterBitMask = ee.Number(2).pow(7).int()
781
795
  qa = image.select("QA_PIXEL")
782
796
  water_extract = qa.bitwiseAnd(WaterBitMask).eq(0)
783
- masked_image = image.updateMask(water_extract).copyProperties(image)
797
+ masked_image = image.updateMask(water_extract).copyProperties(image).set('system:time_start', image.get('system:time_start'))
784
798
  return masked_image
799
+
800
+ @staticmethod
801
+ def MaskWaterLandsat(image):
802
+ warnings.warn(
803
+ "MaskWaterLandsat is deprecated and will be removed in a future release. "
804
+ "Please use maskWater instead.",
805
+ DeprecationWarning,
806
+ stacklevel=2
807
+ )
808
+ return LandsatCollection.maskWater(image)
785
809
 
786
810
  @staticmethod
787
- def MaskWaterLandsatByNDWI(image, threshold, ng_threshold=None):
811
+ def maskWaterByNDWI(image, threshold, ng_threshold=None):
788
812
  """
789
813
  Masks water pixels (mask land and cloud pixels) for all bands based on NDWI and a set threshold where
790
814
  all pixels less than NDWI threshold are masked out. Can specify separate thresholds for Landsat 5 vs 8&9 images, where the threshold
@@ -805,23 +829,34 @@ class LandsatCollection:
805
829
  ndwi_calc.updateMask(ndwi_calc.gte(threshold))
806
830
  .rename("ndwi")
807
831
  .copyProperties(image)
832
+ .set('system:time_start', image.get('system:time_start'))
808
833
  )
809
834
  if ng_threshold != None:
810
835
  water = ee.Algorithms.If(
811
836
  ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
812
- image.updateMask(ndwi_calc.lt(threshold)).set("threshold", threshold),
813
- image.updateMask(ndwi_calc.lt(ng_threshold)).set(
814
- "threshold", ng_threshold
837
+ image.updateMask(ndwi_calc.lt(threshold)).copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
838
+ image.updateMask(ndwi_calc.lt(ng_threshold)).copyProperties(image).set(
839
+ "threshold", ng_threshold, 'system:time_start', image.get('system:time_start')
815
840
  ),
816
841
  )
817
842
  else:
818
- water = image.updateMask(ndwi_calc.lt(threshold)).set(
819
- "threshold", threshold
843
+ water = image.updateMask(ndwi_calc.lt(threshold)).copyProperties(image).set(
844
+ "threshold", threshold, 'system:time_start', image.get('system:time_start')
820
845
  )
821
846
  return water
822
-
847
+
823
848
  @staticmethod
824
- def MaskToWaterLandsat(image):
849
+ def MaskWaterLandsatByNDWI(image, threshold, ng_threshold=None):
850
+ warnings.warn(
851
+ "MaskWaterLandsatByNDWI is deprecated and will be removed in a future release. "
852
+ "Please use maskWaterByNDWI instead.",
853
+ DeprecationWarning,
854
+ stacklevel=2
855
+ )
856
+ return LandsatCollection.maskWaterByNDWI(image, threshold, ng_threshold=ng_threshold)
857
+
858
+ @staticmethod
859
+ def maskToWater(image):
825
860
  """
826
861
  Masks image to water pixels by masking land and cloud pixels based on Landsat image QA band.
827
862
 
@@ -834,11 +869,21 @@ class LandsatCollection:
834
869
  WaterBitMask = ee.Number(2).pow(7).int()
835
870
  qa = image.select("QA_PIXEL")
836
871
  water_extract = qa.bitwiseAnd(WaterBitMask).neq(0)
837
- masked_image = image.updateMask(water_extract).copyProperties(image)
872
+ masked_image = image.updateMask(water_extract).copyProperties(image).set('system:time_start', image.get('system:time_start'))
838
873
  return masked_image
874
+
875
+ @staticmethod
876
+ def MaskToWaterLandsat(image):
877
+ warnings.warn(
878
+ "MaskToWaterLandsat is deprecated and will be removed in a future release. "
879
+ "Please use maskToWater instead.",
880
+ DeprecationWarning,
881
+ stacklevel=2
882
+ )
883
+ return LandsatCollection.maskToWater(image)
839
884
 
840
885
  @staticmethod
841
- def MaskToWaterLandsatByNDWI(image, threshold, ng_threshold=None):
886
+ def maskToWaterByNDWI(image, threshold, ng_threshold=None):
842
887
  """
843
888
  Masks water pixels using NDWI based on threshold. Can specify separate thresholds for Landsat 5 vs 8&9 images, where the threshold
844
889
  argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8&9
@@ -858,20 +903,65 @@ class LandsatCollection:
858
903
  ndwi_calc.updateMask(ndwi_calc.gte(threshold))
859
904
  .rename("ndwi")
860
905
  .copyProperties(image)
906
+ .set('system:time_start', image.get('system:time_start'))
861
907
  )
862
908
  if ng_threshold != None:
863
909
  water = ee.Algorithms.If(
864
910
  ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
865
- image.updateMask(ndwi_calc.gte(threshold)).set("threshold", threshold),
866
- image.updateMask(ndwi_calc.gte(ng_threshold)).set(
867
- "threshold", ng_threshold
911
+ image.updateMask(ndwi_calc.gte(threshold)).copyProperties(image).set("threshold", threshold, 'system:time_start', image.get('system:time_start')),
912
+ image.updateMask(ndwi_calc.gte(ng_threshold)).copyProperties(image).set(
913
+ "threshold", ng_threshold, 'system:time_start', image.get('system:time_start')
868
914
  ),
869
915
  )
870
916
  else:
871
- water = image.updateMask(ndwi_calc.gte(threshold)).set(
872
- "threshold", threshold
917
+ water = image.updateMask(ndwi_calc.gte(threshold)).copyProperties(image).set(
918
+ "threshold", threshold, 'system:time_start', image.get('system:time_start')
873
919
  )
874
920
  return water
921
+
922
+ @staticmethod
923
+ def MaskToWaterLandsatByNDWI(image, threshold, ng_threshold=None):
924
+ warnings.warn(
925
+ "MaskToWaterLandsatNDWI is deprecated and will be removed in a future release. "
926
+ "Please use maskToWaterByNDWI instead.",
927
+ DeprecationWarning,
928
+ stacklevel=2
929
+ )
930
+ return LandsatCollection.maskToWaterByNDWI(image, threshold, ng_threshold=ng_threshold)
931
+
932
+ # @staticmethod
933
+ # def maskClouds(image):
934
+ # """
935
+ # Masks clouds pixels based on Landsat image QA band.
936
+
937
+ # Args:
938
+ # image (ee.Image): input ee.Image
939
+
940
+ # Returns:
941
+ # ee.Image: ee.Image with cloud pixels masked.
942
+ # """
943
+ # CloudBitMask = ee.Number(2).pow(3).int()
944
+ # qa = image.select("QA_PIXEL")
945
+ # cloud_extract = qa.bitwiseAnd(CloudBitMask).eq(0)
946
+ # masked_image = image.updateMask(cloud_extract).copyProperties(image).set('system:time_start', image.get('system:time_start'))
947
+ # return masked_image
948
+
949
+ # @staticmethod
950
+ # def maskShadows(image):
951
+ # """
952
+ # Masks shadows pixels based on Landsat image QA band.
953
+
954
+ # Args:
955
+ # image (ee.Image): input ee.Image
956
+
957
+ # Returns:
958
+ # ee.Image: ee.Image with cloud pixels masked.
959
+ # """
960
+ # ShadowBitMask = ee.Number(2).pow(4).int()
961
+ # qa = image.select("QA_PIXEL")
962
+ # shadow_extract = qa.bitwiseAnd(ShadowBitMask).eq(0)
963
+ # masked_image = image.updateMask(shadow_extract).copyProperties(image).set('system:time_start', image.get('system:time_start'))
964
+ # return masked_image
875
965
 
876
966
  @staticmethod
877
967
  def mask_via_band_fn(image, band_to_mask, band_for_mask, threshold, mask_above=False, add_band_to_original_image=False):
@@ -899,7 +989,7 @@ class LandsatCollection:
899
989
  if add_band_to_original_image:
900
990
  return image.addBands(band_to_mask_image.updateMask(mask).rename(band_to_mask), overwrite=True)
901
991
  else:
902
- return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image))
992
+ return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image).set('system:time_start', image.get('system:time_start')))
903
993
 
904
994
  @staticmethod
905
995
  def mask_via_singleband_image_fn(image_to_mask, image_for_mask, threshold, band_name_to_mask=None, band_name_for_mask=None, mask_above=True):
@@ -935,7 +1025,7 @@ class LandsatCollection:
935
1025
  mask = band_for_mask_image.gt(threshold)
936
1026
  else:
937
1027
  mask = band_for_mask_image.lt(threshold)
938
- return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask)
1028
+ return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask).set('system:time_start', image_to_mask.get('system:time_start'))
939
1029
 
940
1030
  @staticmethod
941
1031
  def halite_mask(image, threshold, ng_threshold=None):
@@ -959,11 +1049,11 @@ class LandsatCollection:
959
1049
  if ng_threshold != None:
960
1050
  mask = ee.Algorithms.If(
961
1051
  ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
962
- image.updateMask(halite_index.lt(threshold)).copyProperties(image),
963
- image.updateMask(halite_index.lt(ng_threshold)).copyProperties(image),
1052
+ image.updateMask(halite_index.lt(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start')),
1053
+ image.updateMask(halite_index.lt(ng_threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start')),
964
1054
  )
965
1055
  else:
966
- mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image)
1056
+ mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
967
1057
  return mask
968
1058
 
969
1059
  @staticmethod
@@ -1000,11 +1090,13 @@ class LandsatCollection:
1000
1090
  gypsum_index.updateMask(halite_index.lt(halite_threshold))
1001
1091
  .updateMask(gypsum_index.lt(gypsum_threshold))
1002
1092
  .rename("carbonate_muds")
1003
- .copyProperties(image),
1093
+ .copyProperties(image)
1094
+ .set('system:time_start', image.get('system:time_start')),
1004
1095
  gypsum_index.updateMask(halite_index.lt(halite_ng_threshold))
1005
1096
  .updateMask(gypsum_index.lt(gypsum_ng_threshold))
1006
1097
  .rename("carbonate_muds")
1007
- .copyProperties(image),
1098
+ .copyProperties(image)
1099
+ .set('system:time_start', image.get('system:time_start')),
1008
1100
  )
1009
1101
  else:
1010
1102
  mask = (
@@ -1012,11 +1104,12 @@ class LandsatCollection:
1012
1104
  .updateMask(gypsum_index.lt(gypsum_threshold))
1013
1105
  .rename("carbonate_muds")
1014
1106
  .copyProperties(image)
1107
+ .set('system:time_start', image.get('system:time_start'))
1015
1108
  )
1016
1109
  return mask
1017
1110
 
1018
1111
  @staticmethod
1019
- def maskL8clouds(image):
1112
+ def maskClouds(image):
1020
1113
  """
1021
1114
  Masks clouds baseed on Landsat 8 QA band.
1022
1115
 
@@ -1032,9 +1125,17 @@ class LandsatCollection:
1032
1125
  cloud_mask = qa.bitwiseAnd(cloudBitMask).eq(0)
1033
1126
  cirrus_mask = qa.bitwiseAnd(CirrusBitMask).eq(0)
1034
1127
  return image.updateMask(cloud_mask).updateMask(cirrus_mask)
1128
+
1129
+ @staticmethod
1130
+ def maskL8clouds(image):
1131
+ warnings.warn(
1132
+ "maskL8clouds is deprecated and will be removed in a future release. Please use maskClouds instead.",
1133
+ DeprecationWarning,
1134
+ stacklevel=2)
1135
+ return LandsatCollection.maskClouds(image)
1035
1136
 
1036
1137
  @staticmethod
1037
- def maskL8shadows(image):
1138
+ def maskShadows(image):
1038
1139
  """
1039
1140
  Masks cloud shadows based on Landsat 8 QA band.
1040
1141
 
@@ -1048,6 +1149,14 @@ class LandsatCollection:
1048
1149
  qa = image.select("QA_PIXEL")
1049
1150
  shadow_mask = qa.bitwiseAnd(shadowBitMask).eq(0)
1050
1151
  return image.updateMask(shadow_mask)
1152
+
1153
+ @staticmethod
1154
+ def maskL8shadows(image):
1155
+ warnings.warn(
1156
+ "maskL8shadows is deprecated and will be removed in a future release. Please use maskShadows instead.",
1157
+ DeprecationWarning,
1158
+ stacklevel=2)
1159
+ return LandsatCollection.maskShadows(image)
1051
1160
 
1052
1161
  @staticmethod
1053
1162
  def temperature_bands(img):
@@ -1162,7 +1271,7 @@ class LandsatCollection:
1162
1271
  return out.copyProperties(img)
1163
1272
 
1164
1273
  @staticmethod
1165
- def PixelAreaSum(
1274
+ def pixelAreaSum(
1166
1275
  image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
1167
1276
  ):
1168
1277
  """
@@ -1221,8 +1330,18 @@ class LandsatCollection:
1221
1330
  # Call to iterate the calculate_and_set_area function over the list of bands, starting with the original image
1222
1331
  final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
1223
1332
  return final_image
1333
+
1334
+ @staticmethod
1335
+ def PixelAreaSum(image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12):
1336
+ warnings.warn(
1337
+ "PixelAreaSum is deprecated and will be removed in a future release. "
1338
+ "Please use pixelAreaSum instead.",
1339
+ DeprecationWarning,
1340
+ stacklevel=2
1341
+ )
1342
+ return LandsatCollection.pixelAreaSum(image, band_name, geometry, threshold, scale, maxPixels)
1224
1343
 
1225
- def PixelAreaSumCollection(
1344
+ def pixelAreaSumCollection(
1226
1345
  self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None):
1227
1346
  """
1228
1347
  Calculates the geodesic summation of area for pixels of interest (above a specific threshold)
@@ -1248,7 +1367,7 @@ class LandsatCollection:
1248
1367
  collection = self.collection
1249
1368
  # Area calculation for each image in the collection, using the PixelAreaSum function
1250
1369
  AreaCollection = collection.map(
1251
- lambda image: LandsatCollection.PixelAreaSum(
1370
+ lambda image: LandsatCollection.pixelAreaSum(
1252
1371
  image,
1253
1372
  band_name=band_name,
1254
1373
  geometry=geometry,
@@ -1264,17 +1383,27 @@ class LandsatCollection:
1264
1383
 
1265
1384
  # If an export path is provided, the area data will be exported to a CSV file
1266
1385
  if area_data_export_path:
1267
- LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
1386
+ LandsatCollection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
1268
1387
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
1269
1388
  if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
1270
1389
  return self._PixelAreaSumCollection
1271
1390
  elif output_type == 'LandsatCollection':
1272
1391
  return LandsatCollection(collection=self._PixelAreaSumCollection)
1273
1392
  elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
1274
- return LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
1393
+ return LandsatCollection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names)
1275
1394
  else:
1276
1395
  raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'LandsatCollection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
1277
1396
 
1397
+ def PixelAreaSumCollection(
1398
+ self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None):
1399
+ warnings.warn(
1400
+ "PixelAreaSumCollection is deprecated and will be removed in a future release. "
1401
+ "Please use pixelAreaSumCollection instead.",
1402
+ DeprecationWarning,
1403
+ stacklevel=2
1404
+ )
1405
+ return self.pixelAreaSumCollection(band_name, geometry, threshold, scale, maxPixels, output_type, area_data_export_path)
1406
+
1278
1407
  @staticmethod
1279
1408
  def add_month_property_fn(image):
1280
1409
  """
@@ -1378,7 +1507,12 @@ class LandsatCollection:
1378
1507
  return LandsatCollection(collection=ee.ImageCollection(paired.map(_pair_two)))
1379
1508
 
1380
1509
  # Preferred path: merge many singleband products into the parent
1381
- if not isinstance(collections, list) or len(collections) == 0:
1510
+ # if not isinstance(collections, list) or len(collections) == 0:
1511
+ # raise ValueError("Provide a non-empty list of LandsatCollection objects in `collections`.")
1512
+ if not isinstance(collections, list):
1513
+ collections = [collections]
1514
+
1515
+ if len(collections) == 0:
1382
1516
  raise ValueError("Provide a non-empty list of LandsatCollection objects in `collections`.")
1383
1517
 
1384
1518
  result = self.collection
@@ -1505,7 +1639,7 @@ class LandsatCollection:
1505
1639
  self._dates = dates
1506
1640
  return self._dates
1507
1641
 
1508
- def ExportProperties(self, property_names, file_path=None):
1642
+ def exportProperties(self, property_names, file_path=None):
1509
1643
  """
1510
1644
  Fetches and returns specified properties from each image in the collection as a list, and returns a pandas DataFrame and optionally saves the results to a csv file.
1511
1645
 
@@ -1560,6 +1694,15 @@ class LandsatCollection:
1560
1694
  print(f"Properties saved to {file_path}")
1561
1695
 
1562
1696
  return df
1697
+
1698
+ def ExportProperties(self, property_names, file_path=None):
1699
+ warnings.warn(
1700
+ "ExportProperties is deprecated and will be removed in a future release. "
1701
+ "Please use exportProperties instead.",
1702
+ DeprecationWarning,
1703
+ stacklevel=2
1704
+ )
1705
+ return self.exportProperties(property_names, file_path)
1563
1706
 
1564
1707
  def get_filtered_collection(self):
1565
1708
  """
@@ -1781,6 +1924,7 @@ class LandsatCollection:
1781
1924
  """
1782
1925
  if self._monthly_mean is None:
1783
1926
  collection = self.collection
1927
+ # Capture projection from the first image to restore it after reduction
1784
1928
  target_proj = collection.first().projection()
1785
1929
  # Get the start and end dates of the entire collection.
1786
1930
  date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
@@ -2092,6 +2236,391 @@ class LandsatCollection:
2092
2236
  pass
2093
2237
 
2094
2238
  return self._monthly_min
2239
+
2240
+ def yearly_mean_collection(self, start_month=1, end_month=12):
2241
+ """
2242
+ Creates a yearly mean composite from the collection, with optional monthly filtering.
2243
+
2244
+ This function computes the mean for each year within the collection's date range.
2245
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
2246
+ to calculate the mean only using imagery from that specific season for each year.
2247
+
2248
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
2249
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
2250
+
2251
+ Args:
2252
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
2253
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
2254
+
2255
+ Returns:
2256
+ Object: A new instance of the same class (e.g., LandsatCollection) containing the yearly mean composites.
2257
+ """
2258
+ if self._yearly_mean is None:
2259
+
2260
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
2261
+ start_date_full = ee.Date(date_range.get('min'))
2262
+ end_date_full = ee.Date(date_range.get('max'))
2263
+
2264
+ start_year = start_date_full.get('year')
2265
+ end_year = end_date_full.get('year')
2266
+
2267
+ if start_month != 1 or end_month != 12:
2268
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
2269
+ else:
2270
+ processing_collection = self.collection
2271
+
2272
+ # Capture projection from the first image to restore it after reduction
2273
+ target_proj = self.collection.first().projection()
2274
+
2275
+ years = ee.List.sequence(start_year, end_year)
2276
+
2277
+ def create_yearly_composite(year):
2278
+ year = ee.Number(year)
2279
+ # Define the full calendar year range
2280
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
2281
+ end_of_year = start_of_year.advance(1, 'year')
2282
+
2283
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
2284
+
2285
+ # Calculate stats
2286
+ image_count = yearly_subset.size()
2287
+ yearly_reduction = yearly_subset.mean()
2288
+
2289
+ # Define the timestamp for the composite.
2290
+ # We use the start_month of that year to accurately reflect the data start time.
2291
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
2292
+
2293
+ return yearly_reduction.set({
2294
+ 'system:time_start': composite_date.millis(),
2295
+ 'year': year,
2296
+ 'month': start_month,
2297
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
2298
+ 'image_count': image_count,
2299
+ 'season_start': start_month,
2300
+ 'season_end': end_month
2301
+ }).reproject(target_proj)
2302
+
2303
+ # Map the function over the years list
2304
+ yearly_composites_list = years.map(create_yearly_composite)
2305
+
2306
+ # Convert to Collection
2307
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
2308
+
2309
+ # Filter out any composites that were created from zero images.
2310
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
2311
+
2312
+ self._yearly_mean = LandsatCollection(collection=final_collection)
2313
+ else:
2314
+ pass
2315
+ return self._yearly_mean
2316
+
2317
+ def yearly_median_collection(self, start_month=1, end_month=12):
2318
+ """
2319
+ Creates a yearly median composite from the collection, with optional monthly filtering.
2320
+
2321
+ This function computes the median for each year within the collection's date range.
2322
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
2323
+ to calculate the median only using imagery from that specific season for each year.
2324
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
2325
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
2326
+
2327
+ Args:
2328
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
2329
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
2330
+
2331
+ Returns:
2332
+ Object: A new instance of the same class (e.g., LandsatCollection) containing the yearly median composites.
2333
+ """
2334
+ if self._yearly_median is None:
2335
+
2336
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
2337
+ start_date_full = ee.Date(date_range.get('min'))
2338
+ end_date_full = ee.Date(date_range.get('max'))
2339
+
2340
+ start_year = start_date_full.get('year')
2341
+ end_year = end_date_full.get('year')
2342
+
2343
+ if start_month != 1 or end_month != 12:
2344
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
2345
+ else:
2346
+ processing_collection = self.collection
2347
+
2348
+ # Capture projection from the first image to restore it after reduction
2349
+ target_proj = self.collection.first().projection()
2350
+
2351
+ years = ee.List.sequence(start_year, end_year)
2352
+
2353
+ def create_yearly_composite(year):
2354
+ year = ee.Number(year)
2355
+ # Define the full calendar year range
2356
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
2357
+ end_of_year = start_of_year.advance(1, 'year')
2358
+
2359
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
2360
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
2361
+
2362
+ # Calculate stats
2363
+ image_count = yearly_subset.size()
2364
+ yearly_reduction = yearly_subset.median()
2365
+
2366
+ # Define the timestamp for the composite.
2367
+ # We use the start_month of that year to accurately reflect the data start time.
2368
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
2369
+
2370
+ return yearly_reduction.set({
2371
+ 'system:time_start': composite_date.millis(),
2372
+ 'year': year,
2373
+ 'month': start_month,
2374
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
2375
+ 'image_count': image_count,
2376
+ 'season_start': start_month,
2377
+ 'season_end': end_month
2378
+ }).reproject(target_proj)
2379
+
2380
+ # Map the function over the years list
2381
+ yearly_composites_list = years.map(create_yearly_composite)
2382
+
2383
+ # Convert to Collection
2384
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
2385
+
2386
+ # Filter out any composites that were created from zero images.
2387
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
2388
+
2389
+ self._yearly_median = LandsatCollection(collection=final_collection)
2390
+ else:
2391
+ pass
2392
+ return self._yearly_median
2393
+
2394
+ def yearly_max_collection(self, start_month=1, end_month=12):
2395
+ """
2396
+ Creates a yearly max composite from the collection, with optional monthly filtering.
2397
+
2398
+ This function computes the max for each year within the collection's date range.
2399
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
2400
+ to calculate the max only using imagery from that specific season for each year.
2401
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
2402
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
2403
+
2404
+ Args:
2405
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
2406
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
2407
+
2408
+ Returns:
2409
+ Object: A new instance of the same class (e.g., LandsatCollection) containing the yearly max composites.
2410
+ """
2411
+ if self._yearly_max is None:
2412
+
2413
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
2414
+ start_date_full = ee.Date(date_range.get('min'))
2415
+ end_date_full = ee.Date(date_range.get('max'))
2416
+
2417
+ start_year = start_date_full.get('year')
2418
+ end_year = end_date_full.get('year')
2419
+
2420
+ if start_month != 1 or end_month != 12:
2421
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
2422
+ else:
2423
+ processing_collection = self.collection
2424
+
2425
+ # Capture projection from the first image to restore it after reduction
2426
+ target_proj = self.collection.first().projection()
2427
+
2428
+ years = ee.List.sequence(start_year, end_year)
2429
+
2430
+ def create_yearly_composite(year):
2431
+ year = ee.Number(year)
2432
+ # Define the full calendar year range
2433
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
2434
+ end_of_year = start_of_year.advance(1, 'year')
2435
+
2436
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
2437
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
2438
+
2439
+ # Calculate stats
2440
+ image_count = yearly_subset.size()
2441
+ yearly_reduction = yearly_subset.max()
2442
+
2443
+ # Define the timestamp for the composite.
2444
+ # We use the start_month of that year to accurately reflect the data start time.
2445
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
2446
+
2447
+ return yearly_reduction.set({
2448
+ 'system:time_start': composite_date.millis(),
2449
+ 'year': year,
2450
+ 'month': start_month,
2451
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
2452
+ 'image_count': image_count,
2453
+ 'season_start': start_month,
2454
+ 'season_end': end_month
2455
+ }).reproject(target_proj)
2456
+
2457
+ # Map the function over the years list
2458
+ yearly_composites_list = years.map(create_yearly_composite)
2459
+
2460
+ # Convert to Collection
2461
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
2462
+
2463
+ # Filter out any composites that were created from zero images.
2464
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
2465
+
2466
+ self._yearly_max = LandsatCollection(collection=final_collection)
2467
+ else:
2468
+ pass
2469
+ return self._yearly_max
2470
+
2471
+ def yearly_min_collection(self, start_month=1, end_month=12):
2472
+ """
2473
+ Creates a yearly min composite from the collection, with optional monthly filtering.
2474
+
2475
+ This function computes the min for each year within the collection's date range.
2476
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
2477
+ to calculate the min only using imagery from that specific season for each year.
2478
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
2479
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
2480
+
2481
+ Args:
2482
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
2483
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
2484
+
2485
+ Returns:
2486
+ Object: A new instance of the same class (e.g., LandsatCollection) containing the yearly min composites.
2487
+ """
2488
+ if self._yearly_min is None:
2489
+
2490
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
2491
+ start_date_full = ee.Date(date_range.get('min'))
2492
+ end_date_full = ee.Date(date_range.get('max'))
2493
+
2494
+ start_year = start_date_full.get('year')
2495
+ end_year = end_date_full.get('year')
2496
+
2497
+ if start_month != 1 or end_month != 12:
2498
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
2499
+ else:
2500
+ processing_collection = self.collection
2501
+
2502
+ # Capture projection from the first image to restore it after reduction
2503
+ target_proj = self.collection.first().projection()
2504
+
2505
+ years = ee.List.sequence(start_year, end_year)
2506
+
2507
+ def create_yearly_composite(year):
2508
+ year = ee.Number(year)
2509
+ # Define the full calendar year range
2510
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
2511
+ end_of_year = start_of_year.advance(1, 'year')
2512
+
2513
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
2514
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
2515
+
2516
+ # Calculate stats
2517
+ image_count = yearly_subset.size()
2518
+ yearly_reduction = yearly_subset.min()
2519
+
2520
+ # Define the timestamp for the composite.
2521
+ # We use the start_month of that year to accurately reflect the data start time.
2522
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
2523
+
2524
+ return yearly_reduction.set({
2525
+ 'system:time_start': composite_date.millis(),
2526
+ 'year': year,
2527
+ 'month': start_month,
2528
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
2529
+ 'image_count': image_count,
2530
+ 'season_start': start_month,
2531
+ 'season_end': end_month
2532
+ }).reproject(target_proj)
2533
+
2534
+ # Map the function over the years list
2535
+ yearly_composites_list = years.map(create_yearly_composite)
2536
+
2537
+ # Convert to Collection
2538
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
2539
+
2540
+ # Filter out any composites that were created from zero images.
2541
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
2542
+
2543
+ self._yearly_min = LandsatCollection(collection=final_collection)
2544
+ else:
2545
+ pass
2546
+ return self._yearly_min
2547
+
2548
+ def yearly_sum_collection(self, start_month=1, end_month=12):
2549
+ """
2550
+ Creates a yearly sum composite from the collection, with optional monthly filtering.
2551
+
2552
+ This function computes the sum for each year within the collection's date range.
2553
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
2554
+ to calculate the sum only using imagery from that specific season for each year.
2555
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
2556
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
2557
+
2558
+ Args:
2559
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
2560
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
2561
+
2562
+ Returns:
2563
+ Object: A new instance of the same class (e.g., LandsatCollection) containing the yearly sum composites.
2564
+ """
2565
+ if self._yearly_sum is None:
2566
+
2567
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
2568
+ start_date_full = ee.Date(date_range.get('min'))
2569
+ end_date_full = ee.Date(date_range.get('max'))
2570
+
2571
+ start_year = start_date_full.get('year')
2572
+ end_year = end_date_full.get('year')
2573
+
2574
+ if start_month != 1 or end_month != 12:
2575
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
2576
+ else:
2577
+ processing_collection = self.collection
2578
+
2579
+ # Capture projection from the first image to restore it after reduction
2580
+ target_proj = self.collection.first().projection()
2581
+
2582
+ years = ee.List.sequence(start_year, end_year)
2583
+
2584
+ def create_yearly_composite(year):
2585
+ year = ee.Number(year)
2586
+ # Define the full calendar year range
2587
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
2588
+ end_of_year = start_of_year.advance(1, 'year')
2589
+
2590
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
2591
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
2592
+
2593
+ # Calculate stats
2594
+ image_count = yearly_subset.size()
2595
+ yearly_reduction = yearly_subset.sum()
2596
+
2597
+ # Define the timestamp for the composite.
2598
+ # We use the start_month of that year to accurately reflect the data start time.
2599
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
2600
+
2601
+ return yearly_reduction.set({
2602
+ 'system:time_start': composite_date.millis(),
2603
+ 'year': year,
2604
+ 'month': start_month,
2605
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
2606
+ 'image_count': image_count,
2607
+ 'season_start': start_month,
2608
+ 'season_end': end_month
2609
+ }).reproject(target_proj)
2610
+
2611
+ # Map the function over the years list
2612
+ yearly_composites_list = years.map(create_yearly_composite)
2613
+
2614
+ # Convert to Collection
2615
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
2616
+
2617
+ # Filter out any composites that were created from zero images.
2618
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
2619
+
2620
+ self._yearly_sum = LandsatCollection(collection=final_collection)
2621
+ else:
2622
+ pass
2623
+ return self._yearly_sum
2095
2624
 
2096
2625
  @property
2097
2626
  def ndwi(self):
@@ -2693,7 +3222,7 @@ class LandsatCollection:
2693
3222
  LandsatCollection: LandsatCollection image collection
2694
3223
  """
2695
3224
  if self._masked_water_collection is None:
2696
- col = self.collection.map(LandsatCollection.MaskWaterLandsat)
3225
+ col = self.collection.map(LandsatCollection.maskWater)
2697
3226
  self._masked_water_collection = LandsatCollection(collection=col)
2698
3227
  return self._masked_water_collection
2699
3228
 
@@ -2708,7 +3237,7 @@ class LandsatCollection:
2708
3237
  LandsatCollection: LandsatCollection image collection
2709
3238
  """
2710
3239
  col = self.collection.map(
2711
- lambda image: LandsatCollection.MaskWaterLandsatByNDWI(
3240
+ lambda image: LandsatCollection.maskWaterByNDWI(
2712
3241
  image, threshold=threshold
2713
3242
  )
2714
3243
  )
@@ -2723,7 +3252,7 @@ class LandsatCollection:
2723
3252
  LandsatCollection: LandsatCollection image collection
2724
3253
  """
2725
3254
  if self._masked_to_water_collection is None:
2726
- col = self.collection.map(LandsatCollection.MaskToWaterLandsat)
3255
+ col = self.collection.map(LandsatCollection.maskToWater)
2727
3256
  self._masked_to_water_collection = LandsatCollection(collection=col)
2728
3257
  return self._masked_to_water_collection
2729
3258
 
@@ -2738,7 +3267,7 @@ class LandsatCollection:
2738
3267
  LandsatCollection: LandsatCollection image collection
2739
3268
  """
2740
3269
  col = self.collection.map(
2741
- lambda image: LandsatCollection.MaskToWaterLandsatByNDWI(
3270
+ lambda image: LandsatCollection.maskToWaterByNDWI(
2742
3271
  image, threshold=threshold
2743
3272
  )
2744
3273
  )
@@ -2753,7 +3282,7 @@ class LandsatCollection:
2753
3282
  LandsatCollection: LandsatCollection image collection
2754
3283
  """
2755
3284
  if self._masked_clouds_collection is None:
2756
- col = self.collection.map(LandsatCollection.maskL8clouds)
3285
+ col = self.collection.map(LandsatCollection.maskClouds)
2757
3286
  self._masked_clouds_collection = LandsatCollection(collection=col)
2758
3287
  return self._masked_clouds_collection
2759
3288
 
@@ -2766,7 +3295,7 @@ class LandsatCollection:
2766
3295
  LandsatCollection: LandsatCollection image collection
2767
3296
  """
2768
3297
  if self._masked_shadows_collection is None:
2769
- col = self.collection.map(LandsatCollection.maskL8shadows)
3298
+ col = self.collection.map(LandsatCollection.maskShadows)
2770
3299
  self._masked_shadows_collection = LandsatCollection(collection=col)
2771
3300
  return self._masked_shadows_collection
2772
3301
 
@@ -2835,20 +3364,14 @@ class LandsatCollection:
2835
3364
  LandsatCollection: masked LandsatCollection image collection
2836
3365
 
2837
3366
  """
2838
- if self._geometry_masked_collection is None:
2839
- # Convert the polygon to a mask
2840
- mask = ee.Image.constant(1).clip(polygon)
3367
+ # Convert the polygon to a mask
3368
+ mask = ee.Image.constant(1).clip(polygon)
2841
3369
 
2842
- # Update the mask of each image in the collection
2843
- masked_collection = self.collection.map(lambda img: img.updateMask(mask))
3370
+ # Update the mask of each image in the collection
3371
+ masked_collection = self.collection.map(lambda img: img.updateMask(mask)\
3372
+ .copyProperties(img).set('system:time_start', img.get('system:time_start')))
2844
3373
 
2845
- # Update the internal collection state
2846
- self._geometry_masked_collection = LandsatCollection(
2847
- collection=masked_collection
2848
- )
2849
-
2850
- # Return the updated object
2851
- return self._geometry_masked_collection
3374
+ return LandsatCollection(collection=masked_collection)
2852
3375
 
2853
3376
  def mask_out_polygon(self, polygon):
2854
3377
  """
@@ -2861,23 +3384,18 @@ class LandsatCollection:
2861
3384
  LandsatCollection: masked LandsatCollection image collection
2862
3385
 
2863
3386
  """
2864
- if self._geometry_masked_out_collection is None:
2865
- # Convert the polygon to a mask
2866
- full_mask = ee.Image.constant(1)
2867
-
2868
- # Use paint to set pixels inside polygon as 0
2869
- area = full_mask.paint(polygon, 0)
3387
+ # Convert the polygon to a mask
3388
+ full_mask = ee.Image.constant(1)
2870
3389
 
2871
- # Update the mask of each image in the collection
2872
- masked_collection = self.collection.map(lambda img: img.updateMask(area))
3390
+ # Use paint to set pixels inside polygon as 0
3391
+ area = full_mask.paint(polygon, 0)
2873
3392
 
2874
- # Update the internal collection state
2875
- self._geometry_masked_out_collection = LandsatCollection(
2876
- collection=masked_collection
2877
- )
3393
+ # Update the mask of each image in the collection
3394
+ masked_collection = self.collection.map(lambda img: img.updateMask(area)\
3395
+ .copyProperties(img).set('system:time_start', img.get('system:time_start')))
2878
3396
 
2879
3397
  # Return the updated object
2880
- return self._geometry_masked_out_collection
3398
+ return LandsatCollection(collection=masked_collection)
2881
3399
 
2882
3400
  def mask_halite(self, threshold, ng_threshold=None):
2883
3401
  """
@@ -2963,20 +3481,24 @@ class LandsatCollection:
2963
3481
  if classify_above_threshold:
2964
3482
  if mask_zeros is True:
2965
3483
  col = self.collection.map(
2966
- lambda image: image.select(band_name).gte(threshold).rename(band_name).selfMask().copyProperties(image)
3484
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).selfMask()
3485
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
2967
3486
  )
2968
3487
  else:
2969
3488
  col = self.collection.map(
2970
- lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
3489
+ lambda image: image.select(band_name).gte(threshold).rename(band_name)
3490
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
2971
3491
  )
2972
3492
  else:
2973
3493
  if mask_zeros is True:
2974
3494
  col = self.collection.map(
2975
- lambda image: image.select(band_name).lte(threshold).rename(band_name).selfMask().copyProperties(image)
3495
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).selfMask()
3496
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
2976
3497
  )
2977
3498
  else:
2978
3499
  col = self.collection.map(
2979
- lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
3500
+ lambda image: image.select(band_name).lte(threshold).rename(band_name)
3501
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
2980
3502
  )
2981
3503
 
2982
3504
  return LandsatCollection(collection=col)
@@ -3017,6 +3539,237 @@ class LandsatCollection:
3017
3539
  col = self.collection.map(lambda image: LandsatCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
3018
3540
  return LandsatCollection(collection=col)
3019
3541
 
3542
+ def mann_kendall_trend(self, target_band=None, join_method='system:time_start', geometry=None):
3543
+ """
3544
+ Calculates the Mann-Kendall S-value, Variance, Z-Score, and Confidence Level for each pixel in the image collection, in addition to calculating
3545
+ the Sen's slope for each pixel in the image collection. The output is an image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
3546
+
3547
+ This function can be used to identify trends in the image collection over time, such as increasing or decreasing values in the target band, and can be used to assess the significance of these trends.
3548
+ Note that this function is computationally intensive and may take a long time to run for large image collections or high-resolution images.
3549
+
3550
+ The 's_statistic' band represents the Mann-Kendall S-value, which is a measure of the strength and direction of the trend.
3551
+ The 'variance' band represents the variance of the S-value, which is a measure of the variability of the S-value.
3552
+ The 'z_score' band represents the Z-Score, which is a measure of the significance of the trend.
3553
+ The 'confidence' band represents the confidence level of the trend based on the z_score, which is a probabilistic measure of the confidence in the trend (percentage).
3554
+ The 'slope' band represents the Sen's slope, which is a measure of the rate of change in the target band over time. This value can be small as multispectral indices commonly range from -1 to 1, so a slope may have values of <0.2 for most cases.
3555
+
3556
+ Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
3557
+ You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
3558
+ The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
3559
+
3560
+ Args:
3561
+ image_collection (LandsatCollection or ee.ImageCollection): The input image collection for which the Mann-Kendall and Sen's slope trend statistics will be calculated.
3562
+ target_band (str): The band name to be used for the output anomaly image. e.g. 'ndvi'
3563
+ join_method (str, optional): The method used to join images in the collection. Options are 'system:time_start' or 'Date_Filter'. Default is 'system:time_start'.
3564
+ geometry (ee.Geometry, optional): An ee.Geometry object to limit the area over which the trend statistics are calculated and mask the output image. Default is None.
3565
+
3566
+ Returns:
3567
+ ee.Image: An image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
3568
+ """
3569
+ ########## PART 1 - S-VALUE CALCULATION ##########
3570
+ ##### https://vsp.pnnl.gov/help/vsample/design_trend_mann_kendall.htm #####
3571
+ image_collection = self
3572
+ if isinstance(image_collection, LandsatCollection):
3573
+ image_collection = image_collection.collection
3574
+ elif isinstance(image_collection, ee.ImageCollection):
3575
+ pass
3576
+ else:
3577
+ raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid LandsatCollection or ee.ImageCollection object.')
3578
+
3579
+ if target_band is None:
3580
+ raise ValueError('The `target_band` parameter must be specified.')
3581
+ if not isinstance(target_band, str):
3582
+ raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
3583
+
3584
+ if geometry is not None and not isinstance(geometry, ee.Geometry):
3585
+ raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
3586
+ # define the join, which will join all images newer than the current image
3587
+ # use system:time_start if the image does not have a Date_Filter property
3588
+ if join_method == 'system:time_start':
3589
+ # get all images where the leftField value is less than (before) the rightField value
3590
+ time_filter = ee.Filter.lessThan(leftField='system:time_start',
3591
+ rightField='system:time_start')
3592
+ elif join_method == 'Date_Filter':
3593
+ # get all images where the leftField value is less than (before) the rightField value
3594
+ time_filter = ee.Filter.lessThan(leftField='Date_Filter',
3595
+ rightField='Date_Filter')
3596
+ else:
3597
+ raise ValueError(f'The chosen `join_method`: {join_method} does not match the options of "system:time_start" or "Date_Filter".')
3598
+
3599
+ native_projection = image_collection.first().select(target_band).projection()
3600
+
3601
+ # for any matches during a join, set image as a property key called 'future_image'
3602
+ join = ee.Join.saveAll(matchesKey='future_image')
3603
+
3604
+ # apply the join on the input collection
3605
+ # joining all images newer than the current image with the current image
3606
+ joined_collection = ee.ImageCollection(join.apply(primary=image_collection,
3607
+ secondary=image_collection, condition=time_filter))
3608
+
3609
+ # defining a collection to calculate the partial S value for each match in the join
3610
+ # e.g. t4-t1, t3-t1, t2-1 if there are 4 images
3611
+ def calculate_partial_s(current_image):
3612
+ # select the target band for arithmetic
3613
+ current_val = current_image.select(target_band)
3614
+ # get the joined images from the current image properties and cast the joined images as a list
3615
+ future_image_list = ee.List(current_image.get('future_image'))
3616
+ # convert the joined list to an image collection
3617
+ future_image_collection = ee.ImageCollection(future_image_list)
3618
+
3619
+ # define a function that will calculate the difference between the joined images and the current image,
3620
+ # then calculate the partial S sign based on the value of the difference calculation
3621
+ def get_sign(future_image):
3622
+ # select the target band for arithmetic from the future image
3623
+ future_val = future_image.select(target_band)
3624
+ # calculate the difference, i.e. t2-t1
3625
+ difference = future_val.subtract(current_val)
3626
+ # determine the sign of the difference value (1 if diff > 0, 0 if 0, and -1 if diff < 0)
3627
+ # use .unmask(0) to set any masked pixels as 0 to avoid
3628
+
3629
+ sign = difference.signum().unmask(0)
3630
+
3631
+ return sign
3632
+
3633
+ # map the get_sign() function along the future image col
3634
+ # then sum the values for each pixel to get the partial S value
3635
+ return future_image_collection.map(get_sign).sum()
3636
+
3637
+ # calculate the partial s value for each image in the joined/input image collection
3638
+ partial_s_col = joined_collection.map(calculate_partial_s)
3639
+
3640
+ # convert the image collection to an image of s_statistic values per pixel
3641
+ # where the s_statistic is the sum of partial s values
3642
+ # renaming the band as 's_statistic' for later usage
3643
+ final_s_image = partial_s_col.sum().rename('s_statistic').setDefaultProjection(native_projection)
3644
+
3645
+
3646
+ ########## PART 2 - VARIANCE and Z-SCORE ##########
3647
+ # to calculate variance we need to know how many pixels were involved in the partial_s calculations per pixel
3648
+ # we do this by using count() and turn the value to a float for later arithmetic
3649
+ n = image_collection.select(target_band).count().toFloat()
3650
+
3651
+ ##### VARIANCE CALCULATION #####
3652
+ # as we are using floating point values with high precision, it is HIGHLY
3653
+ # unlikely that there will be multiple pixel values with the same value.
3654
+ # Thus, we opt to use the simplified variance calculation approach as the
3655
+ # impacts to the output value are negligible and the processing benefits are HUGE
3656
+ # variance = (n * (n - 1) * (2n + 5)) / 18
3657
+ var_s = n.multiply(n.subtract(1))\
3658
+ .multiply(n.multiply(2).add(5))\
3659
+ .divide(18).rename('variance')
3660
+
3661
+ z_score = ee.Image().expression(
3662
+ """
3663
+ (s > 0) ? (s - 1) / sqrt(var) :
3664
+ (s < 0) ? (s + 1) / sqrt(var) :
3665
+ 0
3666
+ """,
3667
+ {'s': final_s_image, 'var': var_s}
3668
+ ).rename('z_score')
3669
+
3670
+ confidence = z_score.abs().divide(ee.Number(2).sqrt()).erf().rename('confidence')
3671
+
3672
+ stat_bands = ee.Image([var_s, z_score, confidence])
3673
+
3674
+ mk_stats_image = final_s_image.addBands(stat_bands)
3675
+
3676
+ ########## PART 3 - Sen's Slope ##########
3677
+ def add_year_band(image):
3678
+ if join_method == 'Date_Filter':
3679
+ # Get the string 'YYYY-MM-DD'
3680
+ date_string = image.get('Date_Filter')
3681
+ # Parse it into an ee.Date object (handles the conversion to time math)
3682
+ date = ee.Date.parse('YYYY-MM-dd', date_string)
3683
+ else:
3684
+ # Standard way: assumes system:time_start exists
3685
+ date = image.date()
3686
+ years = date.difference(ee.Date('1970-01-01'), 'year')
3687
+ return image.addBands(ee.Image(years).float().rename('year'))
3688
+
3689
+ slope_input = image_collection.map(add_year_band).select(['year', target_band])
3690
+
3691
+ sens_slope = slope_input.reduce(ee.Reducer.sensSlope())
3692
+
3693
+ slope_band = sens_slope.select('slope')
3694
+
3695
+ # add a mask to the final image to remove pixels with less than min_observations
3696
+ # mainly an effort to mask pixels outside of the boundary of the input image collection
3697
+ min_observations = 1
3698
+ valid_mask = n.gte(min_observations)
3699
+
3700
+ final_image = mk_stats_image.addBands(slope_band).updateMask(valid_mask)
3701
+
3702
+ if geometry is not None:
3703
+ mask = ee.Image(1).clip(geometry)
3704
+ final_image = final_image.updateMask(mask)
3705
+
3706
+ return final_image.setDefaultProjection(native_projection)
3707
+
3708
+ def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
3709
+ """
3710
+ Calculates Sen's Slope (trend magnitude) for the collection.
3711
+ This is a lighter-weight alternative to the full `mann_kendall_trend` function if only
3712
+ the direction and magnitude of the trend are needed.
3713
+
3714
+ Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
3715
+ You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
3716
+ The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
3717
+
3718
+ Args:
3719
+ target_band (str): The name of the band to analyze. Defaults to 'ndvi'.
3720
+ join_method (str): Property to use for time sorting ('system:time_start' or 'Date_Filter').
3721
+ geometry (ee.Geometry, optional): Geometry to mask the final output.
3722
+
3723
+ Returns:
3724
+ ee.Image: An image containing the 'slope' band.
3725
+ """
3726
+ image_collection = self
3727
+ if isinstance(image_collection, LandsatCollection):
3728
+ image_collection = image_collection.collection
3729
+ elif isinstance(image_collection, ee.ImageCollection):
3730
+ pass
3731
+ else:
3732
+ raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid LandsatCollection or ee.ImageCollection object.')
3733
+
3734
+ if target_band is None:
3735
+ raise ValueError('The `target_band` parameter must be specified.')
3736
+ if not isinstance(target_band, str):
3737
+ raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
3738
+
3739
+ if geometry is not None and not isinstance(geometry, ee.Geometry):
3740
+ raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
3741
+
3742
+ native_projection = image_collection.first().select(target_band).projection()
3743
+
3744
+ # Add Year Band (Time X-Axis)
3745
+ def add_year_band(image):
3746
+ # Handle user-defined date strings vs system time
3747
+ if join_method == 'Date_Filter':
3748
+ date_string = image.get('Date_Filter')
3749
+ date = ee.Date.parse('YYYY-MM-dd', date_string)
3750
+ else:
3751
+ date = image.date()
3752
+
3753
+ # Convert to fractional years relative to epoch
3754
+ years = date.difference(ee.Date('1970-01-01'), 'year')
3755
+ return image.addBands(ee.Image(years).float().rename('year'))
3756
+
3757
+ # Prepare Collection: Select ONLY [Year, Target]
3758
+ # sensSlope expects Band 0 = Independent (X), Band 1 = Dependent (Y)
3759
+ slope_input = self.collection.map(add_year_band).select(['year', target_band])
3760
+
3761
+ # Run the Native Reducer
3762
+ sens_result = slope_input.reduce(ee.Reducer.sensSlope())
3763
+
3764
+ # Extract and Mask
3765
+ slope_band = sens_result.select('slope')
3766
+
3767
+ if geometry is not None:
3768
+ mask = ee.Image(1).clip(geometry)
3769
+ slope_band = slope_band.updateMask(mask)
3770
+
3771
+ return slope_band.setDefaultProjection(native_projection)
3772
+
3020
3773
  def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
3021
3774
  """
3022
3775
  Masks select pixels of a selected band from an image based on another specified band and threshold (optional).
@@ -3099,7 +3852,8 @@ class LandsatCollection:
3099
3852
  )
3100
3853
 
3101
3854
  # guarantee single band + keep properties
3102
- out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())
3855
+ out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())\
3856
+ .set('system:time_start', prim.get('system:time_start'))
3103
3857
  out = out.set('Date_Filter', prim.get('Date_Filter'))
3104
3858
  return ee.Image(out) # <-- return as Image
3105
3859
 
@@ -3181,7 +3935,7 @@ class LandsatCollection:
3181
3935
  new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
3182
3936
  return new_col.first()
3183
3937
 
3184
- def CollectionStitch(self, img_col2):
3938
+ def collectionStitch(self, img_col2):
3185
3939
  """
3186
3940
  Function to mosaic two LandsatCollection objects which share image dates.
3187
3941
  Mosaics are only formed for dates where both image collections have images.
@@ -3233,9 +3987,15 @@ class LandsatCollection:
3233
3987
 
3234
3988
  # Return a LandsatCollection instance
3235
3989
  return LandsatCollection(collection=new_col)
3990
+
3991
+ def CollectionStitch(self, img_col2):
3992
+ warnings.warn(
3993
+ "CollectionStitch is deprecated and will be removed in future versions. Please use the 'collectionStitch' property instead.",
3994
+ DeprecationWarning, stacklevel=2)
3995
+ return self.collectionStitch(img_col2)
3236
3996
 
3237
3997
  @property
3238
- def MosaicByDate(self):
3998
+ def mosaicByDateDepr(self):
3239
3999
  """
3240
4000
  Property attribute function to mosaic collection images that share the same date.
3241
4001
 
@@ -3300,6 +4060,73 @@ class LandsatCollection:
3300
4060
 
3301
4061
  # Convert the list of mosaics to an ImageCollection
3302
4062
  return self._MosaicByDate
4063
+
4064
+ @property
4065
+ def mosaicByDate(self):
4066
+ """
4067
+ Property attribute function to mosaic collection images that share the same date.
4068
+
4069
+ The property CLOUD_COVER for each image is used to calculate an overall mean,
4070
+ which replaces the CLOUD_COVER property for each mosaiced image.
4071
+ Server-side friendly.
4072
+
4073
+ NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
4074
+
4075
+ Returns:
4076
+ LandsatCollection: LandsatCollection image collection with mosaiced imagery and mean CLOUD_COVER as a property
4077
+ """
4078
+ if self._MosaicByDate is None:
4079
+ distinct_dates = self.collection.distinct("Date_Filter")
4080
+
4081
+ # Define a join to link images by Date_Filter
4082
+ filter_date = ee.Filter.equals(leftField="Date_Filter", rightField="Date_Filter")
4083
+ join = ee.Join.saveAll(matchesKey="date_matches")
4084
+
4085
+ # Apply the join
4086
+ # Primary: Distinct dates collection
4087
+ # Secondary: The full original collection
4088
+ joined_col = ee.ImageCollection(join.apply(distinct_dates, self.collection, filter_date))
4089
+
4090
+ # Define the mosaicking function
4091
+ def _mosaic_day(img):
4092
+ # Recover the list of images for this day
4093
+ daily_list = ee.List(img.get("date_matches"))
4094
+ daily_col = ee.ImageCollection.fromImages(daily_list)
4095
+
4096
+ # Create the mosaic
4097
+ mosaic = daily_col.mosaic().setDefaultProjection(img.projection())
4098
+
4099
+ # Calculate mean metadata properties
4100
+ cloud_percentage = daily_col.aggregate_mean("CLOUD_COVER")
4101
+
4102
+ # Properties to preserve from the representative image
4103
+ props_of_interest = [
4104
+ "SPACECRAFT_ID",
4105
+ "SENSOR_ID",
4106
+ "PROCESSING_LEVEL",
4107
+ "ACQUISITION_DATE",
4108
+ "system:time_start",
4109
+ "Date_Filter"
4110
+ ]
4111
+
4112
+ # Return mosaic with properties set
4113
+ return mosaic.copyProperties(img, props_of_interest).set({
4114
+ "CLOUD_COVER": cloud_percentage
4115
+ })
4116
+
4117
+ # 5. Map the function and wrap the result
4118
+ mosaiced_col = joined_col.map(_mosaic_day)
4119
+ self._MosaicByDate = LandsatCollection(collection=mosaiced_col)
4120
+
4121
+ # Convert the list of mosaics to an ImageCollection
4122
+ return self._MosaicByDate
4123
+
4124
+ @property
4125
+ def MosaicByDate(self):
4126
+ warnings.warn(
4127
+ "MosaicByDate is deprecated and will be removed in future versions. Please use the 'mosaicByDate' property instead.",
4128
+ DeprecationWarning, stacklevel=2)
4129
+ return self.mosaicByDate
3303
4130
 
3304
4131
  @staticmethod
3305
4132
  def ee_to_df(