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