From a80a0e5dc2b9eb875ec6a9fd5d9ae9c2d3d42786 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 09:23:05 +0100
Subject: [PATCH 01/27] ADD: logo

---
 doc/logo.png | Bin 0 -> 7532 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 doc/logo.png

diff --git a/doc/logo.png b/doc/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc854dbcfc60b8aa0f40a0f8df459c96e424716e
GIT binary patch
literal 7532
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
CR%L(y

literal 0
HcmV?d00001

-- 
GitLab


From cd600b49f9b8f7f498b65e79b5385be14ae4e700 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 10:16:27 +0100
Subject: [PATCH 02/27] REFAC: architecture

---
 scenes/core.py       | 152 +++++++++++++++++++++++++++++++++++++++++++
 scenes/indexation.py |   2 +-
 scenes/spot.py       | 143 +++++++---------------------------------
 scenes/utils.py      |  18 ++++-
 4 files changed, 194 insertions(+), 121 deletions(-)
 create mode 100644 scenes/core.py

diff --git a/scenes/core.py b/scenes/core.py
new file mode 100644
index 0000000..d2f3de3
--- /dev/null
+++ b/scenes/core.py
@@ -0,0 +1,152 @@
+"""
+This module provides the Source class, which aims to handle the access to the rasters delivered by EO products
+"""
+import pyotb
+from abc import ABC, abstractmethod
+from scenes import utils
+import datetime
+
+class Source(pyotb.Output):
+    """
+    Source class.
+    Holds common operations on image sources (e.g. drill, resample, extract an ROI, etc.)
+    """
+    def __init__(self, root_imagery, out, parent=None):
+        """
+        :param root_imagery: root Imagery instance
+        :param out: image to deliver (can be an image filename (str), a pyotb.App, etc.)
+        :param parent: parent Source instance
+        """
+        assert isinstance(root_imagery, Imagery)
+        self.root_imagery = root_imagery  # root imagery
+        # Here we call the pyotb.Output() constructor.
+        # Since it can only be called with pyotb apps, we do the following:
+        # - if the output is a str, (e.g. the original dimap filename), we instantiate a pyotb.Input(),
+        # - else we use the original output (should be pyotb application)
+        super().__init__(app=pyotb.Input(out) if isinstance(out, str) else out, output_parameter_key="out")
+        assert parent is not self, "You cannot assign a new source to its parent instance"
+        self.parent = parent  # parent source (is another Source instance)
+        self._app_stack = []  # list of otb applications or output to keep trace
+
+    def new_source(self, *args):
+        """
+        Return a new Source instance with new apps added at the end of the pipeline.
+        :param *args: list of pyotb.app instances to append to the existing pipeline
+        :return: new source
+        """
+        for new_app in args:
+            self._app_stack.append(new_app)
+        return Source(root_imagery=self.root_imagery, out=self._app_stack[-1], parent=self)
+
+    def drilled(self, msk_vec_file, inside=True, nodata=0):
+        """
+        Return the source drilled from the input vector data.
+        The default behavior is that the hole is made inside the polygon.
+        This can be changed setting the "inside" parameter to False.
+        :param msk_vec_file: input vector data filename
+        :param inside: whether the drill is happening inside the polygon or outside
+        :param nodata: nodata value inside holes
+        :return: drilled source
+        """
+        if utils.open_vector_layer(msk_vec_file):
+            # Cloud mask is not empty
+            rasterization = pyotb.Rasterization({"in": msk_vec_file,
+                                                 "im": self,
+                                                 "mode": "binary",
+                                                 "mode.binary.foreground": 0 if inside else 255,
+                                                 "background": 255 if inside else 255})
+            manage_nodata = pyotb.ManageNoData({"in": self,
+                                                "mode": "apply",
+                                                "mode.apply.mask": rasterization,
+                                                "mode.apply.ndval": nodata})
+            return self.new_source(rasterization, manage_nodata)
+        return self  # Nothing but a soft copy of the source
+
+    def resample_over(self, ref_img, interpolator="nn", nodata=0):
+        """
+        Return the source superimposed over the input image
+        :param ref_img: reference image
+        :param interpolator: interpolator
+        :param nodata: no data value
+        :return: resampled image source
+        """
+        return self.new_source(pyotb.Superimpose({"inm": self,
+                                                  "inr": ref_img,
+                                                  "interpolator": interpolator,
+                                                  "fv": nodata}))
+
+    def clip_over_img(self, ref_img):
+        """
+        Return the source clipped over the ROI specified by the input image extent
+        :param ref_img: reference image
+        :return: ROI clipped source
+        """
+        return self.new_source(pyotb.ExtractROI({"in": self,
+                                                 "mode": "fit",
+                                                 "mode.fit.im": ref_img}))
+
+    def clip_over_vec(self, ref_vec):
+        """
+        Return the source clipped over the ROI specified by the input vector extent
+        :param ref_vec: reference vector data
+        :return: ROI clipped source
+        """
+        return self.new_source(pyotb.ExtractROI({"in": self,
+                                                 "mode": "fit",
+                                                 "mode.fit.vec": ref_vec}))
+
+
+class Imagery:
+    """
+    Imagery class.
+    This class carry the base image source, and additional generic stuff common to all sensors imagery.
+    """
+
+    def __init__(self, root_scene):
+        """
+        :param root_scene: The Scene of which the Imagery instance is attached
+        """
+        self.root_scene = root_scene
+
+
+class Scene(ABC):
+    """
+    Scene class.
+    The class carries all the metadata from the scene, and can be used to retrieve its imagery.
+    The get_imagery() function is abstract and must be implemented in child classes.
+    """
+
+    def __init__(self, acquisition_date, bbox_wgs84, epsg):
+        """
+        Constructor
+        :param acquisition_date: Acquisition date
+        :param bbox_wgs84: Bounding box, in WGS84 coordinates reference system
+        :param epsg: EPSG code
+        """
+
+        assert isinstance(acquisition_date, datetime.datetime), "acquisition_date must be a datetime.datetime instance"
+        self.acquisition_date = acquisition_date
+        self.bbox_wgs84 = bbox_wgs84
+        assert isinstance(epsg, int), "epsg must be an int"
+        self.epsg = epsg
+
+    @abstractmethod
+    def get_imagery(self, **kwargs):
+        """
+        Must be implemented in child classes.
+        Return the imagery.
+        :param **kwargs: Imagery options
+        :return: Imagery instance
+        """
+        pass
+
+    def __repr__(self):
+        """
+        Enable one instance to be used with print()
+        """
+        out = {
+            "Acquisition date": self.acquisition_date,
+            "Bounding box (WGS84)": self.bbox_wgs84,
+            "EPSG": self.epsg,
+        }
+        return "\n".join(["{}: {}".format(key, value) for key, value in out.items()])
diff --git a/scenes/indexation.py b/scenes/indexation.py
index e3026a7..3de42ba 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -54,7 +54,7 @@ class Index:
         properties.dimension = 3
         self.index = rtree.index.Index(properties=properties)
         for scene_idx, scene in enumerate(scenes_list):
-            bbox = self.new_bbox(bbox_wgs84=scene.bbox, dt=scene.acquisition_date)
+            bbox = self.new_bbox(bbox_wgs84=scene.bbox_wgs84, dt=scene.acquisition_date)
             self.index.insert(scene_idx, bbox)
 
     def find_indices(self, bbox_wgs84, dt_min=None, dt_max=None):
diff --git a/scenes/spot.py b/scenes/spot.py
index 1a39146..921fa55 100644
--- a/scenes/spot.py
+++ b/scenes/spot.py
@@ -3,67 +3,15 @@ Spot 6/7 root_scene class
 """
 import datetime
 import xml.etree.ElementTree as ET
-import gdal
-from scenes import utils
 import pyotb
+from scenes import utils
+from scenes.core import Source, Imagery, Scene
 
 
-class Source(pyotb.Output):
+class Spot67Source(Source):
     """
-    Spot 6/7 source class.
-    Holds common operations on image sources (e.g. drill, resample, extract an ROI, etc.)
+    Spot 6/7 source class
     """
-
-    def __init__(self, root_imagery, out, parent=None):
-        """
-        :param root_imagery: root Imagery instance
-        :param out: image to deliver (can be an image filename (str), a pyotb.App, etc.)
-        :param parent: parent Source instance
-        """
-        self.root_imagery = root_imagery  # root imagery
-        # Here we call the pyotb.Output() constructor.
-        # Since it can only be called with pyotb apps, we do the following:
-        # - if the output is a str, (e.g. the original dimap filename), we instantiate a pyotb.Input(),
-        # - else we use the original output (should be pyotb application)
-        super().__init__(app=pyotb.Input(out) if isinstance(out, str) else out, output_parameter_key="out")
-        assert parent is not self, "You cannot assign a new source to its parent instance"
-        self.parent = parent  # parent source (is another Source instance)
-        self._app_stack = []  # list of otb applications or output to keep trace
-
-    def new_source(self, *args):
-        """
-        Return a new Source instance with new apps added at the end of the pipeline.
-        :param *args: list of pyotb.app instances to append to the existing pipeline
-        :return: new source
-        """
-        for new_app in args:
-            self._app_stack.append(new_app)
-        return Source(root_imagery=self.root_imagery, out=self._app_stack[-1], parent=self)
-
-    def drilled(self, msk_vec_file, inside=True, nodata=0):
-        """
-        Return the source drilled from the input vector data.
-        The default behavior is that the hole is made inside the polygon.
-        This can be changed setting the "inside" parameter to False.
-        :param msk_vec_file: input vector data filename
-        :param inside: whether the drill is happening inside the polygon or outside
-        :param nodata: nodata value inside holes
-        :return: drilled source
-        """
-        if utils.open_vector_layer(msk_vec_file):
-            # Cloud mask is not empty
-            rasterization = pyotb.Rasterization({"in": msk_vec_file,
-                                                 "im": self,
-                                                 "mode": "binary",
-                                                 "mode.binary.foreground": 0 if inside else 255,
-                                                 "background": 255 if inside else 255})
-            manage_nodata = pyotb.ManageNoData({"in": self,
-                                                "mode": "apply",
-                                                "mode.apply.mask": rasterization,
-                                                "mode.apply.ndval": nodata})
-            return self.new_source(rasterization, manage_nodata)
-        return self  # Nothing but a soft copy of the source
-
     def cld_msk_drilled(self, nodata=0):
         """
         Return the source drilled from the cloud mask
@@ -72,53 +20,19 @@ class Source(pyotb.Output):
         """
         return self.drilled(msk_vec_file=self.root_imagery.root_scene.cld_msk_file, nodata=nodata)
 
-    def resample_over(self, ref_img, interpolator="nn", nodata=0):
-        """
-        Return the source superimposed over the input image
-        :param ref_img: reference image
-        :param interpolator: interpolator
-        :param nodata: no data value
-        :return: resampled image source
-        """
-        return self.new_source(pyotb.Superimpose({"inm": self,
-                                                  "inr": ref_img,
-                                                  "interpolator": interpolator,
-                                                  "fv": nodata}))
-
-    def clip_over_img(self, ref_img):
-        """
-        Return the source clipped over the ROI specified by the input image extent
-        :param ref_img: reference image
-        :return: ROI clipped source
-        """
-        return self.new_source(pyotb.ExtractROI({"in": self,
-                                                 "mode": "fit",
-                                                 "mode.fit.im": ref_img}))
-
-    def clip_over_vec(self, ref_vec):
-        """
-        Return the source clipped over the ROI specified by the input vector extent
-        :param ref_vec: reference vector data
-        :return: ROI clipped source
-        """
-        return self.new_source(pyotb.ExtractROI({"in": self,
-                                                 "mode": "fit",
-                                                 "mode.fit.vec": ref_vec}))
-
 
-class Imagery:
+class Spot67Imagery(Imagery):
     """
     Spot 6/7 imagery class.
     This class carry the base image source, which can be radiometrically or geometrically corrected.
     """
-
-    def __init__(self, scene, epsg=None, reflectance=None):
+    def __init__(self, root_scene, epsg=None, reflectance=None):
         """
-        :param scene: The Scene of which the Imagery instance is attached
+        :param root_scene: The Scene of which the Imagery instance is attached
         :param epsg: optional option to project PAN and MS images
         :param reflectance: optional level of reflectance
         """
-        self.root_scene = scene
+        super().__init__(root_scene=root_scene)
 
         # Base
         self.xs = self.root_scene.dimap_file_xs
@@ -169,7 +83,7 @@ class Imagery:
                                 inside=False)  # new source masked outside the original ROI
 
 
-class Scene:
+class Spot67Scene(Scene):
     """
     Spot 6/7 root_scene class.
     The class carries all the metadata from the root_scene, and can be used to retrieve its imagery.
@@ -228,7 +142,7 @@ class Scene:
         c_nodes = root.find("Geometric_Data/Use_Area/Located_Geometric_Values")
         for node in c_nodes:
             if node.tag == "TIME":
-                self.acquisition_date = datetime.datetime.strptime(node.text[0:10], "%Y-%m-%d")
+                acquisition_date = datetime.datetime.strptime(node.text[0:10], "%Y-%m-%d")
                 break
 
         # Sun angles
@@ -240,21 +154,14 @@ class Scene:
                 self.sun_elevation = float(node.text)
 
         # Get EPSG and bounding box
-        def _get_epsg_bbox(dimap_file):
-            gdal_ds = gdal.Open(dimap_file)
-            epsg = utils.get_epsg(gdal_ds)
-            bbox_wgs84 = utils.get_bbox_wgs84(gdal_ds)
-            return epsg, bbox_wgs84
-
-        epsg_xs, self.bbox_xs = _get_epsg_bbox(dimap_file_xs)
-        epsg_pan, self.bbox_pan = _get_epsg_bbox(dimap_file_pan)
+        epsg_xs, self.bbox_wgs84_xs = utils.get_epsg_bbox(dimap_file_xs)
+        epsg_pan, self.bbox_wgs84_pan = utils.get_epsg_bbox(dimap_file_pan)
 
         # Check that EPSG for PAN and XS are the same
         if epsg_pan != epsg_xs:
             raise ValueError("EPSG of XS and PAN sources are different: "
                              "XS EPSG is {}, PAN EPSG is {}"
                              .format(epsg_xs, epsg_pan))
-        self.epsg = int(epsg_xs)
 
         # Here we compute bounding boxes overlap, to choose the most appropriated
         # CLD and ROI masks for the scene. Indeed, sometimes products are not
@@ -262,8 +169,8 @@ class Scene:
         # so CLD and ROI masks are not the same for XS and PAN.
         # We keep the ROI+CLD masks of the PAN or XS imagery lying completely inside
         # the other one.
-        self.bbox_xs_overlap = utils.bbox_overlap(self.bbox_xs, self.bbox_pan)
-        self.bbox_pan_overlap = utils.bbox_overlap(self.bbox_pan, self.bbox_xs)
+        self.bbox_xs_overlap = utils.bbox_overlap(self.bbox_wgs84_xs, self.bbox_wgs84_pan)
+        self.bbox_pan_overlap = utils.bbox_overlap(self.bbox_wgs84_pan, self.bbox_wgs84_xs)
 
         # Get ROI+CLD filenames in XS and PAN products
         self.cld_msk_file_xs = _get_mask(dimap_file_xs, "CLD*.GML")
@@ -272,25 +179,28 @@ class Scene:
         self.roi_msk_file_pan = _get_mask(dimap_file_pan, "ROI*.GML")
 
         # Choice based on the pxs overlap
-        self.bbox = self.bbox_xs
+        bbox_wgs84 = self.bbox_wgs84_xs
         self.cld_msk_file = self.cld_msk_file_xs
         self.roi_msk_file = self.roi_msk_file_xs
         self.pxs_overlap = self.bbox_xs_overlap
         if self.bbox_pan_overlap > self.bbox_xs_overlap:
-            self.bbox = self.bbox_pan
+            bbox_wgs84 = self.bbox_wgs84_pan
             self.cld_msk_file = self.cld_msk_file_pan
             self.roi_msk_file = self.roi_msk_file_pan
             self.pxs_overlap = self.bbox_pan_overlap
 
         # Throw some warning or error, depending on the pxs overlap value
         msg = "Bounding boxes of XS and PAN sources have {:.2f}% overlap. " \
-              "\n\tXS bbox: {} \n\tPAN bbox: {}" \
-              "".format(100 * self.pxs_overlap, self.bbox_xs, self.bbox_pan)
+              "\n\tXS bbox_wgs84: {} \n\tPAN bbox_wgs84: {}" \
+              "".format(100 * self.pxs_overlap, self.bbox_wgs84_xs, self.bbox_wgs84_pan)
         if self.pxs_overlap == 0:
             raise ValueError(msg)
         if self.has_partial_pxs_overlap():
             raise Warning(msg)
 
+        # Call parent constructor
+        super().__init(acquisition_date=acquisition_date, bbox_wgs84=bbox_wgs84, epsg=epsg_xs)
+
     def has_partial_pxs_overlap(self):
         """
         :return: True if at least PAN or XS imagery lies completely inside the other one. False else.
@@ -304,7 +214,7 @@ class Scene:
         :param reflectance: optional level of reflectance
         :return: Imagery instance
         """
-        return Imagery(self, epsg=epsg, reflectance=reflectance)
+        return Spot67Imagery(self, epsg=epsg, reflectance=reflectance)
 
     def __repr__(self):
         """
@@ -324,12 +234,9 @@ class Scene:
             "Viewing angle along track": self.viewing_angle_along,
             "Viewing angle": self.viewing_angle,
             "Incidence angle": self.incidence_angle,
-            "Acquisition date": self.acquisition_date,
             "Sun elevation": self.sun_elevation,
             "Sun azimuth": self.sun_azimuth,
-            "EPSG": self.epsg,
-            "Bounding box (WGS84)": self.bbox,
-            "Bounding box XS (WGS84)": self.bbox_xs,
-            "Bounding box PAN (WGS84)": self.bbox_pan
+            "Bounding box XS (WGS84)": self.bbox_wgs84_xs,
+            "Bounding box PAN (WGS84)": self.bbox_wgs84_pan
         }
-        return "\n".join(["{}: {}".format(key, value) for key, value in out.items()])
+        return super().__repr__() + "\n".join(["{}: {}".format(key, value) for key, value in out.items()])
diff --git a/scenes/utils.py b/scenes/utils.py
index 2aad643..ec69727 100644
--- a/scenes/utils.py
+++ b/scenes/utils.py
@@ -25,7 +25,9 @@ def get_epsg(gdal_ds):
     :return: EPSG code (int)
     """
     proj = osr.SpatialReference(wkt=gdal_ds.GetProjection())
-    return proj.GetAttrValue('AUTHORITY', 1)
+    epsg = proj.GetAttrValue('AUTHORITY', 1)
+    assert isinstance(epsg, int)
+    return epsg
 
 
 def get_extent(gdal_ds):
@@ -72,6 +74,18 @@ def get_bbox_wgs84(gdal_ds):
     return reproject_coords(coords, src_srs, tgt_srs)
 
 
+def get_epsg_bbox(filename):
+    """
+    Returns (epsg, bbox_wgs84) from a raster file that GDAL can open.
+    :param filename: file name
+    :return: (epsg, bbox_wgs84)
+    """
+    gdal_ds = gdal.Open(filename)
+    epsg = get_epsg(gdal_ds)
+    bbox_wgs84 = get_bbox_wgs84(gdal_ds)
+    return epsg, bbox_wgs84
+
+
 def coords2poly(coords):
     """
     Converts a list of coordinates into a polygon
@@ -122,7 +136,7 @@ def bbox_overlap(bbox, other_bbox):
     Returns the ratio of bounding boxes overlap.
     :param bbox: bounding box
     :param other_bbox: other bounding box
-    :return: overlap (in the [0, 1] range). 0 -> no overlap with other_bbox, 1 -> bbox lies inside other_bbox
+    :return: overlap (in the [0, 1] range). 0 -> no overlap with other_bbox, 1 -> bbox_wgs84 lies inside other_bbox
     """
     poly = coords2poly(bbox)
     other_poly = coords2poly(other_bbox)
-- 
GitLab


From cf2537f848e51b9ab4a53754249b6a75b84962d1 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 10:33:23 +0100
Subject: [PATCH 03/27] DOC: Update arch.md

---
 doc/arch.md | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/doc/arch.md b/doc/arch.md
index 345f1cc..f4e0383 100644
--- a/doc/arch.md
+++ b/doc/arch.md
@@ -49,6 +49,8 @@ superimposed.write("superimposed.tif")
 ```mermaid
 classDiagram
 
+    Scene <|-- Spot67Scene
+    Imagery <|-- Spot67Imagery
     Scene --*  Imagery: root_scene
     Imagery --*  Source: root_imagery
 
@@ -56,14 +58,18 @@ classDiagram
         +datetime acquisition_date
         +int epsg
         +bbox_wgs84
-        +bbox
+        +Imagery get_imagery()
+        +__repr__()
+    }
+
+    class Spot67Scene{
         +float azimuth_angle
         +float azimuth_angle_across
         +float azimuth_angle_along
         +float incidence_angle
         +float sun_azimuth_angle
         +float sun_elev_angle
-        +Imagery get_imagery()
+        +Spot67Imagery get_imagery()
         +__repr__()
     }
 
@@ -80,11 +86,15 @@ classDiagram
     }
 
     class Imagery{
-        +__init__(scene, epsg=None, reflectance=None)
+        +__init__(root_scene)
+        +Scene root_scene
+    }
+
+    class Spot67Imagery{
+        +__init__(root_scene, epsg=None, reflectance=None)
         +Source get_pxs()
         +Source get_pan()
         +Source get_xs()
-        +Scene root_scene
     }
 
 ```
-- 
GitLab


From 68d36e1290151ce3470051d5ebccf2efb64a81da Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 10:40:08 +0100
Subject: [PATCH 04/27] REFAC: architecture

---
 scenes/core.py  | 6 +++---
 scenes/spot.py  | 2 +-
 scenes/utils.py | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/scenes/core.py b/scenes/core.py
index d2f3de3..94f94f3 100644
--- a/scenes/core.py
+++ b/scenes/core.py
@@ -1,10 +1,11 @@
 """
 This module provides the Source class, which aims to handle the access to the rasters delivered by EO products
 """
-import pyotb
 from abc import ABC, abstractmethod
-from scenes import utils
 import datetime
+import pyotb
+from scenes import utils
+
 
 class Source(pyotb.Output):
     """
@@ -138,7 +139,6 @@ class Scene(ABC):
         :param **kwargs: Imagery options
         :return: Imagery instance
         """
-        pass
 
     def __repr__(self):
         """
diff --git a/scenes/spot.py b/scenes/spot.py
index 921fa55..d911681 100644
--- a/scenes/spot.py
+++ b/scenes/spot.py
@@ -199,7 +199,7 @@ class Spot67Scene(Scene):
             raise Warning(msg)
 
         # Call parent constructor
-        super().__init(acquisition_date=acquisition_date, bbox_wgs84=bbox_wgs84, epsg=epsg_xs)
+        super().__init__(acquisition_date=acquisition_date, bbox_wgs84=bbox_wgs84, epsg=epsg_xs)
 
     def has_partial_pxs_overlap(self):
         """
diff --git a/scenes/utils.py b/scenes/utils.py
index ec69727..d4495a8 100644
--- a/scenes/utils.py
+++ b/scenes/utils.py
@@ -4,7 +4,7 @@ A set of helpers for generic purpose
 import os
 import glob
 import pathlib
-from osgeo import osr, ogr
+from osgeo import osr, ogr, gdal
 
 
 def epsg2srs(epsg):
-- 
GitLab


From a6962a0a37102b31001e6795057187d41f9f8cc4 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 10:42:23 +0100
Subject: [PATCH 05/27] REFAC: architecture

---
 scenes/drs.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scenes/drs.py b/scenes/drs.py
index 2f61d61..96fe9dd 100644
--- a/scenes/drs.py
+++ b/scenes/drs.py
@@ -41,7 +41,7 @@ def get_spot67_scenes(root_dir):
                 raise ValueError("{} DIMAPS candidates found in {} ".format(nb_files, pan_path))
             dimap_file_pan = dimap_pan_files[0]
             # Instantiate a new scene object
-            new_scene = spot.Scene(dimap_file_pan=dimap_file_pan, dimap_file_xs=dimap_file_xs)
+            new_scene = spot.Spot67Scene(dimap_file_pan=dimap_file_pan, dimap_file_xs=dimap_file_xs)
             scenes.append(new_scene)
         except Exception as error:
             if dimap_file_xs not in errors:
-- 
GitLab


From 8aec7a3f9a2fec22193936047a6d346ed6a18481 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 10:45:25 +0100
Subject: [PATCH 06/27] REFAC: architecture

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

diff --git a/test/imagery_test.py b/test/imagery_test.py
index de8bd9b..f90daae 100644
--- a/test/imagery_test.py
+++ b/test/imagery_test.py
@@ -15,8 +15,8 @@ class ImageryTest(ScenesTestBase):
                              "DIM_SPOT6_P_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML")
 
     def get_scene1(self):
-        return spot.Scene(dimap_file_xs=self.get_dimap_xs1(),
-                          dimap_file_pan=self.get_dimap_pan1())
+        return spot.Spot67Scene(dimap_file_xs=self.get_dimap_xs1(),
+                                dimap_file_pan=self.get_dimap_pan1())
 
     def get_scene1_imagery(self):
         scene1 = self.get_scene1()
-- 
GitLab


From fa8209f403680f9ba4fc35f8ae745a7560dfbb51 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 10:48:16 +0100
Subject: [PATCH 07/27] REFAC: architecture

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

diff --git a/scenes/utils.py b/scenes/utils.py
index d4495a8..9b14974 100644
--- a/scenes/utils.py
+++ b/scenes/utils.py
@@ -26,8 +26,8 @@ def get_epsg(gdal_ds):
     """
     proj = osr.SpatialReference(wkt=gdal_ds.GetProjection())
     epsg = proj.GetAttrValue('AUTHORITY', 1)
-    assert isinstance(epsg, int)
-    return epsg
+    assert str(epsg).isdigit()
+    return int(epsg)
 
 
 def get_extent(gdal_ds):
-- 
GitLab


From af3dce654990dae94aed64001e178d89937c3948 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 10:50:07 +0100
Subject: [PATCH 08/27] REFAC: architecture

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

diff --git a/scenes/spot.py b/scenes/spot.py
index d911681..f6af8a9 100644
--- a/scenes/spot.py
+++ b/scenes/spot.py
@@ -207,9 +207,9 @@ class Spot67Scene(Scene):
         """
         return self.pxs_overlap < self.PXS_OVERLAP_THRESH
 
-    def get_imagery(self, epsg=None, reflectance=None):
+    def get_imagery(self, epsg=None, reflectance=None):  # pylint: disable=arguments-differ
         """
-        Return the imagery
+        Return the Spot 6/7 imagery
         :param epsg: optional option to project PAN and MS images
         :param reflectance: optional level of reflectance
         :return: Imagery instance
-- 
GitLab


From f9a6086c6b3fd3bc0281c9f129982d3d95f3f453 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 11:10:44 +0100
Subject: [PATCH 09/27] ADD: pickle serialization

---
 scenes/drs.py      | 25 +++++++++++++++++++++++--
 test/drs_import.py |  6 +++++-
 2 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/scenes/drs.py b/scenes/drs.py
index 96fe9dd..7745faf 100644
--- a/scenes/drs.py
+++ b/scenes/drs.py
@@ -2,6 +2,7 @@
 A set of utils to deal with DRS products
 """
 import tqdm
+import pickle
 from scenes import spot, utils
 
 
@@ -17,8 +18,8 @@ def find_all_dimaps(pth):
 def get_spot67_scenes(root_dir):
     """
     Return the list of pairs of PAN/XS DIMAPS
-    :param root_dir: directory
-    :return: list of pairs of filenames
+    :param root_dir: directory containing "MS" and "PAN" subdirectories
+    :return: list of Spot67Scenes instances
     """
     # List files
     look_dir = root_dir + "/MS"
@@ -59,3 +60,23 @@ def get_spot67_scenes(root_dir):
                 print("\t{}".format(error))
 
     return scenes
+
+
+def save_scenes(scenes_list, pickle_file):
+    """
+    Use pickle to save scenes
+    :param scenes_list: a list of Scene instances
+    :param pickle_file: pickle file
+    """
+    pickle.dump(scenes_list, open(pickle_file, "wb"))
+
+
+def load_scenes(pickle_file):
+    """
+    Use pickle to save Spot-6/7 scenes
+    :param pickle_file: pickle file
+    :return: list of Scene instances
+    """
+    return pickle.load(open(pickle_file, "rb"))
+
+
diff --git a/test/drs_import.py b/test/drs_import.py
index 1cfad67..9aec815 100644
--- a/test/drs_import.py
+++ b/test/drs_import.py
@@ -4,7 +4,11 @@ from scenes import drs
 # Arguments
 parser = argparse.ArgumentParser(description="Test",)
 parser.add_argument("--root_dir", help="Root directory containing MS and PAN folders", required=True)
+parser.add_argument("--out_pickle", help="Output pickle file", required=True)
 params = parser.parse_args()
 
-# Find pairs of DIMAPS
+# Get all scenes in the root_dir
 scenes = drs.get_spot67_scenes(params.root_dir)
+
+# Save scenes in a pickle file
+drs.save_scenes(scenes, params.out_pickle)
-- 
GitLab


From f9cf3f331f66422c440c4284a0bce87905a10ae0 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 11:14:36 +0100
Subject: [PATCH 10/27] ADD: pickle serialization

---
 test/drs_import.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/test/drs_import.py b/test/drs_import.py
index 9aec815..2be81e5 100644
--- a/test/drs_import.py
+++ b/test/drs_import.py
@@ -3,12 +3,14 @@ from scenes import drs
 
 # Arguments
 parser = argparse.ArgumentParser(description="Test",)
-parser.add_argument("--root_dir", help="Root directory containing MS and PAN folders", required=True)
+parser.add_argument("--root_dir", nargs='+', help="Root directories containing MS and PAN folders", required=True)
 parser.add_argument("--out_pickle", help="Output pickle file", required=True)
 params = parser.parse_args()
 
 # Get all scenes in the root_dir
-scenes = drs.get_spot67_scenes(params.root_dir)
+scenes = []
+for root_dir in params.root_dir:
+    scenes += drs.get_spot67_scenes(root_dir)
 
 # Save scenes in a pickle file
 drs.save_scenes(scenes, params.out_pickle)
-- 
GitLab


From 25cf5cb9d2348ae7caa6922d22c4f968ce0ef761 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 11:54:30 +0100
Subject: [PATCH 11/27] ADD: pickle serialization

---
 scenes/spot.py     | 14 +++++++-------
 scenes/utils.py    | 37 ++++++++++++++++++++++++-------------
 test/drs_search.py |  2 +-
 test/drs_stack.py  |  2 +-
 4 files changed, 33 insertions(+), 22 deletions(-)

diff --git a/scenes/spot.py b/scenes/spot.py
index f6af8a9..be13d46 100644
--- a/scenes/spot.py
+++ b/scenes/spot.py
@@ -154,8 +154,8 @@ class Spot67Scene(Scene):
                 self.sun_elevation = float(node.text)
 
         # Get EPSG and bounding box
-        epsg_xs, self.bbox_wgs84_xs = utils.get_epsg_bbox(dimap_file_xs)
-        epsg_pan, self.bbox_wgs84_pan = utils.get_epsg_bbox(dimap_file_pan)
+        epsg_xs, extent_wgs84_xs, self.bbox_wgs84_xs = utils.get_epsg_extent_bbox(dimap_file_xs)
+        epsg_pan, extent_wgs84_pan, self.bbox_wgs84_pan = utils.get_epsg_extent_bbox(dimap_file_pan)
 
         # Check that EPSG for PAN and XS are the same
         if epsg_pan != epsg_xs:
@@ -169,8 +169,8 @@ class Spot67Scene(Scene):
         # so CLD and ROI masks are not the same for XS and PAN.
         # We keep the ROI+CLD masks of the PAN or XS imagery lying completely inside
         # the other one.
-        self.bbox_xs_overlap = utils.bbox_overlap(self.bbox_wgs84_xs, self.bbox_wgs84_pan)
-        self.bbox_pan_overlap = utils.bbox_overlap(self.bbox_wgs84_pan, self.bbox_wgs84_xs)
+        self.xs_overlap = utils.extent_overlap(extent_wgs84_xs, extent_wgs84_pan)
+        self.pan_overlap = utils.extent_overlap(extent_wgs84_pan, extent_wgs84_xs)
 
         # Get ROI+CLD filenames in XS and PAN products
         self.cld_msk_file_xs = _get_mask(dimap_file_xs, "CLD*.GML")
@@ -182,12 +182,12 @@ class Spot67Scene(Scene):
         bbox_wgs84 = self.bbox_wgs84_xs
         self.cld_msk_file = self.cld_msk_file_xs
         self.roi_msk_file = self.roi_msk_file_xs
-        self.pxs_overlap = self.bbox_xs_overlap
-        if self.bbox_pan_overlap > self.bbox_xs_overlap:
+        self.pxs_overlap = self.xs_overlap
+        if self.pan_overlap > self.xs_overlap:
             bbox_wgs84 = self.bbox_wgs84_pan
             self.cld_msk_file = self.cld_msk_file_pan
             self.roi_msk_file = self.roi_msk_file_pan
-            self.pxs_overlap = self.bbox_pan_overlap
+            self.pxs_overlap = self.pan_overlap
 
         # Throw some warning or error, depending on the pxs overlap value
         msg = "Bounding boxes of XS and PAN sources have {:.2f}% overlap. " \
diff --git a/scenes/utils.py b/scenes/utils.py
index 9b14974..10f34eb 100644
--- a/scenes/utils.py
+++ b/scenes/utils.py
@@ -60,7 +60,7 @@ def reproject_coords(coords, src_srs, tgt_srs):
     return trans_coords
 
 
-def get_bbox_wgs84(gdal_ds):
+def get_extent_wgs84(gdal_ds):
     """
     Returns the bounding box in WGS84 CRS from a GDAL dataset
     :param gdal_ds: GDAL dataset
@@ -74,16 +74,27 @@ def get_bbox_wgs84(gdal_ds):
     return reproject_coords(coords, src_srs, tgt_srs)
 
 
-def get_epsg_bbox(filename):
+def get_bbox_from_extent(extent):
     """
-    Returns (epsg, bbox_wgs84) from a raster file that GDAL can open.
+    Converts an extent into a bounding box
+    :param extent: extent
+    :return: bounding box (xmin, xmax, ymin, ymax)
+    """
+    (xmin, ymax), (xmax, _), (_, ymin), (_, _) = extent
+    return xmin, xmax, ymin, ymax
+
+
+def get_epsg_extent_bbox(filename):
+    """
+    Returns (epsg, extent_wgs84) from a raster file that GDAL can open.
     :param filename: file name
-    :return: (epsg, bbox_wgs84)
+    :return: (epsg, extent_wgs84)
     """
     gdal_ds = gdal.Open(filename)
     epsg = get_epsg(gdal_ds)
-    bbox_wgs84 = get_bbox_wgs84(gdal_ds)
-    return epsg, bbox_wgs84
+    extent_wgs84 = get_extent_wgs84(gdal_ds)
+    bbox_wgs84 = get_bbox_from_extent(extent_wgs84)
+    return epsg, extent_wgs84, bbox_wgs84
 
 
 def coords2poly(coords):
@@ -131,15 +142,15 @@ def poly_overlap(poly, other_poly):
     return inter.GetArea() / poly.GetArea()
 
 
-def bbox_overlap(bbox, other_bbox):
+def extent_overlap(extent, other_extent):
     """
-    Returns the ratio of bounding boxes overlap.
-    :param bbox: bounding box
-    :param other_bbox: other bounding box
-    :return: overlap (in the [0, 1] range). 0 -> no overlap with other_bbox, 1 -> bbox_wgs84 lies inside other_bbox
+    Returns the ratio of extents overlap.
+    :param extent: extent
+    :param other_extent: other extent
+    :return: overlap (in the [0, 1] range). 0 -> no overlap with other_extent, 1 -> extent lies inside other_extent
     """
-    poly = coords2poly(bbox)
-    other_poly = coords2poly(other_bbox)
+    poly = coords2poly(extent)
+    other_poly = coords2poly(other_extent)
     return poly_overlap(poly=poly, other_poly=other_poly)
 
 
diff --git a/test/drs_search.py b/test/drs_search.py
index a076d4f..938fe61 100644
--- a/test/drs_search.py
+++ b/test/drs_search.py
@@ -19,7 +19,7 @@ idx = indexation.Index(scenes)
 # search
 print("search roi")
 gdal_ds = gdal.Open(params.roi)
-bbox = utils.get_bbox_wgs84(gdal_ds)
+bbox = utils.get_extent_wgs84(gdal_ds)
 matches = idx.find(bbox_wgs84=bbox)
 print("{} scenes found.".format(len(matches)))
 #for scene_match in matches:
diff --git a/test/drs_stack.py b/test/drs_stack.py
index 7e65e7e..bb00738 100644
--- a/test/drs_stack.py
+++ b/test/drs_stack.py
@@ -20,7 +20,7 @@ idx = indexation.Index(scenes)
 # search
 print("search roi")
 gdal_ds = gdal.Open(params.roi)
-bbox = utils.get_bbox_wgs84(gdal_ds)
+bbox = utils.get_extent_wgs84(gdal_ds)
 matches = idx.find(bbox_wgs84=bbox)
 print("{} scenes found.".format(len(matches)))
 
-- 
GitLab


From a47f8176e833ecac22a1a42e21f00046c7671cc6 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 11:57:00 +0100
Subject: [PATCH 12/27] ADD: pickle serialization

---
 scenes/drs.py        | 4 +---
 scenes/indexation.py | 4 ++--
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/scenes/drs.py b/scenes/drs.py
index 7745faf..e40efab 100644
--- a/scenes/drs.py
+++ b/scenes/drs.py
@@ -1,8 +1,8 @@
 """
 A set of utils to deal with DRS products
 """
-import tqdm
 import pickle
+import tqdm
 from scenes import spot, utils
 
 
@@ -78,5 +78,3 @@ def load_scenes(pickle_file):
     :return: list of Scene instances
     """
     return pickle.load(open(pickle_file, "rb"))
-
-
diff --git a/scenes/indexation.py b/scenes/indexation.py
index 3de42ba..cf7de49 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -28,7 +28,7 @@ class Index:
         """
         eps_ts = 10 * 3600  # Ten hours
         timestamp = get_timestamp(dt)
-        _, (xmax, ymax), _, (xmin, ymin) = bbox_wgs84
+        (xmin, xmax, ymin, ymax) = bbox_wgs84
         return xmin, ymin, timestamp - eps_ts, xmax, ymax, timestamp + eps_ts
 
     @staticmethod
@@ -40,7 +40,7 @@ class Index:
         :param dt_max: date max (datetime.datetime)
         :return: item for rtree
         """
-        _, (xmax, ymax), _, (xmin, ymin) = bbox_wgs84
+        (xmin, xmax, ymin, ymax) = bbox_wgs84
         return xmin, ymin, get_timestamp(dt_min), xmax, ymax, get_timestamp(dt_max)
 
     def __init__(self, scenes_list):
-- 
GitLab


From be9112ab9594be22e144e25f6619810b33e51964 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 12:22:35 +0100
Subject: [PATCH 13/27] TEST: indexation tests

---
 .gitlab-ci.yml          |  5 +++++
 test/imagery_test.py    |  4 ----
 test/indexation_test.py | 34 ++++++++++++++++++++++++++++++++++
 3 files changed, 39 insertions(+), 4 deletions(-)
 create mode 100644 test/indexation_test.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8a6c2e1..4939b2b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -46,3 +46,8 @@ imagery:
   extends: .test_base
   script:
     - pytest -o log_cli=true --log-cli-level=INFO --junitxml=report_imagery_test.xml test/imagery_test.py
+
+indexation:
+  extends: .test_base
+  script:
+    - pytest -o log_cli=true --log-cli-level=INFO --junitxml=report_indexation_test.xml test/indexation_test.py
diff --git a/test/imagery_test.py b/test/imagery_test.py
index f90daae..b1603c3 100644
--- a/test/imagery_test.py
+++ b/test/imagery_test.py
@@ -41,10 +41,6 @@ class ImageryTest(ScenesTestBase):
         # We test the central area only, because bayes method messes with nodata outside image
         self.compare_images(pxs, pxs_baseline, roi=[2000, 2000, 1000, 1000])
 
-    def test_epsg(self):
-        scene1 = self.get_scene1()
-        self.assertTrue(scene1.epsg == 2154)
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/indexation_test.py b/test/indexation_test.py
new file mode 100644
index 0000000..357c4e8
--- /dev/null
+++ b/test/indexation_test.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from scenes_test_base import ScenesTestBase
+from scenes import spot, indexation
+import pyotb
+
+
+class ImageryTest(ScenesTestBase):
+
+    def get_dimap_xs1(self):
+        return self.get_path("input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/"
+                             "DIM_SPOT6_MS_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML")
+
+    def get_dimap_pan1(self):
+        return self.get_path("input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_P_001_A/"
+                             "DIM_SPOT6_P_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML")
+
+    def get_scene1(self):
+        return spot.Spot67Scene(dimap_file_xs=self.get_dimap_xs1(),
+                                dimap_file_pan=self.get_dimap_pan1())
+
+    def test_scene1_indexation(self):
+        bbox_wgs84 = (4.3448, 43.6605, 4.3980, 43.6993)
+        scene1 = self.get_scene1()
+        index = indexation.Index(scenes_list=[scene1])
+        matches = index.find(bbox_wgs84=bbox_wgs84)
+        self.assertTrue(len(matches)==1)
+
+    def test_epsg(self):
+        scene1 = self.get_scene1()
+        self.assertTrue(scene1.epsg == 2154)
+
+
+if __name__ == '__main__':
+    unittest.main()
-- 
GitLab


From edf4917fff4370c9d47aa45fbad9f4a6d3e805d5 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 12:33:19 +0100
Subject: [PATCH 14/27] TEST: indexation tests

---
 .gitlab-ci.yml           |  2 +-
 test/imagery_test.py     | 16 ++--------------
 test/indexation_test.py  | 23 ++++-------------------
 test/scenes_test_base.py | 23 -----------------------
 test/tests_data.py       | 16 ++++++++++++++++
 5 files changed, 23 insertions(+), 57 deletions(-)
 create mode 100644 test/tests_data.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4939b2b..a727df1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -36,7 +36,7 @@ codespell:
   stage: Test
   allow_failure: false
   before_script:
-    - sudo python3 -m pip install pytest pytest-cov pyotb
+    - sudo python3 -m pip install pytest pytest-cov pyotb rtree
     - export PYTHONPATH=$PYTHONPATH:$PWD
     - wget -P . --no-verbose -e robots=off --recursive --level=inf --no-parent -R "index.html*" -R "LOGO.JPG" --cut-dirs=3 --no-host-directories --content-on-error http://indexof.montpellier.irstea.priv/projets/geocicd/scenes/test_data/
     - mkdir tests_artifacts
diff --git a/test/imagery_test.py b/test/imagery_test.py
index b1603c3..1dd0c86 100644
--- a/test/imagery_test.py
+++ b/test/imagery_test.py
@@ -1,26 +1,14 @@
 # -*- coding: utf-8 -*-
 from scenes_test_base import ScenesTestBase
-from scenes import spot
 import pyotb
+import tests_data
 
 
 class ImageryTest(ScenesTestBase):
 
-    def get_dimap_xs1(self):
-        return self.get_path("input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/"
-                             "DIM_SPOT6_MS_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML")
-
-    def get_dimap_pan1(self):
-        return self.get_path("input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_P_001_A/"
-                             "DIM_SPOT6_P_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML")
-
-    def get_scene1(self):
-        return spot.Spot67Scene(dimap_file_xs=self.get_dimap_xs1(),
-                                dimap_file_pan=self.get_dimap_pan1())
 
     def get_scene1_imagery(self):
-        scene1 = self.get_scene1()
-        return scene1.get_imagery()
+        return tests_data.SCENE1.get_imagery()
 
     def test_xs_imagery1(self):
         imagery = self.get_scene1_imagery()
diff --git a/test/indexation_test.py b/test/indexation_test.py
index 357c4e8..bda7fda 100644
--- a/test/indexation_test.py
+++ b/test/indexation_test.py
@@ -1,33 +1,18 @@
 # -*- coding: utf-8 -*-
 from scenes_test_base import ScenesTestBase
-from scenes import spot, indexation
-import pyotb
-
+from scenes import indexation
+import tests_data
 
 class ImageryTest(ScenesTestBase):
 
-    def get_dimap_xs1(self):
-        return self.get_path("input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/"
-                             "DIM_SPOT6_MS_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML")
-
-    def get_dimap_pan1(self):
-        return self.get_path("input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_P_001_A/"
-                             "DIM_SPOT6_P_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML")
-
-    def get_scene1(self):
-        return spot.Spot67Scene(dimap_file_xs=self.get_dimap_xs1(),
-                                dimap_file_pan=self.get_dimap_pan1())
-
     def test_scene1_indexation(self):
         bbox_wgs84 = (4.3448, 43.6605, 4.3980, 43.6993)
-        scene1 = self.get_scene1()
-        index = indexation.Index(scenes_list=[scene1])
+        index = indexation.Index(scenes_list=[tests_data.SCENE1])
         matches = index.find(bbox_wgs84=bbox_wgs84)
         self.assertTrue(len(matches)==1)
 
     def test_epsg(self):
-        scene1 = self.get_scene1()
-        self.assertTrue(scene1.epsg == 2154)
+        self.assertTrue(tests_data.SCENE1.epsg == 2154)
 
 
 if __name__ == '__main__':
diff --git a/test/scenes_test_base.py b/test/scenes_test_base.py
index b121be6..bad32a8 100644
--- a/test/scenes_test_base.py
+++ b/test/scenes_test_base.py
@@ -1,37 +1,14 @@
 # -*- coding: utf-8 -*-
-import os
 import unittest
 import filecmp
 from abc import ABC
 import pyotb
 
 
-def get_env_var(var):
-    """
-    Return the value of the sepficied environment variable
-    :param var: environment variable to return
-    :return: value of the environment variable
-    """
-    value = os.environ[var]
-    if value is None:
-        print("Environment variable {} is not set. Returning value None.".format(var))
-    return value
-
-
 class ScenesTestBase(ABC, unittest.TestCase):
     """
     Base class for tests
     """
-    TEST_DATA_DIR = get_env_var("TEST_DATA_DIR")
-
-    def get_path(self, path):
-        """
-        Return a path
-        :param path: relative path
-        :return: actual absolute path
-        """
-        return self.TEST_DATA_DIR + "/" + path
-
     def compare_images(self, image, reference, roi=None, mae_threshold=0.01):
         """
         Compare one image (typically: an output image) with a reference
diff --git a/test/tests_data.py b/test/tests_data.py
new file mode 100644
index 0000000..4319945
--- /dev/null
+++ b/test/tests_data.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+import os
+from scenes import spot
+
+
+# TEST_DATA_DIR environment variable
+TEST_DATA_DIR = os.environ["TEST_DATA_DIR"]
+
+# Filenames
+DIMAP1_XS = TEST_DATA_DIR + "input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/" \
+                            "DIM_SPOT6_MS_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML"
+DIMAP1_P = TEST_DATA_DIR + "input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_P_001_A/" \
+                           "DIM_SPOT6_P_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML"
+
+# Instances
+SCENE1 = spot.Spot67Scene(dimap_file_xs=DIMAP1_XS, dimap_file_pan=DIMAP1_P)
\ No newline at end of file
-- 
GitLab


From 93b24a4752a7dac3bf221d5e76a2ce5e2788ac9c Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 12:36:53 +0100
Subject: [PATCH 15/27] TEST: indexation tests

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

diff --git a/test/tests_data.py b/test/tests_data.py
index 4319945..75d0e3a 100644
--- a/test/tests_data.py
+++ b/test/tests_data.py
@@ -7,9 +7,9 @@ from scenes import spot
 TEST_DATA_DIR = os.environ["TEST_DATA_DIR"]
 
 # Filenames
-DIMAP1_XS = TEST_DATA_DIR + "input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/" \
+DIMAP1_XS = TEST_DATA_DIR + "/input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/" \
                             "DIM_SPOT6_MS_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML"
-DIMAP1_P = TEST_DATA_DIR + "input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_P_001_A/" \
+DIMAP1_P = TEST_DATA_DIR + "/input/ROI_1_Bundle_Ortho_GSD2015/PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_P_001_A/" \
                            "DIM_SPOT6_P_201503261014386_ORT_SPOT6_20170524_1422391k0ha487979cy_1.XML"
 
 # Instances
-- 
GitLab


From 5e6cb91f8e4ddcb91e14b0f98882e547aabfbafc Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 12:42:25 +0100
Subject: [PATCH 16/27] TEST: indexation tests

---
 scenes/indexation.py | 2 ++
 test/imagery_test.py | 8 ++++----
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/scenes/indexation.py b/scenes/indexation.py
index cf7de49..bf653ba 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -28,7 +28,9 @@ class Index:
         """
         eps_ts = 10 * 3600  # Ten hours
         timestamp = get_timestamp(dt)
+        print(bbox_wgs84)
         (xmin, xmax, ymin, ymax) = bbox_wgs84
+        print(xmin)
         return xmin, ymin, timestamp - eps_ts, xmax, ymax, timestamp + eps_ts
 
     @staticmethod
diff --git a/test/imagery_test.py b/test/imagery_test.py
index 1dd0c86..b1d496b 100644
--- a/test/imagery_test.py
+++ b/test/imagery_test.py
@@ -13,18 +13,18 @@ class ImageryTest(ScenesTestBase):
     def test_xs_imagery1(self):
         imagery = self.get_scene1_imagery()
         xs = imagery.get_xs()
-        self.compare_images(xs, self.get_dimap_xs1())
+        self.compare_images(xs, tests_data.DIMAP1_XS)
 
     def test_pan_imagery1(self):
         imagery = self.get_scene1_imagery()
         pan = imagery.get_pan()
-        self.compare_images(pan, self.get_dimap_pan1())
+        self.compare_images(pan, tests_data.DIMAP1_P)
 
     def test_pxs_imagery1(self):
         imagery = self.get_scene1_imagery()
         pxs = imagery.get_pxs()
-        pxs_baseline = pyotb.BundleToPerfectSensor({"inxs": self.get_dimap_xs1(),
-                                                    "inp": self.get_dimap_pan1(),
+        pxs_baseline = pyotb.BundleToPerfectSensor({"inxs": tests_data.DIMAP1_XS,
+                                                    "inp": tests_data.DIMAP1_P,
                                                     "method": "bayes"})
         # We test the central area only, because bayes method messes with nodata outside image
         self.compare_images(pxs, pxs_baseline, roi=[2000, 2000, 1000, 1000])
-- 
GitLab


From 20df136b149b686d783753ed4ec35861ff7ea51b Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 12:47:54 +0100
Subject: [PATCH 17/27] TEST: indexation tests

---
 scenes/indexation.py | 2 --
 scenes/utils.py      | 2 +-
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/scenes/indexation.py b/scenes/indexation.py
index bf653ba..cf7de49 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -28,9 +28,7 @@ class Index:
         """
         eps_ts = 10 * 3600  # Ten hours
         timestamp = get_timestamp(dt)
-        print(bbox_wgs84)
         (xmin, xmax, ymin, ymax) = bbox_wgs84
-        print(xmin)
         return xmin, ymin, timestamp - eps_ts, xmax, ymax, timestamp + eps_ts
 
     @staticmethod
diff --git a/scenes/utils.py b/scenes/utils.py
index 10f34eb..49ef761 100644
--- a/scenes/utils.py
+++ b/scenes/utils.py
@@ -81,7 +81,7 @@ def get_bbox_from_extent(extent):
     :return: bounding box (xmin, xmax, ymin, ymax)
     """
     (xmin, ymax), (xmax, _), (_, ymin), (_, _) = extent
-    return xmin, xmax, ymin, ymax
+    return min(xmin, xmax), max(xmin, xmax), min(ymin, ymax), max(ymin, ymax)
 
 
 def get_epsg_extent_bbox(filename):
-- 
GitLab


From 73810126f28d656d0213d63f6c3ee92748e87371 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 13:26:31 +0100
Subject: [PATCH 18/27] TEST: indexation tests

---
 scenes/indexation.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/scenes/indexation.py b/scenes/indexation.py
index cf7de49..a3ddb13 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -29,6 +29,10 @@ class Index:
         eps_ts = 10 * 3600  # Ten hours
         timestamp = get_timestamp(dt)
         (xmin, xmax, ymin, ymax) = bbox_wgs84
+        print(xmin)
+        print(xmax)
+        print(ymin)
+        print(ymax)
         return xmin, ymin, timestamp - eps_ts, xmax, ymax, timestamp + eps_ts
 
     @staticmethod
-- 
GitLab


From 746e97fbb8ec08c9c6ee7bf4732a78a1494024f4 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 13:27:34 +0100
Subject: [PATCH 19/27] TEST: indexation tests

---
 scenes/indexation.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/scenes/indexation.py b/scenes/indexation.py
index a3ddb13..3959489 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -29,6 +29,7 @@ class Index:
         eps_ts = 10 * 3600  # Ten hours
         timestamp = get_timestamp(dt)
         (xmin, xmax, ymin, ymax) = bbox_wgs84
+        print("bbox:")
         print(xmin)
         print(xmax)
         print(ymin)
@@ -45,6 +46,11 @@ class Index:
         :return: item for rtree
         """
         (xmin, xmax, ymin, ymax) = bbox_wgs84
+        print("bbox:")
+        print(xmin)
+        print(xmax)
+        print(ymin)
+        print(ymax)
         return xmin, ymin, get_timestamp(dt_min), xmax, ymax, get_timestamp(dt_max)
 
     def __init__(self, scenes_list):
-- 
GitLab


From 29e80589e4ddb8582a27afef74ed5a43db463798 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 13:57:47 +0100
Subject: [PATCH 20/27] TEST: indexation tests

---
 scenes/utils.py | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/scenes/utils.py b/scenes/utils.py
index 49ef761..8407477 100644
--- a/scenes/utils.py
+++ b/scenes/utils.py
@@ -62,9 +62,9 @@ def reproject_coords(coords, src_srs, tgt_srs):
 
 def get_extent_wgs84(gdal_ds):
     """
-    Returns the bounding box in WGS84 CRS from a GDAL dataset
+    Returns the extent in WGS84 CRS from a GDAL dataset
     :param gdal_ds: GDAL dataset
-    :return: bounding box in WGS84 CRS
+    :return: extent coordinates in WGS84 CRS
     """
     coords = get_extent(gdal_ds)
     src_srs = osr.SpatialReference()
@@ -74,7 +74,7 @@ def get_extent_wgs84(gdal_ds):
     return reproject_coords(coords, src_srs, tgt_srs)
 
 
-def get_bbox_from_extent(extent):
+def extent_to_bbox(extent):
     """
     Converts an extent into a bounding box
     :param extent: extent
@@ -84,6 +84,16 @@ def get_bbox_from_extent(extent):
     return min(xmin, xmax), max(xmin, xmax), min(ymin, ymax), max(ymin, ymax)
 
 
+def get_bbox_wgs84(gdal_ds):
+    """
+    Returns the bounding box in WGS84 CRS from a GDAL dataset
+    :param gdal_ds: GDAL dataset
+    :return: bounding box (xmin, xmax, ymin, ymax)
+    """
+    extend_wgs84 = get_extent_wgs84(gdal_ds)
+    return extent_to_bbox(extend_wgs84)
+
+
 def get_epsg_extent_bbox(filename):
     """
     Returns (epsg, extent_wgs84) from a raster file that GDAL can open.
@@ -93,7 +103,7 @@ def get_epsg_extent_bbox(filename):
     gdal_ds = gdal.Open(filename)
     epsg = get_epsg(gdal_ds)
     extent_wgs84 = get_extent_wgs84(gdal_ds)
-    bbox_wgs84 = get_bbox_from_extent(extent_wgs84)
+    bbox_wgs84 = extent_to_bbox(extent_wgs84)
     return epsg, extent_wgs84, bbox_wgs84
 
 
-- 
GitLab


From 691241ffd8bf50d6c20040182d59b4ca9fc191d4 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 13:58:44 +0100
Subject: [PATCH 21/27] ADD: pickle serialization

---
 test/drs_search.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/drs_search.py b/test/drs_search.py
index 938fe61..16dd34a 100644
--- a/test/drs_search.py
+++ b/test/drs_search.py
@@ -19,7 +19,7 @@ idx = indexation.Index(scenes)
 # search
 print("search roi")
 gdal_ds = gdal.Open(params.roi)
-bbox = utils.get_extent_wgs84(gdal_ds)
+bbox = utils.get_bbox_wgs84(gdal_ds=gdal_ds)
 matches = idx.find(bbox_wgs84=bbox)
 print("{} scenes found.".format(len(matches)))
 #for scene_match in matches:
-- 
GitLab


From 7c56267822d981765e11a8e91bee5a0df04e19a3 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 14:06:26 +0100
Subject: [PATCH 22/27] ADD: pickle serialization

---
 scenes/indexation.py | 64 +++++++++++++++++++-------------------------
 1 file changed, 27 insertions(+), 37 deletions(-)

diff --git a/scenes/indexation.py b/scenes/indexation.py
index 3959489..0a5afb0 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -14,45 +14,35 @@ def get_timestamp(dt):
     return dt.replace(tzinfo=datetime.timezone.utc).timestamp()
 
 
-class Index:
+def new_bbox(bbox_wgs84, dt):
     """
-    Stores an indexation structures for a list of Scenes
+    Return a bounding box in the domain (lat, lon, time)
+    :param bbox_wgs84: Bounding box (in WGS84)
+    :param dt: date datetime.datetime
+    :return: item for rtree
     """
-    @staticmethod
-    def new_bbox(bbox_wgs84, dt):
-        """
-        Return a bounding box in the domain (lat, lon, time)
-        :param bbox_wgs84: Bounding box (in WGS84)
-        :param dt: date datetime.datetime)
-        :return: item for rtree
-        """
-        eps_ts = 10 * 3600  # Ten hours
-        timestamp = get_timestamp(dt)
-        (xmin, xmax, ymin, ymax) = bbox_wgs84
-        print("bbox:")
-        print(xmin)
-        print(xmax)
-        print(ymin)
-        print(ymax)
-        return xmin, ymin, timestamp - eps_ts, xmax, ymax, timestamp + eps_ts
+    dt_min = dt - datetime.timedelta(days=1)
+    dt_max = dt + datetime.timedelta(days=1)
+
+    return bbox(bbox_wgs84=bbox_wgs84, dt_min=dt_min, dt_max=dt_max)
 
-    @staticmethod
-    def bbox(bbox_wgs84, dt_min, dt_max):
-        """
-        Return a bounding box in the domain (lat, lon, time)
-        :param bbox_wgs84: Bounding box (in WGS84)
-        :param dt_min: date min (datetime.datetime)
-        :param dt_max: date max (datetime.datetime)
-        :return: item for rtree
-        """
-        (xmin, xmax, ymin, ymax) = bbox_wgs84
-        print("bbox:")
-        print(xmin)
-        print(xmax)
-        print(ymin)
-        print(ymax)
-        return xmin, ymin, get_timestamp(dt_min), xmax, ymax, get_timestamp(dt_max)
 
+def bbox(bbox_wgs84, dt_min, dt_max):
+    """
+    Return a bounding box in the domain (lat, lon, time)
+    :param bbox_wgs84: Bounding box (in WGS84)
+    :param dt_min: date min (datetime.datetime)
+    :param dt_max: date max (datetime.datetime)
+    :return: item for rtree
+    """
+    (xmin, xmax, ymin, ymax) = bbox_wgs84
+    return xmin, ymin, get_timestamp(dt_min), xmax, ymax, get_timestamp(dt_max)
+
+
+class Index:
+    """
+    Stores an indexation structures for a list of Scenes
+    """
     def __init__(self, scenes_list):
         """
         :param scenes_list: list of scenes
@@ -64,7 +54,7 @@ class Index:
         properties.dimension = 3
         self.index = rtree.index.Index(properties=properties)
         for scene_idx, scene in enumerate(scenes_list):
-            bbox = self.new_bbox(bbox_wgs84=scene.bbox_wgs84, dt=scene.acquisition_date)
+            bbox = new_bbox(bbox_wgs84=scene.bbox_wgs84, dt=scene.acquisition_date)
             self.index.insert(scene_idx, bbox)
 
     def find_indices(self, bbox_wgs84, dt_min=None, dt_max=None):
@@ -79,7 +69,7 @@ class Index:
             dt_min = datetime.datetime.strptime("2000-01-01", "%Y-%m-%d")
         if not dt_max:
             dt_max = datetime.datetime.strptime("3000-01-01", "%Y-%m-%d")
-        bbox_search = self.bbox(bbox_wgs84=bbox_wgs84, dt_min=dt_min, dt_max=dt_max)
+        bbox_search = bbox(bbox_wgs84=bbox_wgs84, dt_min=dt_min, dt_max=dt_max)
         return self.index.intersection(bbox_search)
 
     def find(self, bbox_wgs84, dt_min=None, dt_max=None):
-- 
GitLab


From b5b20eade4f83ee81e1a394cd07994b765e4fe2e Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 14:12:10 +0100
Subject: [PATCH 23/27] ADD: pickle serialization

---
 test/indexation_test.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/test/indexation_test.py b/test/indexation_test.py
index bda7fda..706948b 100644
--- a/test/indexation_test.py
+++ b/test/indexation_test.py
@@ -6,10 +6,11 @@ import tests_data
 class ImageryTest(ScenesTestBase):
 
     def test_scene1_indexation(self):
-        bbox_wgs84 = (4.3448, 43.6605, 4.3980, 43.6993)
         index = indexation.Index(scenes_list=[tests_data.SCENE1])
-        matches = index.find(bbox_wgs84=bbox_wgs84)
-        self.assertTrue(len(matches)==1)
+        matches1 = index.find(bbox_wgs84=(43.6605, 43.6993, 4.3448, 4.3980))
+        self.assertTrue(matches1)
+        matches2 = index.find(bbox_wgs84=(43.000, 43.001, 3.000, 3.001))
+        self.assertFalse(matches2)
 
     def test_epsg(self):
         self.assertTrue(tests_data.SCENE1.epsg == 2154)
-- 
GitLab


From 4bbc99332cb80015e210a26a2fb7c93d38fdc114 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 14:16:44 +0100
Subject: [PATCH 24/27] ADD: pickle serialization

---
 test/indexation_test.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/indexation_test.py b/test/indexation_test.py
index 706948b..6b440d6 100644
--- a/test/indexation_test.py
+++ b/test/indexation_test.py
@@ -6,6 +6,7 @@ import tests_data
 class ImageryTest(ScenesTestBase):
 
     def test_scene1_indexation(self):
+        print(tests_data.SCENE1.bbox_wgs84)
         index = indexation.Index(scenes_list=[tests_data.SCENE1])
         matches1 = index.find(bbox_wgs84=(43.6605, 43.6993, 4.3448, 4.3980))
         self.assertTrue(matches1)
-- 
GitLab


From f18445b3bb2c8a8ccc131ad722b250d7209b5399 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 14:17:47 +0100
Subject: [PATCH 25/27] ADD: pickle serialization

---
 test/indexation_test.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/indexation_test.py b/test/indexation_test.py
index 6b440d6..319c4f1 100644
--- a/test/indexation_test.py
+++ b/test/indexation_test.py
@@ -8,7 +8,7 @@ class ImageryTest(ScenesTestBase):
     def test_scene1_indexation(self):
         print(tests_data.SCENE1.bbox_wgs84)
         index = indexation.Index(scenes_list=[tests_data.SCENE1])
-        matches1 = index.find(bbox_wgs84=(43.6605, 43.6993, 4.3448, 4.3980))
+        matches1 = index.find(bbox_wgs84=(43.706, 43.708, 4.317, 4.420))
         self.assertTrue(matches1)
         matches2 = index.find(bbox_wgs84=(43.000, 43.001, 3.000, 3.001))
         self.assertFalse(matches2)
-- 
GitLab


From 0353747e9fff64c486a0c4d63fd0f8a6d394e705 Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 14:18:34 +0100
Subject: [PATCH 26/27] ADD: pickle serialization

---
 test/indexation_test.py | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/test/indexation_test.py b/test/indexation_test.py
index 319c4f1..c764d32 100644
--- a/test/indexation_test.py
+++ b/test/indexation_test.py
@@ -6,12 +6,9 @@ import tests_data
 class ImageryTest(ScenesTestBase):
 
     def test_scene1_indexation(self):
-        print(tests_data.SCENE1.bbox_wgs84)
         index = indexation.Index(scenes_list=[tests_data.SCENE1])
-        matches1 = index.find(bbox_wgs84=(43.706, 43.708, 4.317, 4.420))
-        self.assertTrue(matches1)
-        matches2 = index.find(bbox_wgs84=(43.000, 43.001, 3.000, 3.001))
-        self.assertFalse(matches2)
+        self.assertTrue(index.find(bbox_wgs84=(43.706, 43.708, 4.317, 4.420)))
+        self.assertFalse(index.find(bbox_wgs84=(43.000, 43.001, 3.000, 3.001)))
 
     def test_epsg(self):
         self.assertTrue(tests_data.SCENE1.epsg == 2154)
-- 
GitLab


From 714805a02843ab0e110f31c757119c7def6d700f Mon Sep 17 00:00:00 2001
From: Remi Cresson <remi.cresson@irstea.fr>
Date: Wed, 23 Feb 2022 16:13:25 +0100
Subject: [PATCH 27/27] ADD: pickle serialization

---
 scenes/indexation.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/scenes/indexation.py b/scenes/indexation.py
index 0a5afb0..1968902 100644
--- a/scenes/indexation.py
+++ b/scenes/indexation.py
@@ -54,8 +54,7 @@ class Index:
         properties.dimension = 3
         self.index = rtree.index.Index(properties=properties)
         for scene_idx, scene in enumerate(scenes_list):
-            bbox = new_bbox(bbox_wgs84=scene.bbox_wgs84, dt=scene.acquisition_date)
-            self.index.insert(scene_idx, bbox)
+            self.index.insert(scene_idx, new_bbox(bbox_wgs84=scene.bbox_wgs84, dt=scene.acquisition_date))
 
     def find_indices(self, bbox_wgs84, dt_min=None, dt_max=None):
         """
-- 
GitLab