Repository: CS-SI/eodag
Pull request #654 (2023-02-22 07:39:22)

feat: mundi georss geometry handling
- Adds a new method `convert_from_georss()` in `metadata_mapping`
- Handle `mundi` GeoRSS geometries
- `-180 -90 180 90` as `defaultGeometry` for `mundi` products without geometry

github-actions[bot]: ## Unit Test Results
    2 files  ±    0      2 suites  ±0   2m 27s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") - 2m 51s
364 tests +    1  361 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +    1  2 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests")  - 1  1 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") +1 
408 runs   - 318  405 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests")  - 315  2 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests")  - 4  1 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") +1 

For more details on these failures, see [this check](https://github.com/CS-SI/eodag/runs/11512330787).

Results for commit daa96cf8. ± Comparison against base commit a10c807e.

[test-results]:data:application/gzip;base64,H4sIAJ/H9WMC/02MQQ6DIBAAv2I497AIVehnGrpAQqrSIJxM/16gih5nJpmNWDeZlTy6/taRNbnYQKegovNLRsrHLHKKJbKBH/RcE2JV9FRv99kXf2GVm8qkCROCD9lANiEt5clB7HAsOdybOY+VL8PK1x/6eXYxA9FKyQGtoFQZalmPDCRDYbVE+jIgwaAdwTDy/QGgfHevBQEAAA==


Patch:
diff --git a/eodag/api/product/metadata_mapping.py b/eodag/api/product/metadata_mapping.py
index 971ed9c9..4d730512 100644
--- a/eodag/api/product/metadata_mapping.py
+++ b/eodag/api/product/metadata_mapping.py
@@ -31,7 +31,7 @@ from jsonpath_ng.jsonpath import Child, Fields, Index
 from lxml import etree
 from lxml.etree import XPathEvalError
 from shapely import wkt
-from shapely.geometry import MultiPolygon
+from shapely.geometry import MultiPolygon, Polygon
 from shapely.ops import transform
 
 from eodag.utils import (
@@ -138,6 +138,7 @@ def format_metadata(search_param, *args, **kwargs):
         - ``to_geojson``: convert to a GeoJSON (via __geo_interface__ if exists)
         - ``from_ewkt``: convert EWKT to shapely geometry / WKT in DEFAULT_PROJ
         - ``to_ewkt``: convert to EWKT (Extended Well-Known text)
+        - ``from_georss``: convert GeoRSS to shapely geometry / WKT in DEFAULT_PROJ
         - ``csv_list``: convert to a comma separated list
         - ``to_iso_utc_datetime_from_milliseconds``: convert a utc timestamp in given
           milliseconds to a utc iso datetime
@@ -324,9 +325,12 @@ def format_metadata(search_param, *args, **kwargs):
                 from_proj = pyproj.Proj(from_proj)
                 to_proj = pyproj.Proj(DEFAULT_PROJ)
 
-                project = partial(pyproj.transform, from_proj, to_proj)
-
-                return transform(project, input_geom)
+                if from_proj != to_proj:
+                    # reproject
+                    project = partial(pyproj.transform, from_proj, to_proj)
+                    return transform(project, input_geom)
+                else:
+                    return input_geom
             else:
                 logger.warning(f"Could not read {ewkt_string} as EWKT")
                 return ewkt_string
@@ -340,6 +344,57 @@ def format_metadata(search_param, *args, **kwargs):
 
             return f"{proj};{wkt_geom}"
 
+        @staticmethod
+        def convert_from_georss(georss):
+            """Convert GeoRSS to shapely geometry"""
+
+            if "polygon" in georss.tag:
+                # Polygon
+                coords_list = georss.text.split()
+                polygon_args = [
+                    (float(coords_list[2 * i]), float(coords_list[2 * i + 1]))
+                    for i in range(int(len(coords_list) / 2))
+                ]
+                return Polygon(polygon_args)
+            elif len(georss) == 1 and "multisurface" in georss[0].tag.lower():
+                # Multipolygon
+                from_proj = getattr(georss[0], "attrib", {}).get("srsName", None)
+                if from_proj:
+                    from_proj = pyproj.Proj(from_proj)
+                    to_proj = pyproj.Proj(DEFAULT_PROJ)
+                    project = partial(pyproj.transform, from_proj, to_proj)
+
+                # function to get deepest elements
+                def flatten_elements(nested):
+
+                    for e in nested:
+                        if len(e) > 0:
+                            yield from flatten_elements(e)
+                        else:
+                            yield e
+
+                polygons_list = []
+                for elem in flatten_elements(georss[0]):
+                    coords_list = elem.text.split()
+                    polygon_args = [
+                        (float(coords_list[2 * i]), float(coords_list[2 * i + 1]))
+                        for i in range(int(len(coords_list) / 2))
+                    ]
+                    polygon = Polygon(polygon_args)
+                    # reproject if needed
+                    if from_proj and from_proj != to_proj:
+                        polygons_list.append(transform(project, polygon))
+                    else:
+                        polygons_list.append(polygon)
+
+                return MultiPolygon(polygons_list)
+
+            else:
+                logger.warning(
+                    f"Incoming GeoRSS format not supported yet: {str(georss)}"
+                )
+                return georss
+
         @staticmethod
         def convert_csv_list(values_list):
             if isinstance(values_list, list):
@@ -654,6 +709,23 @@ def properties_from_xml(
                 if conversion_or_none is None:
                     properties[metadata] = extracted_value
                 else:
+                    # reformat conversion_or_none as metadata#converter(args) or metadata#converter
+                    if (
+                        len(conversion_or_none) > 1
+                        and isinstance(conversion_or_none, list)
+                        and conversion_or_none[1] is not None
+                    ):
+                        conversion_or_none = "%s(%s)" % (
+                            conversion_or_none[0],
+                            conversion_or_none[1],
+                        )
+                    elif isinstance(conversion_or_none, list):
+                        conversion_or_none = conversion_or_none[0]
+
+                    # check if conversion uses variables to format
+                    if re.search(r"({[^{}]+})+", conversion_or_none):
+                        conversion_or_none = conversion_or_none.format(**properties)
+
                     properties[metadata] = [
                         format_metadata(
                             "{%s%s%s}"
diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml
index 629dee91..e8e549c4 100644
--- a/eodag/resources/providers.yml
+++ b/eodag/resources/providers.yml
@@ -1267,7 +1267,8 @@
       # The geographic extent of the product
       geometry:
         - 'geometry={geometry#to_rounded_wkt}'
-        - 'georss:box/text()'
+        - '{georss:polygon|georss:where#from_georss}'
+      defaultGeometry: '-180 -90 180 90'
       # The url of the quicklook
       quicklook: 'media:group/media:content[media:category="QUICKLOOK"]/@url'
       # The url to download the product "as is" (literal or as a template to be completed either after the search result


Test patch:
diff --git a/tests/units/test_metadata_mapping.py b/tests/units/test_metadata_mapping.py
index 3b1b0e02..06248a5b 100644
--- a/tests/units/test_metadata_mapping.py
+++ b/tests/units/test_metadata_mapping.py
@@ -18,6 +18,7 @@
 import unittest
 
 from jsonpath_ng.ext import parse
+from lxml import etree
 from shapely import wkt
 
 from tests.context import (
@@ -141,6 +142,55 @@ class TestMetadataFormatter(unittest.TestCase):
             "SRID=4326;POINT (0.1100 1.2200)",
         )
 
+    def test_convert_from_georss(self):
+        to_format = "{fieldname#from_georss}"
+        # polygon
+        georss = etree.Element("polygon")
+        georss.text = "1.23 43.42 1.23 43.76 1.68 43.76 1.68 43.42 1.23 43.42"
+        geom = format_metadata(to_format, fieldname=georss)
+        self.assertEqual(
+            geom,
+            "POLYGON ((1.23 43.42, 1.23 43.76, 1.68 43.76, 1.68 43.42, 1.23 43.42))",
+        )
+        # multipolygon
+        georss = etree.Element("where")
+        sub_multipolygon = etree.SubElement(georss, "Multisurface")
+        sub_polygon1 = etree.SubElement(sub_multipolygon, "foo")
+        sub_polygon1.text = "1.23 43.42 1.23 43.76 1.68 43.76 1.68 43.42 1.23 43.42"
+        sub_polygon2 = etree.SubElement(sub_multipolygon, "bar")
+        sub_polygon2.text = "2.23 43.42 2.23 43.76 3.68 43.76 3.68 43.42 2.23 43.42"
+        geom = format_metadata(to_format, fieldname=georss)
+        self.assertEqual(
+            geom,
+            (
+                "MULTIPOLYGON ("
+                "((1.23 43.42, 1.23 43.76, 1.68 43.76, 1.68 43.42, 1.23 43.42)), "
+                "((2.23 43.42, 2.23 43.76, 3.68 43.76, 3.68 43.42, 2.23 43.42))"
+                ")"
+            ),
+        )
+        # multipolygon in different projection
+        georss = etree.Element("where")
+        sub_multipolygon = etree.SubElement(georss, "Multisurface")
+        sub_multipolygon.attrib["srsName"] = "EPSG:3857"
+        sub_polygon1 = etree.SubElement(sub_multipolygon, "foo")
+        sub_polygon1.text = (
+            "4833492 136933 4871341 136933 4871341 187044 4833492 187044 4833492 136933"
+        )
+        sub_polygon2 = etree.SubElement(sub_multipolygon, "bar")
+        sub_polygon2.text = (
+            "4833492 248305 4871341 248305 4871341 409938 4833492 409938 4833492 248305"
+        )
+        wkt_str = format_metadata(to_format, fieldname=georss)
+        geom = wkt.loads(wkt_str)
+        self.assertEqual(len(geom.geoms), 2)
+        self.assertEqual(
+            [round(x, 2) for x in geom.geoms[0].bounds], [1.23, 43.42, 1.68, 43.76]
+        )
+        self.assertEqual(
+            [round(x, 2) for x in geom.geoms[1].bounds], [2.23, 43.42, 3.68, 43.76]
+        )
+
     def test_convert_csv_list(self):
         to_format = "{fieldname#csv_list}"
         self.assertEqual(