From 8a59ec0b622b478257d54e2aedf522124ae75f41 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Wed, 6 Mar 2024 20:16:59 +0100
Subject: [PATCH 01/26] Update 2 files

- /simplestac/extents.py
- /pyproject.toml
---
 pyproject.toml        |   1 +
 simplestac/extents.py | 113 +++++++++++++++---------------------------
 2 files changed, 40 insertions(+), 74 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 7dd4f3d..405ec2f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,6 +26,7 @@ dependencies = [
   "rioxarray",
   "stac_static@git+https://github.com/jsignell/stac-static",
   "stackstac",
+  "rtree",
 ]
 
 [project.optional-dependencies]
diff --git a/simplestac/extents.py b/simplestac/extents.py
index 1979f67..379898d 100644
--- a/simplestac/extents.py
+++ b/simplestac/extents.py
@@ -1,12 +1,12 @@
 """
-Module to deal with STAC Extents.
+Deal with STAC Extents.
 """
 import pystac
 from dataclasses import dataclass
 from datetime import datetime
 from typing import Union
-
-Coords = list[Union[int, float]]
+from stacflow.common_types import Bbox
+from rtree import index
 
 
 @dataclass
@@ -14,7 +14,7 @@ class SmartBbox:
     """
     Small class to work with a single 2D bounding box.
     """
-    coords: Coords = None  # [xmin, ymin, xmax, ymax]
+    coords: Bbox = None  # [xmin, ymin, xmax, ymax]
 
     def touches(self, other: "SmartBbox") -> bool:
         """
@@ -53,7 +53,7 @@ class SmartBbox:
             ]
 
 
-def clusterize_bboxes(bboxes: list[Coords]) -> list[Coords]:
+def clusterize_bboxes(bboxes: list[Bbox]) -> list[Bbox]:
     """
     Computes a list of bounding boxes regrouping all overlapping ones.
 
@@ -64,74 +64,38 @@ def clusterize_bboxes(bboxes: list[Coords]) -> list[Coords]:
         list of 2D bounding boxes (list of int of float)
 
     """
-    # Regroup bboxes into clusters of bboxes
-    smart_bboxes = [SmartBbox(bbox) for bbox in bboxes]
-    clusters = bboxes_to_bboxes_clusters(smart_bboxes)
-
-    # Compute clusters extents
-    clusters_unions = [SmartBbox() for _ in clusters]
-    for i, cluster in enumerate(clusters):
-        for smart_bbox in cluster:
-            clusters_unions[i].update(smart_bbox)
-
-    return [smart_bbox.coords.copy() for smart_bbox in clusters_unions]
-
-
-def bboxes_to_bboxes_clusters(smart_bboxes: list[SmartBbox]) -> list[
-    list[SmartBbox]]:
-    """
-    Transform a list of bounding boxes into a nested list of clustered ones.
-
-    Args:
-        smart_bboxes: a list of `SmartBbox` instances
-
-    Returns:
-        a list of `SmartBbox` instances list
-
-    """
-    clusters_labels = compute_smart_bboxes_clusters(smart_bboxes)
-    clusters_bboxes = [[] for _ in range(max(clusters_labels) + 1)]
-    for smart_bbox, labels in zip(smart_bboxes, clusters_labels):
-        clusters_bboxes[labels].append(smart_bbox)
-    return clusters_bboxes
-
-
-def compute_smart_bboxes_clusters(smart_bboxes: list[SmartBbox]) -> list[int]:
-    """
-    Compute the extent of a cluster of `SmartBbox` instances.
-
-    Args:
-        smart_bboxes: a list of `SmartBbox` instances
-
-    Returns:
-        a vector of same size as `smart_bboxes` with the group numbers (int)
-
-    """
-    labels = len(smart_bboxes) * [None]
-    group = 0
-
-    def dfs(index: int):
-        """
-        Deep first search with o(n) complexity.
+    bboxes = [SmartBbox(bbox) for bbox in bboxes]
+    idx = index.Index()
+    for i, bbox in enumerate(bboxes):
+        idx.insert(id=i, coordinates=bbox.coords)
+    clusters = [bboxes.pop()]
+
+    def _update_clusters_idx():
+        _clusters_idx = index.Index()
+        for _i, cluster_bbox in enumerate(clusters):
+            _clusters_idx.insert(id=_i, coordinates=cluster_bbox.coords)
+        return _clusters_idx
+
+    clusters_idx = _update_clusters_idx()
+    while bboxes:
+        bbox = bboxes.pop()
+        inter_clusters = list(clusters_idx.intersection(bbox.coords))
+        if inter_clusters:
+            # We merge all intersecting clusters into one
+            clusters[inter_clusters[0]].update(bbox)
+            for i in inter_clusters[1:]:
+                clusters[inter_clusters[0]].update(clusters[i])
+            clusters = [
+                cluster
+                for i, cluster in enumerate(clusters)
+                if i not in inter_clusters[1:]
+            ]
+        else:
+            clusters.append(bbox)
 
-        Args:
-            index: vertex index.
+        clusters_idx = _update_clusters_idx()
 
-        """
-        labels[index] = group
-        cur_item = smart_bboxes[index]
-        for i, (item, label) in enumerate(zip(smart_bboxes, labels)):
-            if i != index and cur_item.touches(item) and labels[i] is None:
-                dfs(i)
-
-    while any(label is None for label in labels):
-        next_unmarked = next(
-            i for i, label in enumerate(labels)
-            if label is None
-        )
-        dfs(next_unmarked)
-        group += 1
-    return labels
+    return [cluster.coords for cluster in clusters]
 
 
 class AutoSpatialExtent(pystac.SpatialExtent):
@@ -151,7 +115,7 @@ class AutoSpatialExtent(pystac.SpatialExtent):
         super().__init__(*args, **kwargs)
         self.clusterize_bboxes()
 
-    def update(self, other: pystac.SpatialExtent | Coords):
+    def update(self, other: pystac.SpatialExtent | Bbox):
         """
         Updates itself with a new spatial extent or bounding box. Modifies
         inplace `self.bboxes`.
@@ -208,9 +172,10 @@ class AutoTemporalExtent(pystac.TemporalExtent):
         all_dates = []
         for interval in self.intervals:
             if isinstance(interval, (list, tuple)):
-                all_dates += [i for i in interval]
+                all_dates += [i for i in interval if i is not None]
             elif isinstance(interval, datetime):
                 all_dates.append(interval)
             else:
                 TypeError(f"Unsupported date/range of: {interval}")
-        self.intervals = [[min(all_dates), max(all_dates)]]
+        self.intervals = \
+            [[min(all_dates), max(all_dates)]] if all_dates else [None, None]
-- 
GitLab


From 36ae451e2fd102940d8f26591edbec125f778a61 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 14:21:52 +0100
Subject: [PATCH 02/26] update some docstring

---
 simplestac/utils.py | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index fecce72..20459d8 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -610,19 +610,23 @@ def update_item_properties(x: pystac.Item, remove_item_props=DEFAULT_REMOVE_PROP
             x.properties.pop(k)
 
 
-def harmonize_sen2cor_offet(collection, bands=S2_SEN2COR_BANDS, inplace=False):
+def harmonize_sen2cor_offet(x, bands=S2_SEN2COR_BANDS, inplace=False):
     """
     Harmonize new Sentinel-2 item collection (Sen2Cor v4+, 2022-01-25)
     to the old baseline (v3-):
-    adds an offset of -1000 to all band assets of items
+    adds an offset of -1000 to the asset extra field "raster:bands" of the items
     with datetime >= 2022-01-25
 
     Parameters
     ----------
-    data: ItemCollection
+    x: ItemCollection
         An item collection of S2 scenes
     bands: list
         A list of bands to harmonize
+    
+    inplace: bool
+        Whether to modify the collection in place. Defaults to False.
+        In that case, a cloned collection is returned.
 
     Returns
     -------
-- 
GitLab


From 7895a526a40a1a13da55d8e436c3e7fb1a91b56f Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 15:35:29 +0100
Subject: [PATCH 03/26] update changelog and CI

---
 .gitlab-ci.yml |  3 +++
 CHANGELOG.md   | 18 ++++++++++++++++++
 2 files changed, 21 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8a24467..5df5950 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,6 +16,9 @@ tests:
     - micromamba env list
     - pip install -e .
     - pytest -s
+  only:
+   - merge-request
+   - schedules
   
 pages:
   stage: pages
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e69de29..0f2699d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1,18 @@
+# v1.1.0
+
+## Add
+
+- function `write_assets`: write item assets (rasters only at the moment) of an ItemCollection locally and return the corresponding ItemCollection.
+- function `harmonize_sen2cor_offset`: adds an `offset` property to the assets so it is taken into account by `to_xarray`.
+- method `ItemCollection.drop_duplicates`: drop duplicated ID returned by pgstac.
+- method `ItemCollection.drop_non_raster`: drop non raster assets.
+- `writer_args` to `ItemCollection.apply_...` methods and function in order to specify the outputs format, e.g. the encoding.
+- in local.py, `start_datetime` and `end_datetime` can now be used instead of `datetime` in the template used to build a local ItemCollection.
+- module `extents.py` to manipulate STAC extents.
+- tests for CI
+
+## Fix
+
+- `apply_formula` with "in" operator in apply_formula.
+- COG type of local STAC assets (instead of GTiff)
+- imports in `simplestac.utils`
-- 
GitLab


From fe5b77ca1fbb0691a85f66f74cd9b324c7294a29 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 17:00:24 +0100
Subject: [PATCH 04/26] amend previous commit

---
 simplestac/utils.py | 88 ++++++++++++++++++++++-----------------------
 1 file changed, 43 insertions(+), 45 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 20459d8..23b8e03 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -609,51 +609,6 @@ def update_item_properties(x: pystac.Item, remove_item_props=DEFAULT_REMOVE_PROP
         for k in pop_props:
             x.properties.pop(k)
 
-
-def harmonize_sen2cor_offet(x, bands=S2_SEN2COR_BANDS, inplace=False):
-    """
-    Harmonize new Sentinel-2 item collection (Sen2Cor v4+, 2022-01-25)
-    to the old baseline (v3-):
-    adds an offset of -1000 to the asset extra field "raster:bands" of the items
-    with datetime >= 2022-01-25
-
-    Parameters
-    ----------
-    x: ItemCollection
-        An item collection of S2 scenes
-    bands: list
-        A list of bands to harmonize
-    
-    inplace: bool
-        Whether to modify the collection in place. Defaults to False.
-        In that case, a cloned collection is returned.
-
-    Returns
-    -------
-    ItemCollection
-        A collection of S2 scenes with extra_fields["raster:bands"]
-        added/updated to each band asset with datetime >= 2022-01-25.
-    
-    Notes
-    -----
-    References:
-    - https://planetarycomputer.microsoft.com/dataset/sentinel-2-l2a#Baseline-Change
-    - https://github.com/microsoft/PlanetaryComputer/issues/134
-    """
-    
-    if not inplace:
-        collection = collection.copy()
-    for item in collection:
-        for asset in bands:
-            if asset in item.assets:
-                if item.properties["datetime"] >= "2022-01-25":
-                    item.assets[asset].extra_fields["raster:bands"] = [dict(offset=-1000)]
-                else:
-                    item.assets[asset].extra_fields["raster:bands"] = [dict(offset=0)]
-    if inplace:
-        return collection
-
-
 def apply_item(x, fun, name, output_dir, overwrite=False,
                copy=True, bbox=None, geometry=None, writer_args=None, **kwargs):
     """
@@ -880,4 +835,47 @@ def apply_formula(x, formula):
 
     return eval(formula)
 
+def harmonize_sen2cor_offet(x, bands=S2_SEN2COR_BANDS, inplace=False):
+    """
+    Harmonize new Sentinel-2 item collection (Sen2Cor v4+, 2022-01-25)
+    to the old baseline (v3-):
+    adds an offset of -1000 to the asset extra field "raster:bands" of the items
+    with datetime >= 2022-01-25
+
+    Parameters
+    ----------
+    x: ItemCollection
+        An item collection of S2 scenes
+    bands: list
+        A list of bands to harmonize
+    
+    inplace: bool
+        Whether to modify the collection in place. Defaults to False.
+        In that case, a cloned collection is returned.
+
+    Returns
+    -------
+    ItemCollection
+        A collection of S2 scenes with extra_fields["raster:bands"]
+        added/updated to each band asset with datetime >= 2022-01-25.
+    
+    Notes
+    -----
+    References:
+    - https://planetarycomputer.microsoft.com/dataset/sentinel-2-l2a#Baseline-Change
+    - https://github.com/microsoft/PlanetaryComputer/issues/134
+    """
+    
+    if not inplace:
+        x = x.copy()
+    for item in x:
+        for asset in bands:
+            if asset in item.assets:
+                if item.properties["datetime"] >= "2022-01-25":
+                    item.assets[asset].extra_fields["raster:bands"] = [dict(offset=-1000)]
+                else:
+                    item.assets[asset].extra_fields["raster:bands"] = [dict(offset=0)]
+    if inplace:
+        return x
+
 #######################################
\ No newline at end of file
-- 
GitLab


From 5951e2af26015b9b4972c316c3da13ecf248ddfc Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 17:04:10 +0100
Subject: [PATCH 05/26] add function extract_points

---
 simplestac/utils.py | 50 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 23b8e03..94d920a 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -878,4 +878,54 @@ def harmonize_sen2cor_offet(x, bands=S2_SEN2COR_BANDS, inplace=False):
     if inplace:
         return x
 
+def extract_points(x, df, **kwargs):
+    """_summary_
+
+    Parameters
+    ----------
+    x : xarray.DataArray or xarray.Dataset
+    df : pandas.DataFrame
+        Coordinates of the points
+
+    Returns
+    -------
+    xarray.DataArray or xarray.Dataset
+        The points values, tha can be converted to
+        dataframe with `to_dataframe` `to_dask_dataframe`
+
+    Examples
+    --------
+    >>> import xarray as xr
+    >>> import pandas as pd
+    >>> import dask.array
+    >>> import numpy as np
+    >>> da = xr.DataArray(
+    ... # np.random.random((100,200)),
+    ... dask.array.random.random((100,200,10), chunks=10),
+    ... coords = [('x', np.arange(100)+.5), 
+    ...           ('y', np.arange(200)+.5),
+    ...           ('z', np.arange(10)+.5)]
+    ... ).rename("pixel_value")
+    >>> df = pd.DataFrame(
+    ...    dict(
+    ...        x=np.random.permutation(range(100))[:100]+np.random.random(100),
+    ...        y=np.random.permutation(range(100))[:100]+np.random.random(100),
+    ...        other=range(100),
+    ...    )
+    ... )
+    >>> df.index.rename("id_point", inplace=True)
+    >>> extraction = extract_points(da, df, method="nearest", tolerance=.5)
+    >>> ext_df = extraction.to_dataframe()
+    >>> ext_df.reset_index(drop=False, inplace=True)
+    >>> ext_df.rename({k: k+"_pixel" for k in da.dims}, axis=1, inplace=True)
+    >>> # join extraction to original dataframe
+    >>> df.merge(ext_df, on=["id_point"])
+
+    """
+    # x = da
+    xk = x.dims
+    coords_cols = [c for c in df.keys() if c in xk]
+    coords = df[coords_cols]
+    points = x.sel(coords.to_xarray(), **kwargs)
+    return points
 #######################################
\ No newline at end of file
-- 
GitLab


From e5e40f3178d2d75ec3185c1df1157c17341c5d6f Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 21:30:02 +0100
Subject: [PATCH 06/26] add drop_non_raster to message for stackstac.stack
 ValueError

---
 simplestac/utils.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 94d920a..87ec81e 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -90,7 +90,13 @@ class ExtendPystacClasses:
         # times = pd.to_datetime(
         with warnings.catch_warnings():
             warnings.filterwarnings("ignore", category=UserWarning)
-            arr = stackstac.stack(self, xy_coords=xy_coords, **kwargs)
+            try:
+                arr = stackstac.stack(self, xy_coords=xy_coords, **kwargs)
+            except ValueError as e:
+                if "Cannot automatically compute the resolution" in str(e):
+                    raise ValueError(str(e)+"\nOr drop non-raster assets from collection with ItemCollection.drop_non_raster()")
+                else:
+                    raise e
 
         if bbox is not None:
             arr = arr.rio.clip_box(*bbox)
-- 
GitLab


From 4f5143119c38a5ffd671cc45c75692461d6c2a1c Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 21:31:26 +0100
Subject: [PATCH 07/26] fix crs comparison

---
 simplestac/utils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 87ec81e..463a4c6 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -101,7 +101,7 @@ class ExtendPystacClasses:
         if bbox is not None:
             arr = arr.rio.clip_box(*bbox)
         if geometry is not None:
-            if hasattr(geometry, 'crs') and geometry.crs != arr.rio.crs:
+            if hasattr(geometry, 'crs') and not geometry.crs.equals(arr.rio.crs):
                 logger.debug(f"Reprojecting geometry from {geometry.crs} to {arr.rio.crs}")
                 geometry = geometry.to_crs(arr.rio.crs)
             arr = arr.rio.clip(geometry)
-- 
GitLab


From 7d21fcc0fe11883b9b2c9f41ddde3e3f8fe573d2 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 22:39:59 +0100
Subject: [PATCH 08/26] add method ItemCollection.extract_points

---
 simplestac/utils.py | 104 ++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 95 insertions(+), 9 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 463a4c6..e1dd81a 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -19,6 +19,7 @@ from tqdm import tqdm
 from typing import Union
 import warnings
 import datetime
+import geopandas as gpd
 
 from simplestac.local import stac_asset_info_from_raster, properties_from_assets
 
@@ -492,6 +493,77 @@ class ExtendPystacClasses:
         if not inplace:
             return x
 
+    def extract_points(self, points, method="nearest", tolerance="pixel", drop=False, **kwargs):
+        """Extract points from xarray
+
+        Parameters
+        ----------
+        x : xarray.DataArray or xarray.Dataset
+        points : geopandas.GeoDataFrame or pandas.DataFrame
+            Points or coordinates of the points
+        method, tolerance, drop : see xarray.DataArray.sel
+            Additional keyword arguments passed to xarray.DataArray.sel
+
+        Returns
+        -------
+        xarray.DataArray or xarray.Dataset
+            The points values with points index as coordinate.
+            The returned xarray can then be converted to
+            dataframe with `to_dataframe` or `to_dask_dataframe`.
+
+        Examples
+        --------
+        >>> import xarray as xr
+        >>> import pandas as pd
+        >>> import dask.array
+        >>> import numpy as np
+        >>> da = xr.DataArray(
+        ... # np.random.random((100,200)),
+        ... dask.array.random.random((100,200,10), chunks=10),
+        ... coords = [('x', np.arange(100)+.5), 
+        ...           ('y', np.arange(200)+.5),
+        ...           ('z', np.arange(10)+.5)]
+        ... ).rename("pixel_value")
+        >>> df = pd.DataFrame(
+        ...    dict(
+        ...        x=np.random.permutation(range(100))[:100]+np.random.random(100),
+        ...        y=np.random.permutation(range(100))[:100]+np.random.random(100),
+        ...        other=range(100),
+        ...    )
+        ... )
+        >>> df.index.rename("id_point", inplace=True)
+        >>> extraction = extract_points(da, df, method="nearest", tolerance=.5)
+        >>> ext_df = extraction.to_dataframe()
+        >>> ext_df.reset_index(drop=False, inplace=True)
+        >>> ext_df.rename({k: k+"_pixel" for k in da.dims}, axis=1, inplace=True)
+        >>> # join extraction to original dataframe
+        >>> df.merge(ext_df, on=["id_point"])
+        """ 
+        """_summary_
+
+        Parameters
+        ----------
+        points : _type_
+            _description_
+        tolerance : float or str, optional
+
+        method, tolerance, drop : see xarray.DataArray.sel
+            Additional keyword arguments passed to xarray.DataArray.sel
+            If tolerance is "pixel", it is set to half the resolution of the xarray.
+        Returns
+        -------
+        _type_
+            _description_
+        """
+        # avoid starting anything if not all points
+        if isinstance(points, (gpd.GeoDataFrame, gpd.GeoSeries)):
+            if not points.geom_type.isin(['Point', 'MultiPoint']).all():
+                raise ValueError("All geometries must be of type Point or MultiPoint")
+        
+        arr = self.to_xarray(**kwargs)#geometry=points)
+        if tolerance == "pixel":
+            tolerance = arr.rio.resolution()[0] / 2
+        return extract_points(arr, points, method=method, tolerance=tolerance, drop=drop)
 
 class ItemCollection(pystac.ItemCollection, ExtendPystacClasses):
     pass
@@ -884,20 +956,23 @@ def harmonize_sen2cor_offet(x, bands=S2_SEN2COR_BANDS, inplace=False):
     if inplace:
         return x
 
-def extract_points(x, df, **kwargs):
-    """_summary_
+def extract_points(x, points, method=None, tolerance=None, drop=False):
+    """Extract points from xarray
 
     Parameters
     ----------
     x : xarray.DataArray or xarray.Dataset
-    df : pandas.DataFrame
-        Coordinates of the points
+    points : geopandas.GeoDataFrame or pandas.DataFrame
+        Points or coordinates of the points
+    method, tolerance, drop : see xarray.DataArray.sel
+        Additional keyword arguments passed to xarray.DataArray.sel
 
     Returns
     -------
     xarray.DataArray or xarray.Dataset
-        The points values, tha can be converted to
-        dataframe with `to_dataframe` `to_dask_dataframe`
+        The points values with points index as coordinate.
+        The returned xarray can then be converted to
+        dataframe with `to_dataframe` or `to_dask_dataframe`.
 
     Examples
     --------
@@ -929,9 +1004,20 @@ def extract_points(x, df, **kwargs):
 
     """
     # x = da
+    valid_types = (gpd.GeoDataFrame, gpd.GeoSeries)
+    if isinstance(points, valid_types):
+        if not points.geom_type.isin(['Point', 'MultiPoint']).all():
+            raise ValueError("All geometries must be of type Point")
+
+    if isinstance(points, valid_types):
+        if hasattr(points, 'crs') and not points.crs.equals(x.rio.crs):
+            logger.debug(f"Reprojecting points from {points.crs} to {x.rio.crs}")
+            points = points.to_crs(x.rio.crs)
+        points = points.get_coordinates()
+
     xk = x.dims
-    coords_cols = [c for c in df.keys() if c in xk]
-    coords = df[coords_cols]
-    points = x.sel(coords.to_xarray(), **kwargs)
+    coords_cols = [c for c in points.keys() if c in xk]
+    coords = points[coords_cols]
+    points = x.sel(coords.to_xarray(), method=method, tolerance=tolerance, drop=drop)
     return points
 #######################################
\ No newline at end of file
-- 
GitLab


From 4821f2080e93b65fbfef50a1ec9fbfeeeb3de5b6 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 22:40:36 +0100
Subject: [PATCH 09/26] add tests

---
 tests/test_local.py  | 20 +++++++++++++++++---
 tests/test_remote.py | 33 ++++++++++++++++++++++++++++++++-
 2 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/tests/test_local.py b/tests/test_local.py
index 676448c..3471050 100644
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -2,6 +2,9 @@ from simplestac.local import collection_format, build_item_collection
 from simplestac.utils import write_raster, apply_formula
 import xarray as xr
 import pystac
+from shapely.geometry import MultiPoint
+import geopandas as gpd
+
 
 def test_formatting():
     fmt = collection_format()
@@ -86,7 +89,7 @@ def test_apply_items_raster_args(s2scene_dir, roi):
                 dtype="int16", 
                 scale_factor=0.001,
                 add_offset=0.0,
-                _FillValue= 2**15 - 1,
+                _FillValue= -2**15,
             ),
         )
     )
@@ -95,9 +98,20 @@ def test_apply_items_raster_args(s2scene_dir, roi):
     assert rb["datatype"] == "int16"
     assert rb["scale"] == 0.001
     assert rb["offset"] == 0.0
-    assert rb["nodata"] == 2**15 - 1
+    assert rb["nodata"] == -2**15
+
+def test_extract_points(s2scene_dir, roi):
+    col = build_item_collection(s2scene_dir, collection_format())
+    points = roi.geometry.apply(lambda x: MultiPoint(list(x.exterior.coords)))
+    points.index.rename("id_point", inplace=True)
+    ext = col.extract_points(points)
+    assert ext.id_point.isin(points.index.values).all()
+    coords = points.get_coordinates().reset_index(drop=True)
+    points = gpd.GeoSeries(gpd.points_from_xy(**coords, crs=roi.crs))
+    points.index.rename("id_point", inplace=True)
+    ext = col.extract_points(points)
+    assert ext.id_point.isin(points.index.values).all()
 
-    
 ############################################################
     
 
diff --git a/tests/test_remote.py b/tests/test_remote.py
index 3fced92..beb2b82 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -3,6 +3,21 @@ import planetary_computer as pc
 import pystac_client
 
 URL = "https://planetarycomputer.microsoft.com/api/stac/v1"
+
+def test_to_xarray(roi, s2scene_pc_dir):
+    time_range = "2022-01-01/2022-01-31"
+
+    catalog = pystac_client.Client.open(URL, modifier=pc.sign_inplace)
+    search = catalog.search(
+        collections=["sentinel-2-l2a"],
+        bbox=roi.to_crs(4326).total_bounds,
+        datetime=time_range,
+        sortby="datetime",
+    )
+    col = ItemCollection(search.item_collection())
+    x = col.drop_non_raster().to_xarray()
+    assert len(x.time) == len(col)
+
 def test_offset_harmonization(roi, s2scene_pc_dir):
     time_range = "2022-01-20/2022-01-31"
 
@@ -21,7 +36,23 @@ def test_offset_harmonization(roi, s2scene_pc_dir):
     assert of0 == 0
     assert ofN == -1000
 
+def test_drop_duplicates(roi, s2scene_pc_dir):
+    time_range = "2022-01-20/2022-01-31"
+    catalog = pystac_client.Client.open(URL, modifier=pc.sign_inplace)
+    search = catalog.search(
+        collections=["sentinel-2-l2a"],
+        bbox=roi.to_crs(4326).total_bounds,
+        datetime=time_range,
+        sortby="datetime",
+    )
+    col = search.item_collection()
+    col1 = ItemCollection(col.clone()+col.clone())
+    col1.drop_duplicates(inplace=True)
+    assert len(col1) == len(col)
+
 def test_write_assets(roi, s2scene_pc_dir):
+
+    s2scene_pc_dir.rmtree_p().mkdir_p()
     time_range = "2016-01-01/2016-01-31"
 
     catalog = pystac_client.Client.open(URL)
@@ -35,7 +66,6 @@ def test_write_assets(roi, s2scene_pc_dir):
     col = ItemCollection(search.item_collection()).drop_non_raster()
     bbox = roi.to_crs(col.to_xarray().rio.crs).total_bounds
     col = pc.sign(col)
-    s2scene_pc_dir.rmtree_p().mkdir_p()
     encoding=dict(
         dtype="int16", 
         scale_factor=0.001,
@@ -48,6 +78,7 @@ def test_write_assets(roi, s2scene_pc_dir):
     item = new_col[0]
     assert item.id == col[0].id
     assert len(item.assets) == len(s2scene_pc_dir.dirs()[0].files("*.tif"))
+    assert item.assets["B08"].href.startswith(s2scene_pc_dir)
     assert new_col[0].assets["B08"].extra_fields["raster:bands"][0]["scale"] == 0.001
     
 
-- 
GitLab


From 543760a9634b9aeb12fcfa9ff9e9330ee7b21a33 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 23:05:33 +0100
Subject: [PATCH 10/26] fix extract_points docstring

---
 simplestac/utils.py | 21 +++++----------------
 1 file changed, 5 insertions(+), 16 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index e1dd81a..b038582 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -503,7 +503,11 @@ class ExtendPystacClasses:
             Points or coordinates of the points
         method, tolerance, drop : see xarray.DataArray.sel
             Additional keyword arguments passed to xarray.DataArray.sel
-
+            If tolerance is "pixel", it is set to half the resolution
+            of the xarray, supposing it is a rioxarray.
+        **kwargs:
+            Additional keyword arguments passed to `ItemCollection.to_xarray()`
+            
         Returns
         -------
         xarray.DataArray or xarray.Dataset
@@ -539,22 +543,7 @@ class ExtendPystacClasses:
         >>> # join extraction to original dataframe
         >>> df.merge(ext_df, on=["id_point"])
         """ 
-        """_summary_
-
-        Parameters
-        ----------
-        points : _type_
-            _description_
-        tolerance : float or str, optional
 
-        method, tolerance, drop : see xarray.DataArray.sel
-            Additional keyword arguments passed to xarray.DataArray.sel
-            If tolerance is "pixel", it is set to half the resolution of the xarray.
-        Returns
-        -------
-        _type_
-            _description_
-        """
         # avoid starting anything if not all points
         if isinstance(points, (gpd.GeoDataFrame, gpd.GeoSeries)):
             if not points.geom_type.isin(['Point', 'MultiPoint']).all():
-- 
GitLab


From 9a5500b433d4c33dddbc4e8840d9b114f12c3958 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 23:25:43 +0100
Subject: [PATCH 11/26] try other CI rules

---
 .gitlab-ci.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5df5950..41bc8c8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,9 +16,9 @@ tests:
     - micromamba env list
     - pip install -e .
     - pytest -s
-  only:
-   - merge-request
-   - schedules
+  rules:
+   - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+   - if: $CI_COMMIT_BRANCH =~ /.*/
   
 pages:
   stage: pages
-- 
GitLab


From 4a4adea9f28ca74c2b0193065f5f1cbb654d898a Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Mon, 11 Mar 2024 23:42:24 +0100
Subject: [PATCH 12/26] update changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f2699d..6387fd9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 - function `harmonize_sen2cor_offset`: adds an `offset` property to the assets so it is taken into account by `to_xarray`.
 - method `ItemCollection.drop_duplicates`: drop duplicated ID returned by pgstac.
 - method `ItemCollection.drop_non_raster`: drop non raster assets.
+- function `extract_points` and method `ItemCollection.extract_points` to extract points time series.
 - `writer_args` to `ItemCollection.apply_...` methods and function in order to specify the outputs format, e.g. the encoding.
 - in local.py, `start_datetime` and `end_datetime` can now be used instead of `datetime` in the template used to build a local ItemCollection.
 - module `extents.py` to manipulate STAC extents.
-- 
GitLab


From 6aef6c38490b5b163ba17ef71104fbf3e905f429 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Tue, 12 Mar 2024 00:48:31 +0100
Subject: [PATCH 13/26] add version number

---
 simplestac/__init__.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/simplestac/__init__.py b/simplestac/__init__.py
index e69de29..e60be81 100644
--- a/simplestac/__init__.py
+++ b/simplestac/__init__.py
@@ -0,0 +1,7 @@
+from importlib.metadata import version, PackageNotFoundError
+
+try:
+    __version__ = version("simplestac")
+except PackageNotFoundError:
+    # package is not installed
+    pass
\ No newline at end of file
-- 
GitLab


From d540f1cddb8ef49302654543c66b069c1f896ea9 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Tue, 12 Mar 2024 00:52:31 +0100
Subject: [PATCH 14/26] update gitignore

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index 8d4c383..5c1a962 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ __pycache__
 tests/data*
 draft
 examples/data
+docs/examples
\ No newline at end of file
-- 
GitLab


From 1c4d2825520eacefdcd5a44e54e888568a113902 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Tue, 12 Mar 2024 01:38:07 +0100
Subject: [PATCH 15/26] add light mode in pages

---
 pyproject_doc.toml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/pyproject_doc.toml b/pyproject_doc.toml
index 1fa654e..6d948de 100644
--- a/pyproject_doc.toml
+++ b/pyproject_doc.toml
@@ -53,7 +53,14 @@ features=["navigation.top"]
     icon = "material/theme-light-dark"
     name = "Switch to light mode"
 
+  [[tool.portray.mkdocs.theme.palette]]
+  scheme = "default"
+  primary = "teal"
+  accent = "blue"
 
+    [tool.portray.mkdocs.theme.palette.toggle]  
+    icon = "material/theme-light-dark"
+    name = "Switch to dark mode"
 # favicon = "docs/icon/....png"
 # logo = "docs/icon/....png"
 
-- 
GitLab


From bec9ebba7712d5b9ed26c18b2b95323104ec70bc Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Tue, 12 Mar 2024 12:16:19 +0100
Subject: [PATCH 16/26] Update 3 files

- /simplestac/extents.py
- /pyproject.toml
- /tests/extents.py
---
 pyproject.toml        |  1 -
 simplestac/extents.py | 17 +++--------------
 tests/extents.py      | 20 ++++++++++++++++++++
 3 files changed, 23 insertions(+), 15 deletions(-)
 create mode 100644 tests/extents.py

diff --git a/pyproject.toml b/pyproject.toml
index 405ec2f..7dd4f3d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,7 +26,6 @@ dependencies = [
   "rioxarray",
   "stac_static@git+https://github.com/jsignell/stac-static",
   "stackstac",
-  "rtree",
 ]
 
 [project.optional-dependencies]
diff --git a/simplestac/extents.py b/simplestac/extents.py
index 379898d..f31094e 100644
--- a/simplestac/extents.py
+++ b/simplestac/extents.py
@@ -6,7 +6,6 @@ from dataclasses import dataclass
 from datetime import datetime
 from typing import Union
 from stacflow.common_types import Bbox
-from rtree import index
 
 
 @dataclass
@@ -65,21 +64,13 @@ def clusterize_bboxes(bboxes: list[Bbox]) -> list[Bbox]:
 
     """
     bboxes = [SmartBbox(bbox) for bbox in bboxes]
-    idx = index.Index()
-    for i, bbox in enumerate(bboxes):
-        idx.insert(id=i, coordinates=bbox.coords)
     clusters = [bboxes.pop()]
 
-    def _update_clusters_idx():
-        _clusters_idx = index.Index()
-        for _i, cluster_bbox in enumerate(clusters):
-            _clusters_idx.insert(id=_i, coordinates=cluster_bbox.coords)
-        return _clusters_idx
-
-    clusters_idx = _update_clusters_idx()
     while bboxes:
         bbox = bboxes.pop()
-        inter_clusters = list(clusters_idx.intersection(bbox.coords))
+        inter_clusters = [
+            i for i, cluster in enumerate(clusters) if bbox.touches(cluster)
+        ]
         if inter_clusters:
             # We merge all intersecting clusters into one
             clusters[inter_clusters[0]].update(bbox)
@@ -93,8 +84,6 @@ def clusterize_bboxes(bboxes: list[Bbox]) -> list[Bbox]:
         else:
             clusters.append(bbox)
 
-        clusters_idx = _update_clusters_idx()
-
     return [cluster.coords for cluster in clusters]
 
 
diff --git a/tests/extents.py b/tests/extents.py
new file mode 100644
index 0000000..5b52296
--- /dev/null
+++ b/tests/extents.py
@@ -0,0 +1,20 @@
+import pytest
+from simplestac.extents import AutoSpatialExtent
+
+def test_spatial_extent():
+    """
+    Test the `AutoSpatialExtent` class.
+
+    Two clusters of bboxes (i.e. lists of bboxes) composed respectively with 2 and 1 bbox are 
+    created (by definition, the clusters are disjoint: their bboxes don't overlap)
+    We instanciate an `AutoSpatialExtent` and we check that the two expected clusters are found.
+    """
+    # first cluster (e.g. "france mainland")
+    bbox1 = [4, 42, 6, 44]
+    bbox2 = [3, 41, 5, 43]
+
+    # second cluster (e.g. "corse")
+    bbox3 = [7, 42, 8, 50]
+
+    ase = AutoSpatialExtent(bboxes=[bbox1, bbox2, bbox3])
+    assert ase.bboxes == [[7, 42, 8, 50], [3, 41, 6, 44]]
-- 
GitLab


From 0e1a1948dae2a7b714a3f5eeb1e92a500cb08b59 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Tue, 12 Mar 2024 15:47:49 +0100
Subject: [PATCH 17/26] fix harmonize_sen2cor_offset function name

---
 simplestac/utils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index b038582..bc17843 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -902,7 +902,7 @@ def apply_formula(x, formula):
 
     return eval(formula)
 
-def harmonize_sen2cor_offet(x, bands=S2_SEN2COR_BANDS, inplace=False):
+def harmonize_sen2cor_offset(x, bands=S2_SEN2COR_BANDS, inplace=False):
     """
     Harmonize new Sentinel-2 item collection (Sen2Cor v4+, 2022-01-25)
     to the old baseline (v3-):
-- 
GitLab


From c5c2af828d8a328ab401fcb352e584f5cc3820e3 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Tue, 12 Mar 2024 16:34:48 +0100
Subject: [PATCH 18/26] Update file extents.py

---
 simplestac/extents.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/simplestac/extents.py b/simplestac/extents.py
index f31094e..cff0889 100644
--- a/simplestac/extents.py
+++ b/simplestac/extents.py
@@ -1,5 +1,15 @@
 """
 Deal with STAC Extents.
+
+The `AutoSpatialExtent` is an extension of the `pystac.SpatialExtent`,
+it allows to automatically compute clusters of bboxes from a list of bboxes.
+It can be instanciated with the same arguments as `pystac.SpatialExtent`.
+Bounding boxes lists will be automatically processed: all touching bboxes
+will be merged and updated.
+
+The `AutoTemporalExtent` is an extension of the `pystac.TemporalExtent`.
+It simply computes the date min and date max of a set of dates or dates ranges.
+
 """
 import pystac
 from dataclasses import dataclass
-- 
GitLab


From d5bef12665ae2b959d0f0011cb6abb358cf33eb8 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Tue, 12 Mar 2024 16:39:13 +0100
Subject: [PATCH 19/26] fix harmonize_sen2cor_offset

---
 simplestac/utils.py | 21 ++++++++++++++++++---
 1 file changed, 18 insertions(+), 3 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index bc17843..9ed7813 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -571,6 +571,7 @@ def write_assets(x: Union[ItemCollection, pystac.Item],
                  remove_item_props=DEFAULT_REMOVE_PROPS,
                  overwrite=False,
                  progress=True,
+                 writer_args=None,
                  **kwargs):
     """
     Writes item(s) assets to the specified output directory.
@@ -591,9 +592,16 @@ def write_assets(x: Union[ItemCollection, pystac.Item],
         If None, no properties are removed.
     overwrite : bool, optional
         Whether to overwrite existing files. Defaults to False.
+    writer_args : dict, optional
+        Arguments to pass to write_raster for each asset. Defaults to `None`.
+        See Notes for an example.
     **kwargs
         Additional keyword arguments passed to write_raster.
 
+    Returns
+    -------
+    ItemCollection
+        The item collection with the metadata updated with local asset paths.
     """    
     if isinstance(x, pystac.Item):
         x = [x]
@@ -602,16 +610,23 @@ def write_assets(x: Union[ItemCollection, pystac.Item],
     items = []
     for item in tqdm(x, disable=not progress):
         ic = ItemCollection([item], clone_items=True)
-        arr = ic.to_xarray(bbox=bbox, xy_coords=xy_coords).squeeze("time")
+        arr = ic.to_xarray(bbox=bbox, xy_coords=xy_coords, ).squeeze("time")
         item_dir = (output_dir / item.id).mkdir_p()
         for b in arr.band.values:
             filename = '_'.join([item.id, b+'.tif'])
             file = item_dir / f"{filename}"
+            
+            # Use specific writer args if available
+            if writer_args is not None and b in writer_args:
+                wa = writer_args[b]
+            else:
+                wa = kwargs
+            
             try:
                 if file.exists() and not overwrite:
                     logger.debug(f"File already exists, skipping asset: {file}")
                 else:
-                    write_raster(arr.sel(band=b), file, **kwargs)
+                    write_raster(arr.sel(band=b), file, **wa)
                 
                 # update stac asset info            
                 stac_info = stac_asset_info_from_raster(file)
@@ -934,7 +949,7 @@ def harmonize_sen2cor_offset(x, bands=S2_SEN2COR_BANDS, inplace=False):
     """
     
     if not inplace:
-        x = x.copy()
+        x = x.clone()
     for item in x:
         for asset in bands:
             if asset in item.assets:
-- 
GitLab


From 5df23b0816da0fb2078b46203fc71d9c4996254a Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Tue, 12 Mar 2024 16:45:25 +0100
Subject: [PATCH 20/26] fix tests

---
 tests/test_remote.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/test_remote.py b/tests/test_remote.py
index beb2b82..3333982 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -1,4 +1,4 @@
-from simplestac.utils import write_assets, ItemCollection, harmonize_sen2cor_offet
+from simplestac.utils import write_assets, ItemCollection, harmonize_sen2cor_offset
 import planetary_computer as pc
 import pystac_client
 
@@ -29,7 +29,7 @@ def test_offset_harmonization(roi, s2scene_pc_dir):
         sortby="datetime",
     )
     col = search.item_collection()
-    harmonize_sen2cor_offet(col, inplace=True)
+    harmonize_sen2cor_offset(col, inplace=True)
     of0 = col[0].assets["B02"].extra_fields["raster:bands"][0]["offset"]
     ofN = col[-1].assets["B02"].extra_fields["raster:bands"][0]["offset"]
 
-- 
GitLab


From 6f020803c1542b83e84279e10c5f5504c6d89239 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Tue, 12 Mar 2024 22:19:00 +0100
Subject: [PATCH 21/26] Update file extents.py

---
 simplestac/extents.py | 29 +++++++++++++++++++++++------
 1 file changed, 23 insertions(+), 6 deletions(-)

diff --git a/simplestac/extents.py b/simplestac/extents.py
index cff0889..069e409 100644
--- a/simplestac/extents.py
+++ b/simplestac/extents.py
@@ -1,14 +1,31 @@
 """
 Deal with STAC Extents.
 
-The `AutoSpatialExtent` is an extension of the `pystac.SpatialExtent`,
-it allows to automatically compute clusters of bboxes from a list of bboxes.
-It can be instanciated with the same arguments as `pystac.SpatialExtent`.
-Bounding boxes lists will be automatically processed: all touching bboxes
-will be merged and updated.
+# AutoSpatialExtent
+
+The vanilla `pystac.SpatialExtent` enables to describe a spatial extent 
+from several bounding boxes. While this is useful, sometimes we want to
+merge together bounding boxes that are partially overlapping. For instance,
+we can take the example of the France mainland, covered by multiple remote
+sensing products that are generally partially overlapping, and the Corse 
+island that is also covered by a number of RS products, but spatially 
+disjoint from the France mainland RS products bounding boxes. In this 
+particular exemple, we would like to regroup all RS products bounding 
+boxes so that there is one bbox for the France mainland, and another 
+bbox for the Corse island. This is particularly useful when a STAC 
+collection covers sparsely a broad area (e.g. worldwide), with several 
+isolated regions.
+
+The `AutoSpatialExtent` is an extension of the `pystac.SpatialExtent`.
+
+Instances are initialized with the same arguments as `pystac.SpatialExtent`.
+Internally, bounding boxes lists are processed at initialisation, so all 
+partially overlapping bounding boxes are merged and updated as a single one.
+
+# AutoTemporalExtent
 
 The `AutoTemporalExtent` is an extension of the `pystac.TemporalExtent`.
-It simply computes the date min and date max of a set of dates or dates ranges.
+It computes the date min and date max of a set of dates or dates ranges.
 
 """
 import pystac
-- 
GitLab


From dc6db14c1d6b9d92d6b8160cae5d9561e684a483 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Tue, 12 Mar 2024 23:13:05 +0100
Subject: [PATCH 22/26] Update file extents.py

---
 simplestac/extents.py | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)

diff --git a/simplestac/extents.py b/simplestac/extents.py
index 069e409..d49f1e1 100644
--- a/simplestac/extents.py
+++ b/simplestac/extents.py
@@ -32,7 +32,30 @@ import pystac
 from dataclasses import dataclass
 from datetime import datetime
 from typing import Union
-from stacflow.common_types import Bbox
+from numbers import Number
+from annotated_types import Predicate
+from typing_extensions import Annotated
+
+
+def is_bbox(l: List) -> bool:
+    """
+    Predicate to test is the input list represents a bounding box in WGS84.
+
+    Args:
+        l: a list of `Number`
+
+    Returns:
+
+    """
+    if len(l) == 4:
+        if all(isinstance(i, Number) for i in l):
+            if -180 <= l[0] and -90 <= l[1] and l[2] <= 180 and l[3] <= 90:
+                if l[0] <= l[2] and l[1] <= l[3]:
+                    return True
+    return False
+
+
+Bbox = Annotated[list, Predicate(is_bbox)]
 
 
 @dataclass
-- 
GitLab


From 602d2e01a0e67d6e17d07b195ae86fa6d70de805 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Wed, 13 Mar 2024 08:13:45 +0100
Subject: [PATCH 23/26] Update 2 files

- /tests/extents.py
- /tests/test_extents.py
---
 tests/{extents.py => test_extents.py} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename tests/{extents.py => test_extents.py} (100%)

diff --git a/tests/extents.py b/tests/test_extents.py
similarity index 100%
rename from tests/extents.py
rename to tests/test_extents.py
-- 
GitLab


From 210b1d4db827bfa7f0068e24f8539c46544d8180 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Wed, 13 Mar 2024 08:18:19 +0100
Subject: [PATCH 24/26] Update file extents.py

---
 simplestac/extents.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/simplestac/extents.py b/simplestac/extents.py
index d49f1e1..77e9c79 100644
--- a/simplestac/extents.py
+++ b/simplestac/extents.py
@@ -31,7 +31,7 @@ It computes the date min and date max of a set of dates or dates ranges.
 import pystac
 from dataclasses import dataclass
 from datetime import datetime
-from typing import Union
+from typing import Union, List
 from numbers import Number
 from annotated_types import Predicate
 from typing_extensions import Annotated
-- 
GitLab


From 8a918fd58c746f462b44b09a5f83e52abaac6ef7 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Wed, 13 Mar 2024 09:30:01 +0100
Subject: [PATCH 25/26] Update 2 files

- /tests/test_extents.py
- /simplestac/extents.py
---
 simplestac/extents.py | 17 ++++++++++-------
 tests/test_extents.py | 40 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 7 deletions(-)

diff --git a/simplestac/extents.py b/simplestac/extents.py
index 77e9c79..ef69713 100644
--- a/simplestac/extents.py
+++ b/simplestac/extents.py
@@ -209,12 +209,15 @@ class AutoTemporalExtent(pystac.TemporalExtent):
 
     def make_single_interval(self):
         all_dates = []
-        for interval in self.intervals:
-            if isinstance(interval, (list, tuple)):
-                all_dates += [i for i in interval if i is not None]
-            elif isinstance(interval, datetime):
-                all_dates.append(interval)
-            else:
-                TypeError(f"Unsupported date/range of: {interval}")
+        for dates_or_intervals in self.intervals:
+            # Because base class (`pystac.SpatialExtent`) converts everything 
+            # into [[...]]
+            for date_or_interval in dates_or_intervals:
+                if isinstance(date_or_interval, (list, tuple)):
+                    all_dates += [i for i in date_or_interval if i is not None]
+                elif isinstance(date_or_interval, datetime):
+                    all_dates.append(date_or_interval)
+                else:
+                    TypeError(f"Unsupported date/range of: {date_or_interval}")
         self.intervals = \
             [[min(all_dates), max(all_dates)]] if all_dates else [None, None]
diff --git a/tests/test_extents.py b/tests/test_extents.py
index 5b52296..db93b23 100644
--- a/tests/test_extents.py
+++ b/tests/test_extents.py
@@ -1,6 +1,8 @@
 import pytest
 from simplestac.extents import AutoSpatialExtent
 
+from datetime import datetime
+
 def test_spatial_extent():
     """
     Test the `AutoSpatialExtent` class.
@@ -8,6 +10,7 @@ def test_spatial_extent():
     Two clusters of bboxes (i.e. lists of bboxes) composed respectively with 2 and 1 bbox are 
     created (by definition, the clusters are disjoint: their bboxes don't overlap)
     We instanciate an `AutoSpatialExtent` and we check that the two expected clusters are found.
+
     """
     # first cluster (e.g. "france mainland")
     bbox1 = [4, 42, 6, 44]
@@ -18,3 +21,40 @@ def test_spatial_extent():
 
     ase = AutoSpatialExtent(bboxes=[bbox1, bbox2, bbox3])
     assert ase.bboxes == [[7, 42, 8, 50], [3, 41, 6, 44]]
+
+def test_temporal_extent():
+    """
+    Test the `AutoTemporalExtent`.
+
+    """
+    # dates only (as plain list)
+    dates = [
+        datetime(year=2020, month=1, day=1),
+        datetime(year=2022, month=1, day=1),
+        datetime(year=2023, month=1, day=1),
+    ]
+    auto_text = AutoTemporalExtent(dates)
+    assert auto_text.intervals == [[
+        datetime(year=2020, month=1, day=1),
+        datetime(year=2023, month=1, day=1)
+    ]]
+
+    # dates only (as nested list)
+    auto_text = AutoTemporalExtent([dates])
+    assert auto_text.intervals == [[
+        datetime(year=2020, month=1, day=1),
+        datetime(year=2023, month=1, day=1)
+    ]]
+
+    # mixed dates + ranges
+    auto_text = AutoTemporalExtent([
+        datetime(year=2020, month=1, day=1),
+        [None, None],
+        [datetime(year=2019, month=1, day=1), None],
+        [None, datetime(year=2024, month=1, day=1)],
+        datetime(year=2023, month=1, day=1)
+    ])
+    assert auto_text.intervals == [[
+        datetime(year=2019, month=1, day=1),
+        datetime(year=2024, month=1, day=1)
+    ]]
-- 
GitLab


From 75e084c989eacf25543a8b16b340d44244b4d7f7 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Wed, 13 Mar 2024 09:37:13 +0100
Subject: [PATCH 26/26] Update file test_extents.py

---
 tests/test_extents.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_extents.py b/tests/test_extents.py
index db93b23..f5d8fb2 100644
--- a/tests/test_extents.py
+++ b/tests/test_extents.py
@@ -1,5 +1,5 @@
 import pytest
-from simplestac.extents import AutoSpatialExtent
+from simplestac.extents import AutoSpatialExtent, AutoTemporalExtent
 
 from datetime import datetime
 
-- 
GitLab