diff --git a/geetools/Array/__init__.py b/geetools/Array/__init__.py index 948a82ed..aff5f82a 100644 --- a/geetools/Array/__init__.py +++ b/geetools/Array/__init__.py @@ -20,7 +20,7 @@ def __init__(self, obj: ee.Array): """Initialize the Array class.""" self._obj = obj - @classmethod + # -- alternative constructor ----------------------------------------------- def full( self, width: Union[int, float, ee.Number], @@ -50,6 +50,7 @@ def full( width, height = ee.Number(width).toInt(), ee.Number(height).toInt() return ee.Array(ee.List.repeat(ee.List.repeat(value, width), height)) + # -- data maniputlation ---------------------------------------------------- def set( self, x: Union[int, ee.number], diff --git a/geetools/ComputedObject/__init__.py b/geetools/ComputedObject/__init__.py index 8badd15c..b8a29ff3 100644 --- a/geetools/ComputedObject/__init__.py +++ b/geetools/ComputedObject/__init__.py @@ -7,23 +7,11 @@ import ee +from geetools.accessors import geetools_extend -def extend(cls): - """Extends the cls class. - This is only used on the ``ComputedObject`` as it's the parent class of all. - Using the regular accessor would lead to a duplicate member and undesired behavior. - - Parameters: - cls: Class to extend. - - Returns: - Decorator for extending classes. - """ - return lambda f: (setattr(cls, f.__name__, f) or f) - - -@extend(ee.ComputedObject) +# -- types management ---------------------------------------------------------- +@geetools_extend(ee.ComputedObject) def isInstance(self, klass: Type) -> ee.Number: """Return 1 if the element is the passed type or 0 if not. @@ -46,7 +34,8 @@ def isInstance(self, klass: Type) -> ee.Number: return ee.Algorithms.ObjectType(self).compareTo(klass.__name__).eq(0) -@extend(ee.ComputedObject) +# -- .gee files ---------------------------------------------------------------- +@geetools_extend(ee.ComputedObject) def save(self, path: Union[str, Path]) -> Path: """Save a ``ComputedObject`` to a .gee file. @@ -78,7 +67,7 @@ def save(self, path: Union[str, Path]) -> Path: return path -@extend(ee.ComputedObject) +@geetools_extend(ee.ComputedObject) @classmethod def open(cls, path: Union[str, Path]) -> ee.ComputedObject: """Open a .gee file as a ComputedObject. @@ -108,5 +97,4 @@ def open(cls, path: Union[str, Path]) -> ee.ComputedObject: if (path := Path(path)).suffix != ".gee": raise ValueError("File must be a .gee file") - computedObject = ee.deserializer.decode(json.loads(path.read_text())) - return cls(computedObject) + return ee.deserializer.decode(json.loads(path.read_text())) diff --git a/geetools/Date/__init__.py b/geetools/Date/__init__.py index f59dc5a3..d4478a6d 100644 --- a/geetools/Date/__init__.py +++ b/geetools/Date/__init__.py @@ -18,11 +18,17 @@ def __init__(self, obj: ee.Date): """Initialize the Date class.""" self._obj = obj - def toDatetime(self) -> datetime: - """Convert a ``ee.Date`` to a ``datetime.datetime``. + # -- alternative constructor ----------------------------------------------- + @classmethod + def fromEpoch(cls, number: int, unit: str = "day") -> ee.Date: + """Set an the number of units since epoch (1970-01-01). + + Parameters: + number: The number of units since the epoch. + unit: The unit to return the number of. One of: ``second``, ``minute``, ``hour``, ``day``, ``month``, ``year``. Returns: - The ``datetime.datetime`` representation of the ``ee.Date``. + The date as a ``ee.Date`` object. Examples: .. jupyter-execute:: @@ -31,20 +37,22 @@ def toDatetime(self) -> datetime: ee.Initialize() - d = ee.Date('2020-01-01').geetools.toDatetime() - d.strftime('%Y-%m-%d') - + d = ee.Date.geetools.fromEpoch(49, 'year') + d.getInfo() """ - return datetime.fromtimestamp(self._obj.millis().getInfo() / 1000.0) + cls.check_unit(unit) + return ee.Date(EE_EPOCH.isoformat()).advance(number, unit) - def getUnitSinceEpoch(self, unit: str = "day") -> ee.Number: - """Get the number of units since epoch (1970-01-01). + @classmethod + def fromDOY(cls, doy: int, year: int) -> ee.Date: + """Create a date from a day of year and a year. Parameters: - unit: The unit to return the number of. One of: ``second``, ``minute``, ``hour``, ``day``, ``month``, ``year``. + doy: The day of year. + year: The year. Returns: - The number of units since the epoch. + The date as a ``ee.Date`` object. Examples: .. jupyter-execute:: @@ -53,22 +61,18 @@ def getUnitSinceEpoch(self, unit: str = "day") -> ee.Number: ee.Initialize() - d = ee.Date('2020-01-01').geetools.getUnitSinceEpoch('year') + d = ee.Date.geetools.fromDOY(1, 2020) d.getInfo() """ - self._check_unit(unit) - return self._obj.difference(EE_EPOCH, unit).toInt() - - @classmethod - def fromEpoch(cls, number: int, unit: str = "day") -> ee.Date: - """Set an the number of units since epoch (1970-01-01). + doy, year = ee.Number(doy).toInt(), ee.Number(year).toInt() + return ee.Date.fromYMD(year, 1, 1).advance(doy.subtract(1), "day") - Parameters: - number: The number of units since the epoch. - unit: The unit to return the number of. One of: ``second``, ``minute``, ``hour``, ``day``, ``month``, ``year``. + # -- export date ----------------------------------------------------------- + def to_datetime(self) -> datetime: + """Convert a ``ee.Date`` to a ``datetime.datetime``. Returns: - The date as a ``ee.Date`` object. + The ``datetime.datetime`` representation of the ``ee.Date``. Examples: .. jupyter-execute:: @@ -77,22 +81,21 @@ def fromEpoch(cls, number: int, unit: str = "day") -> ee.Date: ee.Initialize() - d = ee.Date.geetools.fromEpoch(49, 'year') - d.getInfo() + d = ee.Date('2020-01-01').geetools.toDatetime() + d.strftime('%Y-%m-%d') + """ - cls._check_unit(unit) - return ee.Date(EE_EPOCH.isoformat()).advance(number, unit) + return datetime.fromtimestamp(self._obj.millis().getInfo() / 1000.0) - @classmethod - def fromDOY(cls, doy: int, year: int) -> ee.Date: - """Create a date from a day of year and a year. + # -- date operations ------------------------------------------------------- + def getUnitSinceEpoch(self, unit: str = "day") -> ee.Number: + """Get the number of units since epoch (1970-01-01). Parameters: - doy: The day of year. - year: The year. + unit: The unit to return the number of. One of: ``second``, ``minute``, ``hour``, ``day``, ``month``, ``year``. Returns: - The date as a ``ee.Date`` object. + The number of units since the epoch. Examples: .. jupyter-execute:: @@ -101,11 +104,11 @@ def fromDOY(cls, doy: int, year: int) -> ee.Date: ee.Initialize() - d = ee.Date.geetools.fromDOY(1, 2020) + d = ee.Date('2020-01-01').geetools.getUnitSinceEpoch('year') d.getInfo() """ - doy, year = ee.Number(doy).toInt(), ee.Number(year).toInt() - return ee.Date.fromYMD(year, 1, 1).advance(doy.subtract(1), "day") + self.check_unit(unit) + return self._obj.difference(EE_EPOCH, unit).toInt() def isLeap(self) -> ee.Number: """Check if the year of the date is a leap year. @@ -133,8 +136,9 @@ def isLeap(self) -> ee.Number: return isLeap.toInt() - @classmethod - def _check_unit(cls, unit: str) -> None: + # -- helper methods -------------------------------------------------------- + @staticmethod + def check_unit(unit: str) -> None: """Check if the unit is valid.""" if unit not in (units := ["second", "minute", "hour", "day", "month", "year"]): raise ValueError(f"unit must be one of: {','.join(units)}") diff --git a/geetools/DateRange/__init__.py b/geetools/DateRange/__init__.py index 9e60c277..c1d98662 100644 --- a/geetools/DateRange/__init__.py +++ b/geetools/DateRange/__init__.py @@ -16,11 +16,8 @@ def __init__(self, obj: ee.DateRange): """Initialize the DateRange class.""" self._obj = obj - def split( - self, - interval: Union[int, ee.Number], - unit: str = "day", - ) -> ee.List: + # -- date range operations ------------------------------------------------- + def split(self, interval: Union[int, ee.Number], unit: str = "day") -> ee.List: """Convert a ``ee.DateRange`` to a list of ``ee.DateRange``. The DateRange will be split in multiple DateRanges of the specified interval and Unit. @@ -43,7 +40,8 @@ def split( d = ee.DateRange('2020-01-01', '2020-01-31').geetools.split(1, 'day') d.getInfo() """ - interval = ee.Number(interval).toInt().multiply(self._unitMillis(unit)) + self.check_unit(unit) + interval = ee.Number(interval).toInt().multiply(self.unitMillis(unit)) start, end = self._obj.start().millis(), self._obj.end().millis() timestampList = ee.List.sequence(start, end, interval) @@ -56,16 +54,16 @@ def split( ) ) - @classmethod - def _check_unit(cls, unit: str) -> None: + # -- utils ----------------------------------------------------------------- + @staticmethod + def check_unit(unit: str) -> None: """Check if the unit is valid.""" if unit not in (units := ["second", "minute", "hour", "day", "month", "year"]): raise ValueError(f"unit must be one of: {','.join(units)}") - @classmethod - def _unitMillis(cls, unit: str) -> ee.Number: + @staticmethod + def unitMillis(unit: str) -> ee.Number: """Get the milliseconds of a unit.""" - cls._check_unit(unit) millis = { "second": 1000, "minute": 1000 * 60, diff --git a/geetools/Dictionary/__init__.py b/geetools/Dictionary/__init__.py index af2aca21..b8a60616 100644 --- a/geetools/Dictionary/__init__.py +++ b/geetools/Dictionary/__init__.py @@ -16,7 +16,7 @@ def __init__(self, obj: ee.Dictionary): """Initialize the Dictionary class.""" self._obj = obj - @classmethod + # -- alternative constructor ----------------------------------------------- def fromPairs(self, list: Union[list, ee.List]) -> ee.Dictionary: """Create a dictionary from a list of [[key, value], ...]] pairs. @@ -41,6 +41,7 @@ def fromPairs(self, list: Union[list, ee.List]) -> ee.Dictionary: values = list.map(lambda pair: ee.List(pair).get(1)) return ee.Dictionary.fromLists(keys, values) + # -- dictionary operations ------------------------------------------------- def sort(self) -> ee.Dictionary: """Sort the dictionary by keys in ascending order. diff --git a/geetools/Filter/__init__.py b/geetools/Filter/__init__.py index ed086359..218779e6 100644 --- a/geetools/Filter/__init__.py +++ b/geetools/Filter/__init__.py @@ -16,7 +16,7 @@ def __init__(self, obj: ee.Filter): """Initialize the Filter class.""" self._obj = obj - @classmethod + # -- date filters ---------------------------------------------------------- def dateRange(self, range: ee.DateRange) -> Any: """Filter by daterange. diff --git a/geetools/Float/__init__.py b/geetools/Float/__init__.py index 76d11aff..0afbab9f 100644 --- a/geetools/Float/__init__.py +++ b/geetools/Float/__init__.py @@ -1,6 +1,12 @@ """Placeholder Float class to be used in the isInstance method.""" +from __future__ import annotations +import ee +from geetools.accessors import geetools_extend + + +@geetools_extend(ee) class Float: """Placeholder Float class to be used in the isInstance method.""" diff --git a/geetools/Image/__init__.py b/geetools/Image/__init__.py index 52320171..c902ecfa 100644 --- a/geetools/Image/__init__.py +++ b/geetools/Image/__init__.py @@ -4,11 +4,10 @@ from typing import Optional, Union import ee +import ee_extra from geetools.accessors import geetools_accessor -from . import _indices - @geetools_accessor(ee.Image) class Image: @@ -18,17 +17,152 @@ def __init__(self, obj: ee.Image): """Initialize the Image class.""" self._obj = obj - # -- Indices manipulation -------------------------------------------------- - index_list = classmethod(_indices.index_list) - spectralIndices = _indices.spectralIndices - tasseledCap = _indices.tasseledCap + # -- image Indices manipulation -------------------------------------------- + def index_list(cls) -> dict: + """Return the list of indices implemented in this module. - # -- the rest -------------------------------------------------------------- + Returns: + List of indices implemented in this module + + Examples: + .. jupyter-execute:: + + import ee, geetools + + ind = ee.Image.geetools.indices()["BAIS2"] + print(ind["long_name"]) + print(ind["formula"]) + print(ind["reference"]) + """ + return ee_extra.Spectral.core.indices() + + def spectralIndices( + self, + index: str = "NDVI", + G: Union[float, int] = 2.5, + C1: Union[float, int] = 6.0, + C2: Union[float, int] = 7.5, + L: Union[float, int] = 1.0, + cexp: Union[float, int] = 1.16, + nexp: Union[float, int] = 2.0, + alpha: Union[float, int] = 0.1, + slope: Union[float, int] = 1.0, + intercept: Union[float, int] = 0.0, + gamma: Union[float, int] = 1.0, + omega: Union[float, int] = 2.0, + beta: Union[float, int] = 0.05, + k: Union[float, int] = 0.0, + fdelta: Union[float, int] = 0.581, + kernel: str = "RBF", + sigma: str = "0.5 * (a + b)", + p: Union[float, int] = 2.0, + c: Union[float, int] = 1.0, + lambdaN: Union[float, int] = 858.5, + lambdaR: Union[float, int] = 645.0, + lambdaG: Union[float, int] = 555.0, + online: Union[float, int] = False, + ): + """Computes one or more spectral indices (indices are added as bands) for an image from the Awesome List of Spectral Indices. + + Parameters: + self: Image to compute indices on. Must be scaled to [0,1]. + index: Index or list of indices to compute, default = 'NDVI' + Available options: + - 'vegetation' : Compute all vegetation indices. + - 'burn' : Compute all burn indices. + - 'water' : Compute all water indices. + - 'snow' : Compute all snow indices. + - 'urban' : Compute all urban (built-up) indices. + - 'kernel' : Compute all kernel indices. + - 'all' : Compute all indices listed below. + - Awesome Spectral Indices for GEE: Check the complete list of indices `here `_. + G: Gain factor. Used just for index = 'EVI', default = 2.5 + C1: Coefficient 1 for the aerosol resistance term. Used just for index = 'EVI', default = 6.0 + C2: Coefficient 2 for the aerosol resistance term. Used just for index = 'EVI', default = 7.5 + L: Canopy background adjustment. Used just for index = ['EVI','SAVI'], default = 1.0 + cexp: Exponent used for OCVI, default = 1.16 + nexp: Exponent used for GDVI, default = 2.0 + alpha: Weighting coefficient used for WDRVI, default = 0.1 + slope: Soil line slope, default = 1.0 + intercept: Soil line intercept, default = 0.0 + gamma: Weighting coefficient used for ARVI, default = 1.0 + omega: Weighting coefficient used for MBWI, default = 2.0 + beta: Calibration parameter used for NDSIns, default = 0.05 + k: Slope parameter by soil used for NIRvH2, default = 0.0 + fdelta: Adjustment factor used for SEVI, default = 0.581 + kernel: Kernel used for kernel indices, default = 'RBF' + Available options: + - 'linear' : Linear Kernel. + - 'RBF' : Radial Basis Function (RBF) Kernel. + - 'poly' : Polynomial Kernel. + sigma: Length-scale parameter. Used for kernel = 'RBF', default = '0.5 * (a + b)'. If str, this must be an expression including 'a' and 'b'. If numeric, this must be positive. + p: Kernel degree. Used for kernel = 'poly', default = 2.0 + c: Free parameter that trades off the influence of higher-order versus lower-order terms in the polynomial kernel. Used for kernel = 'poly', default = 1.0. This must be greater than or equal to 0. + lambdaN: NIR wavelength used for NIRvH2 and NDGI, default = 858.5 + lambdaR: Red wavelength used for NIRvH2 and NDGI, default = 645.0 + lambdaG: Green wavelength used for NDGI, default = 555.0 + drop: Whether to drop all bands except the new spectral indices, default = False + + Returns: + Image with the computed spectral index, or indices, as new bands. + + Examples: + .. jupyter-execute:: + + import ee, geetools + + ee.Initialize() + image = ee.Image('COPERNICUS/S2_SR/20190828T151811_20190828T151809_T18GYT') + image = image.specralIndices(["NDVI", "NDFI"]) + """ + # fmt: off + return ee_extra.Spectral.core.spectralIndices( + self._obj, index, G, C1, C2, L, cexp, nexp, alpha, slope, intercept, gamma, omega, + beta, k, fdelta, kernel, sigma, p, c, lambdaN, lambdaR, lambdaG, online, + drop=False, + ) + # fmt: on + + def tasseledCap(self): + """Calculates tasseled cap brightness, wetness, and greenness components. + + Tasseled cap transformations are applied using coefficients published for these + supported platforms: + + * Sentinel-2 MSI Level 1C + * Landsat 9 OLI-2 SR + * Landsat 9 OLI-2 TOA + * Landsat 8 OLI SR + * Landsat 8 OLI TOA + * Landsat 7 ETM+ TOA + * Landsat 5 TM Raw DN + * Landsat 4 TM Raw DN + * Landsat 4 TM Surface Reflectance + * MODIS NBAR + + Parameters: + self: ee.Image to calculate tasseled cap components for. Must belong to a supported platform. + + Returns: + Image with the tasseled cap components as new bands. + + Examples: + .. jupyter-execute:: + + import ee, geetools + ee.Initialize() + + image = ee.Image('COPERNICUS/S2_SR/20190828T151811_20190828T151809_T18GYT') + img = img.tasseledCap() + """ + return ee_extra.Spectral.core.tasseledCap(self._obj) + + # -- band manipulation ----------------------------------------------------- def addDate(self) -> ee.Image: """Add a band with the date of the image in the provided format. - The date is stored as a Timestamp in millisecond in a band date. + The date is stored as a Timestamp in millisecond in a band "date". Returns: The image with the date band added. @@ -47,9 +181,8 @@ def addDate(self) -> ee.Image: value = date.reduceRegion(ee.Reducer.first(), buffer, 10).get("date") ee.Date(value).format('YYYY-MM-dd').getInfo() """ - return self._obj.addBands( - ee.Image.constant(self._obj.date().millis()).rename("date") - ) + date = self._obj.date().millis() + return self._obj.addBands(ee.Image.constant(date).rename("date")) def addSuffix( self, suffix: Union[str, ee.String], bands: Union[ee.List, list] = [] @@ -117,6 +250,105 @@ def addPrefix( ) return self._obj.rename(bandNames) + def rename(self, names: Union[ee.Dictionary, dict]) -> ee.Image: + """Rename the bands of the image. + + It's the same function as the one from GEE but it takes a dictionary as input. + Keys are the old names and values are the new names. + + Parameters: + names: The new names of the bands. + + Returns: + The image with the new band names. + + Examples: + .. jupyter-execute:: + + import ee, geetools + + ee.Initialize() + + src = 'COPERNICUS/S2_SR_HARMONIZED/20200101T100319_20200101T100321_T32TQM' + image = ee.Image(src).select(['B1', 'B2', 'B3']) + image = image.geetools.rename({'B1': 'Aerosol', 'B2': 'Blue'}) + print(image.bandNames().getInfo()) + """ + names = ee.Dictionary(names) + bands = names.keys().iterate( + lambda b, n: ee.List(n).replace(b, names.get(b)), self._obj.bandNames() + ) + return self._obj.rename(bands) + + def remove(self, bands: Union[list, ee.List]) -> ee.Image: + """Remove bands from the image. + + Parameters: + bands: The bands to remove. + + Returns: + The image without the specified bands. + + Examples: + .. jupyter-execute:: + + import ee, geetools + + ee.Initialize() + + src = 'COPERNICUS/S2_SR_HARMONIZED/20200101T100319_20200101T100321_T32TQM' + image = ee.Image(src).select(['B1', 'B2', 'B3']) + image = image.geetools.remove(['B1', 'B2']) + print(image.bandNames().getInfo()) + """ + bands = self._obj.bandNames().removeAll(ee.List(bands)) + return self._obj.select(bands) + + def doyToDate( + self, + year, + dateFormat: Union[str, ee.String] = "yyyyMMdd", + band: Union[ee.String, str] = "", + ) -> ee.Image: + """Convert the DOY band to a date band. + + This method only work with date formats that can be converted to numbers as earthengine images don't accept string bands. + + Args: + year: The year to use for the date + dateFormat: The date format to use for the date band + band: The band to use as DOY band. If empty, the first one is selected. + + Returns: + The original image with the DOY band converted to a date band. + + Examples: + .. jupyter-execute:: + + import ee, geetools + + ee.Initialize() + + image = ee.Image.random().multiply(365).toInt() + vatican = ee.Geometry.Point([12.4534, 41.9033]).buffer(1) + + image = image.geetools.doyToDate(2023) + print(image.reduceRegion(ee.Reducer.min(), vatican, 1).getInfo()) + """ + year = ee.Number(year) + band = ee.String(band) if band else ee.String(self._obj.bandNames().get(0)) + dateFormat = ee.String(dateFormat) + + doyList = ee.List.sequence(0, 365) + remapList = doyList.map( + lambda d: ee.Number.parse( + ee.Date.fromYMD(year, 1, 1).advance(d, "day").format(dateFormat) + ) + ) + return self._obj.remap(doyList, remapList, bandName=band).rename(band) + + # -- the rest -------------------------------------------------------------- + def getValues( self, point: ee.Geometry.Point, scale: Union[ee.Number, int] = 0 ) -> ee.Dictionary: @@ -199,60 +431,6 @@ def merge(self, images: Union[ee.List, list]) -> ee.Image: merged = images.iterate(lambda dst, src: ee.Image(src).addBands(dst), self._obj) return ee.Image(merged) - def rename(self, names: Union[ee.Dictionary, dict]) -> ee.Image: - """Rename the bands of the image. - - It's the same function as the one from GEE but it takes a dictionary as input. - Keys are the old names and values are the new names. - - Parameters: - names: The new names of the bands. - - Returns: - The image with the new band names. - - Examples: - .. jupyter-execute:: - - import ee, geetools - - ee.Initialize() - - src = 'COPERNICUS/S2_SR_HARMONIZED/20200101T100319_20200101T100321_T32TQM' - image = ee.Image(src).select(['B1', 'B2', 'B3']) - image = image.geetools.rename({'B1': 'Aerosol', 'B2': 'Blue'}) - print(image.bandNames().getInfo()) - """ - names = ee.Dictionary(names) - bands = names.keys().iterate( - lambda b, n: ee.List(n).replace(b, names.get(b)), self._obj.bandNames() - ) - return self._obj.rename(bands) - - def remove(self, bands: Union[list, ee.List]) -> ee.Image: - """Remove bands from the image. - - Parameters: - bands: The bands to remove. - - Returns: - The image without the specified bands. - - Examples: - .. jupyter-execute:: - - import ee, geetools - - ee.Initialize() - - src = 'COPERNICUS/S2_SR_HARMONIZED/20200101T100319_20200101T100321_T32TQM' - image = ee.Image(src).select(['B1', 'B2', 'B3']) - image = image.geetools.remove(['B1', 'B2']) - print(image.bandNames().getInfo()) - """ - bands = self._obj.bandNames().removeAll(ee.List(bands)) - return self._obj.select(bands) - def toGrid( self, size: Union[ee.Number, int] = 1, @@ -614,49 +792,6 @@ def gauss(self, band: Union[ee.String, str] = "") -> ee.Image: }, ).rename(band.cat("_gauss")) - def doyToDate( - self, - year, - dateFormat: Union[str, ee.String] = "yyyyMMdd", - band: Union[ee.String, str] = "", - ) -> ee.Image: - """Convert the DOY band to a date band. - - This method only work with date formats that can be converted to numbers as earthengine images don't accept string bands. - - Args: - year: The year to use for the date - dateFormat: The date format to use for the date band - band: The band to use as DOY band. If empty, the first one is selected. - - Returns: - The original image with the DOY band converted to a date band. - - Examples: - .. jupyter-execute:: - - import ee, geetools - - ee.Initialize() - - image = ee.Image.random().multiply(365).toInt() - vatican = ee.Geometry.Point([12.4534, 41.9033]).buffer(1) - - image = image.geetools.doyToDate(2023) - print(image.reduceRegion(ee.Reducer.min(), vatican, 1).getInfo()) - """ - year = ee.Number(year) - band = ee.String(band) if band else ee.String(self._obj.bandNames().get(0)) - dateFormat = ee.String(dateFormat) - - doyList = ee.List.sequence(0, 365) - remapList = doyList.map( - lambda d: ee.Number.parse( - ee.Date.fromYMD(year, 1, 1).advance(d, "day").format(dateFormat) - ) - ) - return self._obj.remap(doyList, remapList, bandName=band).rename(band) - def repeat(self, band, repeats: Union[ee.Number, int]) -> ee.image: """Repeat a band of the image. diff --git a/geetools/Image/_indices.py b/geetools/Image/_indices.py deleted file mode 100644 index 66b9326f..00000000 --- a/geetools/Image/_indices.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Methods to compute the spectral indices for the Image class.""" -from __future__ import annotations - -from typing import Union - -import ee_extra - - -def index_list(cls) -> dict: - """Return the list of indices implemented in this module. - - Returns: - List of indices implemented in this module - - Examples: - .. jupyter-execute:: - - import ee, geetools - - ind = ee.Image.geetools.indices()["BAIS2"] - print(ind["long_name"]) - print(ind["formula"]) - print(ind["reference"]) - """ - return ee_extra.Spectral.core.indices() - - -def spectralIndices( - self, - index: str = "NDVI", - G: Union[float, int] = 2.5, - C1: Union[float, int] = 6.0, - C2: Union[float, int] = 7.5, - L: Union[float, int] = 1.0, - cexp: Union[float, int] = 1.16, - nexp: Union[float, int] = 2.0, - alpha: Union[float, int] = 0.1, - slope: Union[float, int] = 1.0, - intercept: Union[float, int] = 0.0, - gamma: Union[float, int] = 1.0, - omega: Union[float, int] = 2.0, - beta: Union[float, int] = 0.05, - k: Union[float, int] = 0.0, - fdelta: Union[float, int] = 0.581, - kernel: str = "RBF", - sigma: str = "0.5 * (a + b)", - p: Union[float, int] = 2.0, - c: Union[float, int] = 1.0, - lambdaN: Union[float, int] = 858.5, - lambdaR: Union[float, int] = 645.0, - lambdaG: Union[float, int] = 555.0, - online: Union[float, int] = False, -): - """Computes one or more spectral indices (indices are added as bands) for an image from the Awesome List of Spectral Indices. - - Parameters: - self: Image to compute indices on. Must be scaled to [0,1]. - index: Index or list of indices to compute, default = 'NDVI' - Available options: - - 'vegetation' : Compute all vegetation indices. - - 'burn' : Compute all burn indices. - - 'water' : Compute all water indices. - - 'snow' : Compute all snow indices. - - 'urban' : Compute all urban (built-up) indices. - - 'kernel' : Compute all kernel indices. - - 'all' : Compute all indices listed below. - - Awesome Spectral Indices for GEE: Check the complete list of indices `here `_. - G: Gain factor. Used just for index = 'EVI', default = 2.5 - C1: Coefficient 1 for the aerosol resistance term. Used just for index = 'EVI', default = 6.0 - C2: Coefficient 2 for the aerosol resistance term. Used just for index = 'EVI', default = 7.5 - L: Canopy background adjustment. Used just for index = ['EVI','SAVI'], default = 1.0 - cexp: Exponent used for OCVI, default = 1.16 - nexp: Exponent used for GDVI, default = 2.0 - alpha: Weighting coefficient used for WDRVI, default = 0.1 - slope: Soil line slope, default = 1.0 - intercept: Soil line intercept, default = 0.0 - gamma: Weighting coefficient used for ARVI, default = 1.0 - omega: Weighting coefficient used for MBWI, default = 2.0 - beta: Calibration parameter used for NDSIns, default = 0.05 - k: Slope parameter by soil used for NIRvH2, default = 0.0 - fdelta: Adjustment factor used for SEVI, default = 0.581 - kernel: Kernel used for kernel indices, default = 'RBF' - Available options: - - 'linear' : Linear Kernel. - - 'RBF' : Radial Basis Function (RBF) Kernel. - - 'poly' : Polynomial Kernel. - sigma: Length-scale parameter. Used for kernel = 'RBF', default = '0.5 * (a + b)'. If str, this must be an expression including 'a' and 'b'. If numeric, this must be positive. - p: Kernel degree. Used for kernel = 'poly', default = 2.0 - c: Free parameter that trades off the influence of higher-order versus lower-order terms in the polynomial kernel. Used for kernel = 'poly', default = 1.0. This must be greater than or equal to 0. - lambdaN: NIR wavelength used for NIRvH2 and NDGI, default = 858.5 - lambdaR: Red wavelength used for NIRvH2 and NDGI, default = 645.0 - lambdaG: Green wavelength used for NDGI, default = 555.0 - drop: Whether to drop all bands except the new spectral indices, default = False - - Returns: - Image with the computed spectral index, or indices, as new bands. - - Examples: - .. jupyter-execute:: - - import ee, geetools - - ee.Initialize() - image = ee.Image('COPERNICUS/S2_SR/20190828T151811_20190828T151809_T18GYT') - image = image.specralIndices(["NDVI", "NDFI"]) - """ - # fmt: off - return ee_extra.Spectral.core.spectralIndices( - self._obj, index, G, C1, C2, L, cexp, nexp, alpha, slope, intercept, gamma, omega, - beta, k, fdelta, kernel, sigma, p, c, lambdaN, lambdaR, lambdaG, online, - drop=False, - ) - # fmt: on - - -def tasseledCap(self): - """Calculates tasseled cap brightness, wetness, and greenness components. - - Tasseled cap transformations are applied using coefficients published for these - supported platforms: - - * Sentinel-2 MSI Level 1C - * Landsat 9 OLI-2 SR - * Landsat 9 OLI-2 TOA - * Landsat 8 OLI SR - * Landsat 8 OLI TOA - * Landsat 7 ETM+ TOA - * Landsat 5 TM Raw DN - * Landsat 4 TM Raw DN - * Landsat 4 TM Surface Reflectance - * MODIS NBAR - - Parameters: - self: ee.Image to calculate tasseled cap components for. Must belong to a supported platform. - - Returns: - Image with the tasseled cap components as new bands. - - Examples: - .. jupyter-execute:: - - import ee, geetools - - ee.Initialize() - - image = ee.Image('COPERNICUS/S2_SR/20190828T151811_20190828T151809_T18GYT') - img = img.tasseledCap() - """ - return ee_extra.Spectral.core.tasseledCap(self._obj) diff --git a/geetools/Integer/__init__.py b/geetools/Integer/__init__.py index 82f39bf1..abc71546 100644 --- a/geetools/Integer/__init__.py +++ b/geetools/Integer/__init__.py @@ -1,6 +1,12 @@ """Placeholder Integer class to be used in the isInstance method.""" +from __future__ import annotations +import ee +from geetools.accessors import geetools_extend + + +@geetools_extend(ee) class Integer: """Placeholder Integer class to be used in the isInstance method.""" diff --git a/geetools/User/__init__.py b/geetools/User/__init__.py index aa58088e..97a9a42d 100644 --- a/geetools/User/__init__.py +++ b/geetools/User/__init__.py @@ -8,7 +8,10 @@ import ee from google.oauth2.credentials import Credentials +from geetools.accessors import geetools_extend + +@geetools_extend(ee) class User: """CRUD system to manage multiple user accounts on the same machine.""" diff --git a/geetools/__init__.py b/geetools/__init__.py index bc4a424e..e3f5bb0f 100644 --- a/geetools/__init__.py +++ b/geetools/__init__.py @@ -41,12 +41,8 @@ from .List import List from .Number import Number from .String import String +from .User import User -# from .User import User - -# add the 2 placeholder classes to the ee namespace for consistency -ee.Integer = Integer -ee.Float = Float __title__ = "geetools" __summary__ = "A set of useful tools to use with Google Earth Engine Python" "API" diff --git a/geetools/accessors.py b/geetools/accessors.py index 6b53dfbd..5fc48883 100644 --- a/geetools/accessors.py +++ b/geetools/accessors.py @@ -31,3 +31,17 @@ def decorator(accessor: Any) -> Any: return accessor return decorator + + +def geetools_extend(obj: Any) -> Callable: + """Extends the objcect whatever the type. + + Be careful when using this decorator as it can override direct members of the existing object. + + Parameters: + cls: Class to extend. + + Returns: + Decorator for extending classes. + """ + return lambda f: (setattr(obj, f.__name__, f) or f) diff --git a/geetools/tools/_deprecated_date.py b/geetools/tools/_deprecated_date.py index b8ed0ec1..7cf09dfe 100644 --- a/geetools/tools/_deprecated_date.py +++ b/geetools/tools/_deprecated_date.py @@ -8,7 +8,7 @@ @deprecated(version="1.0.0", reason="Use ee.Date.geetools.toDatetime instead") def toDatetime(date): """Convert from ee to ``datetime.datetime``.""" - return ee.Date(date).geetools.toDatetime() + return ee.Date(date).geetools.to_datetime() @deprecated(version="1.0.0", reason="Epoch is the same for ee and python") diff --git a/test.ipynb b/test.ipynb index 142f3a2b..2a530781 100644 --- a/test.ipynb +++ b/test.ipynb @@ -1,5 +1,37 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'20230105T100319_20230105T100317_T32TQM'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import ee\n", + "\n", + "ee.Initialize()\n", + "\n", + "vatican = ee.Geometry.Point([12.4534, 41.9033]).buffer(100)\n", + "\n", + "image = (\n", + " ee.ImageCollection(\"COPERNICUS/S2\")\n", + " .filterBounds(vatican)\n", + " .filterDate(\"2023-01-01\", \"2023-12-31\")\n", + " .first()\n", + ")\n", + "image.id().getInfo()" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/tests/test_ComputedObect.py b/tests/test_ComputedObect.py index be3d09b5..ef177677 100644 --- a/tests/test_ComputedObect.py +++ b/tests/test_ComputedObect.py @@ -69,13 +69,11 @@ def test_deprecated_geometry(self): class TestSave: """Test the ``save`` method.""" - @pytest.mark.skip(reason="Not working in tests but tested in a notebook") def test_save(self, tmp_path): file = tmp_path / "test.gee" ee.Number(1.1).save(file) assert file.exists() - @pytest.mark.skip(reason="Not working in tests but tested in a notebook") def test_deprecated_method(self, tmp_path): file = tmp_path / "test.gee" with pytest.deprecated_call(): @@ -86,15 +84,13 @@ def test_deprecated_method(self, tmp_path): class TestOpen: """Test the ``open`` method.""" - @pytest.mark.skip(reason="Not working in tests but tested in a notebook") def test_open(self, tmp_path): (object := ee.Number(1.1)).save((file := tmp_path / "test.gee")) opened = ee.Number.open(file) - assert object.equals(opened).getInfo() + assert object.eq(opened).getInfo() - @pytest.mark.skip(reason="Not working in tests but tested in a notebook") def test_deprecated_method(self, tmp_path): (object := ee.Number(1.1)).save((file := tmp_path / "test.gee")) with pytest.deprecated_call(): opened = geetools.manager.eopen(file) - assert object.equals(opened).getInfo() + assert object.eq(opened).getInfo() diff --git a/tests/test_Date.py b/tests/test_Date.py index d269fe67..3c6454e3 100644 --- a/tests/test_Date.py +++ b/tests/test_Date.py @@ -9,7 +9,7 @@ class TestToDatetime: """Test the toDatetime method.""" def test_to_datetime(self, date_instance): - datetime = date_instance.geetools.toDatetime() + datetime = date_instance.geetools.to_datetime() assert datetime.year == 2020 assert datetime.month == 1 assert datetime.day == 1