"""
The eodata module provides core objects for handling remote sensing multi-temporal data (such as satellite imagery).
Copyright (c) 2017- Sinergise and contributors
For the full list of contributors, see the CREDITS file in the root directory of this source tree.
This source code is licensed under the MIT license, see the LICENSE file in the root directory of this source tree.
"""
from __future__ import annotations
import datetime as dt
import string
from dataclasses import dataclass, field
from typing import Any
import geopandas as gpd
import numpy as np
import pandas as pd
from geopandas.testing import assert_geodataframe_equal
from numpy.testing import assert_array_equal
from sentinelhub import CRS, BBox
from ..constants import FeatureType
from ..eodata import EOPatch
from ..types import FeaturesSpecification
from ..utils.parsing import FeatureParser
DEFAULT_BBOX = BBox((0, 0, 100, 100), crs=CRS("EPSG:32633"))
[docs]@dataclass
class PatchGeneratorConfig:
"""Dataclass containing a more complex setup of the PatchGenerator class."""
num_timestamps: int = 5
timestamps_range: tuple[dt.datetime, dt.datetime] = (dt.datetime(2019, 1, 1), dt.datetime(2019, 12, 31))
timestamps: list[dt.datetime] = field(init=False, repr=False)
max_integer_value: int = 256
raster_shape: tuple[int, int] = (98, 151)
depth_range: tuple[int, int] = (1, 3)
def __post_init__(self) -> None:
self.timestamps = list(pd.date_range(*self.timestamps_range, periods=self.num_timestamps).to_pydatetime())
[docs]def generate_eopatch(
features: FeaturesSpecification | None = None,
bbox: BBox = DEFAULT_BBOX,
timestamps: list[dt.datetime] | None = None,
seed: int = 42,
config: PatchGeneratorConfig | None = None,
) -> EOPatch:
"""A class for generating EOPatches with dummy data."""
config = config if config is not None else PatchGeneratorConfig()
parsed_features = FeatureParser(
features or [], lambda feature_type: feature_type.is_array() or feature_type == FeatureType.META_INFO
).get_features()
rng = np.random.default_rng(seed)
timestamps = timestamps if timestamps is not None else config.timestamps
patch = EOPatch(bbox=bbox, timestamps=timestamps)
# fill eopatch with random data
# note: the patch generation functionality could be extended by generating extra random features
for ftype, fname in parsed_features:
if ftype == FeatureType.META_INFO:
patch[(ftype, fname)] = "".join(rng.choice(list(string.ascii_letters), 20))
else:
shape = _get_feature_shape(rng, ftype, timestamps, config)
patch[(ftype, fname)] = _generate_feature_data(rng, ftype, shape, config)
return patch
def _generate_feature_data(
rng: np.random.Generator, ftype: FeatureType, shape: tuple[int, ...], config: PatchGeneratorConfig
) -> np.ndarray:
if ftype.is_discrete():
return rng.integers(config.max_integer_value, size=shape)
return rng.normal(size=shape)
def _get_feature_shape(
rng: np.random.Generator, ftype: FeatureType, timestamps: list[dt.datetime], config: PatchGeneratorConfig
) -> tuple[int, ...]:
time, height, width, depth = len(timestamps), *config.raster_shape, rng.integers(*config.depth_range)
if ftype.is_spatial() and not ftype.is_vector():
return (time, height, width, depth) if ftype.is_temporal() else (height, width, depth)
return (time, depth) if ftype.is_temporal() else (depth,)
[docs]def assert_feature_data_equal(tested_feature: Any, expected_feature: Any) -> None:
"""Asserts that the data of two features is equal. Cases are specialized for common data found in EOPatches."""
if isinstance(tested_feature, np.ndarray) and isinstance(expected_feature, np.ndarray):
assert_array_equal(tested_feature, expected_feature)
elif isinstance(tested_feature, gpd.GeoDataFrame) and isinstance(expected_feature, gpd.GeoDataFrame):
assert CRS(tested_feature.crs) == CRS(expected_feature.crs)
assert_geodataframe_equal(
tested_feature, expected_feature, check_crs=False, check_index_type=False, check_dtype=False
)
else:
assert tested_feature == expected_feature