Repository: CS-SI/eodag
Pull request #619 (2023-01-27 10:12:27)

feat: handle EWKT geometry format
handle PostGIS [EWKT](https://postgis.net/docs/ST_AsEWKT.html) geometry format with new `MetadataFormatter.convert_from_ewkt()` and `MetadataFormatter.convert_to_ewkt()` methods

github-actions[bot]: ## Unit Test Results
    2 files  ±0      2 suites  ±0   8m 53s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") - 2m 26s
353 tests +2  350 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +2  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 
706 runs  +4  700 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +4  4 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests")  - 2  2 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") +2 

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

Results for commit 368dec92. ± Comparison against base commit 70586e94.

[test-results]:data:application/gzip;base64,H4sIAAel02MC/02Myw7CIBBFf6Vh7YIyBRx/xuAACbEPQ2HV+O+CbdHlOTf3bMyH0a3s1olLx9YcUgObo0lhmQtKgCLKlOoIstF9zURfxX/qGV5HYhfehLGIvgkX4xKLqZ+Y59rUXB1wJjXnzezF4eQjKE7+79EyTSEVYKCu1hEKUKTRAxjpFNJDC0QvTW8EOa0GtOz9AV1LCVoFAQAA

github-actions[bot]: <strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `87%` | :white_check_mark: |
| eodag/api/product/metadata\_mapping.py | `81%` | :white_check_mark: |

<strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `86%` | :white_check_mark: |
| eodag/api/product/metadata\_mapping.py | `81%` | :white_check_mark: |
| eodag/api/product/metadata\_mapping.py | `80%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against 368dec9236c79f33a5e69cb7299f5a1a2ce7649d </p>

Patch:
diff --git a/eodag/api/product/metadata_mapping.py b/eodag/api/product/metadata_mapping.py
index cdb0c2ce..f3247002 100644
--- a/eodag/api/product/metadata_mapping.py
+++ b/eodag/api/product/metadata_mapping.py
@@ -20,9 +20,11 @@ import logging
 import re
 from copy import deepcopy
 from datetime import datetime, timedelta
+from functools import partial
 from string import Formatter
 
 import geojson
+import pyproj
 from dateutil.parser import isoparse
 from dateutil.tz import UTC, tzutc
 from jsonpath_ng.jsonpath import Child
@@ -30,8 +32,10 @@ from lxml import etree
 from lxml.etree import XPathEvalError
 from shapely import wkt
 from shapely.geometry import MultiPolygon
+from shapely.ops import transform
 
 from eodag.utils import (
+    DEFAULT_PROJ,
     cached_parse,
     get_timestamp,
     items_recursive_apply,
@@ -132,6 +136,8 @@ def format_metadata(search_param, *args, **kwargs):
         - ``to_nwse_bounds``: convert to North,West,South,East bounds
         - ``to_nwse_bounds_str``: convert to North,West,South,East bounds string with given separator
         - ``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)
         - ``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
@@ -304,6 +310,36 @@ def format_metadata(search_param, *args, **kwargs):
         def convert_to_geojson(string):
             return geojson.dumps(string)
 
+        @staticmethod
+        def convert_from_ewkt(ewkt_string):
+            """Convert EWKT (Extended Well-Known text) to shapely geometry"""
+
+            ewkt_regex = re.compile(r"^(?P<proj>[A-Za-z]+=[0-9]+);(?P<wkt>.*)$")
+            ewkt_match = ewkt_regex.match(ewkt_string)
+            if ewkt_match:
+                g = ewkt_match.groupdict()
+                from_proj = g["proj"].replace("SRID", "EPSG").replace("=", ":")
+                input_geom = wkt.loads(g["wkt"])
+
+                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)
+            else:
+                logger.warning(f"Could not read {ewkt_string} as EWKT")
+                return ewkt_string
+
+        @staticmethod
+        def convert_to_ewkt(input_geom):
+            """Convert shapely geometry to EWKT (Extended Well-Known text)"""
+
+            proj = DEFAULT_PROJ.upper().replace("EPSG", "SRID").replace(":", "=")
+            wkt_geom = MetadataFormatter.convert_to_rounded_wkt(input_geom)
+
+            return f"{proj};{wkt_geom}"
+
         @staticmethod
         def convert_csv_list(values_list):
             if isinstance(values_list, list):
@@ -339,7 +375,7 @@ def format_metadata(search_param, *args, **kwargs):
             return items_recursive_apply(
                 input_obj,
                 lambda k, v, x, y: re.sub(x, y, v) if isinstance(v, str) else v,
-                **{"x": old, "y": new}
+                **{"x": old, "y": new},
             )
 
         @staticmethod
@@ -471,7 +507,7 @@ def properties_from_json(json, mapping, discovery_config=None):
 
                     properties[metadata] = format_metadata(
                         "{%s%s%s}" % (metadata, SEP, conversion_or_none),
-                        **{metadata: extracted_value}
+                        **{metadata: extracted_value},
                     )
         # properties as python objects when possible (format_metadata returns only strings)
         try:
@@ -587,7 +623,7 @@ def properties_from_xml(
                         conversion_or_none = conversion_or_none[0]
                     properties[metadata] = format_metadata(
                         "{%s%s%s}" % (metadata, SEP, conversion_or_none),
-                        **{metadata: extracted_value[0]}
+                        **{metadata: extracted_value[0]},
                     )
                 # store element tag in used_xpaths
                 used_xpaths.append(
@@ -616,7 +652,7 @@ def properties_from_xml(
                                 SEP,
                                 conversion_or_none,
                             ),  # Re-build conversion format identifier
-                            **{metadata: extracted_value_item}
+                            **{metadata: extracted_value_item},
                         )
                         for extracted_value_item in extracted_value
                     ]
@@ -785,5 +821,5 @@ DEFAULT_METADATA_MAPPING = dict(
         # either after the search result is obtained from the provider or during the eodag
         # download phase)
         "downloadLink": "$.properties.downloadLink",
-    }
+    },
 )


Test patch:
diff --git a/tests/units/test_metadata_mapping.py b/tests/units/test_metadata_mapping.py
index 4089601a..3b1b0e02 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 shapely import wkt
 
 from tests.context import (
     NOT_AVAILABLE,
@@ -123,6 +124,23 @@ class TestMetadataFormatter(unittest.TestCase):
             '{"type": "Point", "coordinates": [0.11, 1.22]}',
         )
 
+    def test_convert_from_ewkt(self):
+        to_format = "{fieldname#from_ewkt}"
+        wkt_str = format_metadata(
+            to_format, fieldname="SRID=3857;POINT (321976 5390999)"
+        )
+        geom = wkt.loads(wkt_str)
+        self.assertEqual(round(geom.x, 1), 43.5)
+        self.assertEqual(round(geom.y, 1), 2.9)
+
+    def test_convert_to_ewkt(self):
+        to_format = "{fieldname#to_ewkt}"
+        geom = get_geometry_from_various(geometry="POINT (0.11 1.22)")
+        self.assertEqual(
+            format_metadata(to_format, fieldname=geom),
+            "SRID=4326;POINT (0.1100 1.2200)",
+        )
+
     def test_convert_csv_list(self):
         to_format = "{fieldname#csv_list}"
         self.assertEqual(