"""Block for Google Earth Engine images and image collections."""
from __future__ import annotations
import math
from logging import getLogger
from typing import Any, Literal
import ee
import geetools # noqa: F401
from geepillow import fonts
from geepillow.blocks import DEFAULT_MODE, Block, FontType, ImageBlock, PositionType, TextBlock
from geepillow.colors import Color
from geepillow.grids import Grid
from geepillow.image import from_eeimage
[docs]
logger = getLogger(__name__)
[docs]
TextPositionType = Literal["top", "bottom"]
[docs]
DEFAULT_GRID_FONT = fonts.opensans_bold(24)
[docs]
class EEImageBlock(ImageBlock):
"""EEImageBlock."""
def __init__(
self,
ee_image: ee.Image,
viz_params: dict | None = None,
dimensions: tuple | int = Block.DEFAULT_SIZE,
scale: int | None = None,
region: ee.Geometry | ee.Feature | None = None,
overlay: ee.FeatureCollection | ee.Feature | ee.Geometry | None = None,
overlay_style: dict | None = None,
style_property: str | None = None,
position: tuple | PositionType = "center-center",
fit_block: bool = True,
keep_proportion: bool = True,
size: tuple | None = None,
background_color: str | Color = "white",
background_opacity: float = 1,
mode: str = DEFAULT_MODE,
):
"""EEImageBlock.
By default, the size of the image matches the size of the block.
Args:
ee_image: Earth Engine image.
viz_params: Visualization parameters.
dimensions: dimensions of the image, in pixels. If only one number is passed, it is used as the maximum, and
the other dimension is computed by proportional scaling.
scale: spatial resolution.
region: region of interest to "clip" the image to.
overlay: a feature collection to overlay on top of the image.
overlay_style: style of the overlay.
style_property: A per-feature property expected to contain a dictionary. Values in the dictionary override
any default values for that feature.
position: position of the image inside the block.
fit_block: if True the element's boundaries will never exceed the block.
keep_proportion: keep proportion (ratio) of the image.
size: size of the block (not the image). If None it will be taken from the dimensions of the image.
background_color: color of the background.
background_opacity: opacity of the background.
mode: mode of the background image
"""
[docs]
self.ee_image = ee_image
[docs]
self.viz_params = viz_params or dict(min=0, max=1)
[docs]
self.dimensions = dimensions
[docs]
self.overlay_style = overlay_style
[docs]
self.style_property = style_property
if size is None and isinstance(dimensions, (int, float)):
size = (dimensions, dimensions)
elif isinstance(dimensions, (tuple, list)):
size = dimensions
image = from_eeimage(
image=ee_image,
dimensions=self.dimensions,
viz_params=self.viz_params,
scale=self.scale,
region=self.region,
overlay=overlay,
overlay_style=overlay_style,
style_property=style_property,
)
super(EEImageBlock, self).__init__(
image=image,
position=position,
fit_block=fit_block,
keep_proportion=keep_proportion,
size=size,
background_color=background_color,
background_opacity=background_opacity,
mode=mode,
)
[docs]
class EEImageCollectionGrid(Grid):
"""A Grid for ImageCollections."""
def __init__(
self,
collection: ee.ImageCollection,
viz_params: dict | None = None,
scale: int | None = None,
region: ee.Geometry | ee.Feature | None = None,
text_pattern: str | None = None,
text_position: TextPositionType = "bottom",
font: str | FontType = DEFAULT_GRID_FONT,
overlay: ee.FeatureCollection | ee.Feature | ee.Geometry | None = None,
overlay_style: dict | None = None,
style_property: str | None = None,
x_space: int = 10,
y_space: int = 10,
n_columns: int | None = None,
n_rows: int | None = None,
image_dimensions: tuple | int | None = None,
dimensions: tuple = (3300, 2250),
image_position: tuple | PositionType = "center-center",
text_inner_position: tuple | PositionType = "center-center",
position: tuple | PositionType = "center-center",
fit_block: bool = True,
keep_proportion: bool = True,
size: tuple | None = None,
background_color: str | Color = "white",
background_opacity: float = 1,
mode: str = DEFAULT_MODE,
):
"""A grid for image collections.
This class is designed for Image Collection with overlapping images.
If n_columns is not None and n_rows is None, the number of rows will be computed
using the number of images. Same the other way around. If both are None, n_columns
will be set to 3.
Args:
collection: Earth Engine image collection.
n_columns: number of columns.
n_rows: number of rows.
viz_params: Visualization parameters.
scale: spatial resolution.
text_pattern: A text pattern to include in the position indicated by text_position param.
Properties of the image can be used inside this text following this guide:
https://geetools.readthedocs.io/en/stable/autoapi/geetools/ee_string/StringAccessor.format.html
text_position: the position of the text block.
font: font to use. The size the font is included in this parameter.
region: region of interest to "clip" each image to. If None it uses the geometry of each image.
overlay: a feature collection to overlay on top of the image.
overlay_style: style of the overlay.
style_property: A per-feature property expected to contain a dictionary. Values in the dictionary override
any default values for that feature.
image_dimensions: dimensions of the image, in pixels. If only one number is passed, it is used as the
maximum, and the other dimension is computed by proportional scaling.
dimensions: dimensions of the grid image in pixels. The default value corresponds to the size of a Letter
at 300 DPI (landscape orientation).
image_position: position of the image inside its block.
text_inner_position: position of the text inside its block.
position: position of the grid inside its block.
fit_block: if True the element's boundaries will never exceed the block.
keep_proportion: keep proportion (ratio) of the image.
size: size of the block (not the image).
background_color: color of the background.
background_opacity: opacity of the background.
mode: mode of the background image
x_space: space on the x axis.
y_space: space on the y axis.
"""
[docs]
self.collection = collection
if n_columns is None and n_rows is None and image_dimensions is None:
raise ValueError(
"You need to pass at least one of: `n_columns`, `n_rows` or `image_dimensions`."
)
if image_dimensions is not None and isinstance(image_dimensions, (int, float)):
image_dimensions = (image_dimensions, image_dimensions)
[docs]
self.viz_params = viz_params or dict(min=0, max=1)
[docs]
self.dimensions = dimensions
[docs]
self.overlay_style = overlay_style
[docs]
self.style_property = style_property
[docs]
self.text_pattern = text_pattern
[docs]
self.text_position = text_position
[docs]
self.image_position = image_position
[docs]
self.text_inner_position = text_inner_position
[docs]
self._image_dimensions = image_dimensions
[docs]
self._n_columns = n_columns
blocks = self.make_blocks()
super().__init__(
blocks=blocks,
x_space=x_space,
y_space=y_space,
position=position,
size=size or dimensions,
fit_block=fit_block,
keep_proportion=keep_proportion,
background_color=background_color,
background_opacity=background_opacity,
mode=mode,
)
@property
[docs]
def image_ids(self):
"""Ids of all the images in the collection."""
if self._image_ids is None:
self._image_ids = self.collection.aggregate_array("system:index").getInfo()
return self._image_ids
@property
[docs]
def image_dimensions(self):
"""Dimensions of each image."""
if self._image_dimensions is not None:
image_dimensions = self._image_dimensions
else:
# compute image dimensions using the number of columns and the dimensions of the grid
# compute max width
x_dimension = self.dimensions[0]
spaces = self.x_space * (self.n_columns - 1)
width = math.floor((x_dimension - spaces) / self.n_columns)
# compute max height
y_dimension = self.dimensions[1]
spaces = self.y_space * (self.n_rows - 1)
height = math.floor((y_dimension - spaces) / self.n_rows)
dim = min(width, height)
image_dimensions = (dim, dim)
return image_dimensions
@property
[docs]
def n_columns(self) -> int:
"""Number of columns."""
if self._n_columns is not None:
return self._n_columns
elif self._n_rows is not None:
return math.ceil(len(self.image_ids) / self._n_rows)
else:
if self._image_dimensions:
return math.floor(
(self.dimensions[0] + self.x_space) / (self._image_dimensions[0] + self.x_space)
)
else:
return 3
@property
[docs]
def n_last(self) -> int:
"""Number of elements in the last row."""
return len(self.image_ids) % self.n_columns
@property
[docs]
def n_rows(self):
"""Number of rows."""
if self._n_rows is not None:
n_rows = self._n_rows
else:
n_rows = int(len(self.image_ids) / self.n_columns)
if self.n_last > 0:
n_rows += 1
return n_rows
[docs]
def make_image_block(self, image: ee.Image) -> Block:
"""Make the block for the image and text block if needed."""
from geepillow.strips import Strip
image_block = EEImageBlock(
image,
viz_params=self.viz_params,
dimensions=self.image_dimensions,
scale=self.scale,
region=self.region,
overlay=self.overlay,
overlay_style=self.overlay_style,
style_property=self.style_property,
)
if self.text_pattern is None:
return image_block
# all properties on the server-side
properties = image.toDictionary(image.propertyNames())
formatted = ee.String(self.text_pattern).geetools.format(properties)
text = formatted.getInfo()
txt_block = TextBlock(text, self.text_inner_position, font=self.font)
strip_blocks: list[Any] = (
[txt_block, image_block] if self.text_position == "top" else [image_block, txt_block]
)
return Strip(strip_blocks, self.y_space, "vertical")
[docs]
def make_blocks(self) -> list[list[Block]]:
"""Make the list of blocks for the grid."""
grid_blocks: list[list[Block]] = []
i = 0
while i < len(self.image_ids):
row: list[Block] = []
# detect if we are in the last row and it is not complete
last_row = len(grid_blocks) == self.n_rows - 1
if not last_row:
columns = range(self.n_columns)
else:
columns = range(self.n_last if self.n_last > 0 else self.n_columns)
for _ in columns:
iid = self.image_ids[i]
eeimage = ee.Image(
self.collection.filter(ee.Filter.eq("system:index", iid)).first()
)
item_block = self.make_image_block(eeimage)
row.append(item_block)
i += 1
grid_blocks.append(row)
return grid_blocks