From 8f1538131480ccf70342be7a76d5c095503e212e Mon Sep 17 00:00:00 2001 From: Travis Harrison Date: Thu, 8 May 2025 15:10:26 -0500 Subject: [PATCH 1/4] add support for more spatial ops --- CHANGELOG.md | 1 + stac_fastapi/core/pytest.ini | 4 + .../stac_fastapi/core/extensions/filter.py | 27 +++- stac_fastapi/tests/extensions/test_filter.py | 138 ++++++++++++++++++ 4 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 stac_fastapi/core/pytest.ini diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc77391..dc910b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added logging to bulk insertion methods to provide detailed feedback on errors encountered during operations. [#364](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/364) - Introduced the `RAISE_ON_BULK_ERROR` environment variable to control whether bulk insertion methods raise exceptions on errors (`true`) or log warnings and continue processing (`false`). [#364](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/364) - Added code coverage reporting to the test suite using pytest-cov. [#87](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/87) +- Added support for `S_CONTAINS`, `S_WITHIN`, `S_DISJOINT` spatial filter operations [#371](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/371) ### Changed diff --git a/stac_fastapi/core/pytest.ini b/stac_fastapi/core/pytest.ini new file mode 100644 index 00000000..db0353ef --- /dev/null +++ b/stac_fastapi/core/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +testpaths = tests +addopts = -sv +asyncio_mode = auto \ No newline at end of file diff --git a/stac_fastapi/core/stac_fastapi/core/extensions/filter.py b/stac_fastapi/core/stac_fastapi/core/extensions/filter.py index 251614e1..a74eff99 100644 --- a/stac_fastapi/core/stac_fastapi/core/extensions/filter.py +++ b/stac_fastapi/core/stac_fastapi/core/extensions/filter.py @@ -10,7 +10,7 @@ # defines the LIKE, IN, and BETWEEN operators. # Basic Spatial Operators (http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-operators) -# defines the intersects operator (S_INTERSECTS). +# defines spatial operators (S_INTERSECTS, S_CONTAINS, S_WITHIN, S_DISJOINT). # """ import re @@ -82,10 +82,13 @@ class AdvancedComparisonOp(str, Enum): IN = "in" -class SpatialIntersectsOp(str, Enum): - """Enumeration for spatial intersection operator as per CQL2 standards.""" +class SpatialOp(str, Enum): + """Enumeration for spatial operators as per CQL2 standards.""" S_INTERSECTS = "s_intersects" + S_CONTAINS = "s_contains" + S_WITHIN = "s_within" + S_DISJOINT = "s_disjoint" queryables_mapping = { @@ -194,9 +197,23 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]: pattern = cql2_like_to_es(query["args"][1]) return {"wildcard": {field: {"value": pattern, "case_insensitive": True}}} - elif query["op"] == SpatialIntersectsOp.S_INTERSECTS: + elif query["op"] in [ + SpatialOp.S_INTERSECTS, + SpatialOp.S_CONTAINS, + SpatialOp.S_WITHIN, + SpatialOp.S_DISJOINT, + ]: field = to_es_field(query["args"][0]["property"]) geometry = query["args"][1] - return {"geo_shape": {field: {"shape": geometry, "relation": "intersects"}}} + + relation_mapping = { + SpatialOp.S_INTERSECTS: "intersects", + SpatialOp.S_CONTAINS: "contains", + SpatialOp.S_WITHIN: "within", + SpatialOp.S_DISJOINT: "disjoint", + } + + relation = relation_mapping[query["op"]] + return {"geo_shape": {field: {"shape": geometry, "relation": relation}}} return {} diff --git a/stac_fastapi/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py index 3102da34..2a56b953 100644 --- a/stac_fastapi/tests/extensions/test_filter.py +++ b/stac_fastapi/tests/extensions/test_filter.py @@ -481,3 +481,141 @@ async def test_search_filter_extension_isnull_get(app_client, ctx): assert resp.status_code == 200 assert len(resp.json()["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_s_intersects_property(app_client, ctx): + intersecting_geom = { + "coordinates": [150.04, -33.14], + "type": "Point", + } + params = { + "filter": { + "op": "s_intersects", + "args": [ + {"property": "geometry"}, + intersecting_geom, + ], + }, + } + resp = await app_client.post("/search", json=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_s_contains_property(app_client, ctx): + contains_geom = { + "coordinates": [150.04, -33.14], + "type": "Point", + } + params = { + "filter": { + "op": "s_contains", + "args": [ + {"property": "geometry"}, + contains_geom, + ], + }, + } + resp = await app_client.post("/search", json=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_s_within_property(app_client, ctx): + within_geom = { + "coordinates": [[[152.15052873427666, -33.82243006904891], [150.1000346138806, -34.257132625788756], [149.5776607193635, -32.514709769700254], [151.6262528041627, -32.08081674221862], [152.15052873427666, -33.82243006904891]]], + "type": "Polygon", + } + params = { + "filter": { + "op": "s_within", + "args": [ + {"property": "geometry"}, + within_geom, + ], + }, + } + resp = await app_client.post("/search", json=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_s_disjoint_property(app_client, ctx): + intersecting_geom = { + "coordinates": [0, 0], + "type": "Point", + } + params = { + "filter": { + "op": "s_disjoint", + "args": [ + {"property": "geometry"}, + intersecting_geom, + ], + }, + } + resp = await app_client.post("/search", json=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_cql2text_s_intersects_property(app_client, ctx): + filter = 'S_INTERSECTS("geometry",POINT(150.04 -33.14))' + params = { + "filter": filter, + "filter_lang": "cql2-text", + } + resp = await app_client.get("/search", params=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_cql2text_s_contains_property(app_client, ctx): + filter = ( + 'S_CONTAINS("geometry",POINT(150.04 -33.14))' + ) + params = { + "filter": filter, + "filter_lang": "cql2-text", + } + resp = await app_client.get("/search", params=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_cql2text_s_within_property(app_client, ctx): + filter = 'S_WITHIN("geometry",POLYGON((152.15052873427666 -33.82243006904891, 150.1000346138806 -34.257132625788756, 149.5776607193635 -32.514709769700254, 151.6262528041627 -32.08081674221862, 152.15052873427666 -33.82243006904891)))' + params = { + "filter": filter, + "filter_lang": "cql2-text", + } + resp = await app_client.get("/search", params=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_search_filter_extension_cql2text_s_disjoint_property(app_client, ctx): + filter = 'S_DISJOINT("geometry",POINT(0 0))' + params = { + "filter": filter, + "filter_lang": "cql2-text", + } + resp = await app_client.get("/search", params=params) + assert resp.status_code == 200 + resp_json = resp.json() + assert len(resp_json["features"]) == 1 From 49e093e3f946f507ca4f17cb6becdbfb38f6378a Mon Sep 17 00:00:00 2001 From: Travis Harrison Date: Thu, 8 May 2025 21:22:12 -0500 Subject: [PATCH 2/4] fix formatting --- stac_fastapi/tests/extensions/test_filter.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/stac_fastapi/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py index 2a56b953..7ef9a164 100644 --- a/stac_fastapi/tests/extensions/test_filter.py +++ b/stac_fastapi/tests/extensions/test_filter.py @@ -528,7 +528,15 @@ async def test_search_filter_extension_s_contains_property(app_client, ctx): @pytest.mark.asyncio async def test_search_filter_extension_s_within_property(app_client, ctx): within_geom = { - "coordinates": [[[152.15052873427666, -33.82243006904891], [150.1000346138806, -34.257132625788756], [149.5776607193635, -32.514709769700254], [151.6262528041627, -32.08081674221862], [152.15052873427666, -33.82243006904891]]], + "coordinates": [ + [ + [152.15052873427666, -33.82243006904891], + [150.1000346138806, -34.257132625788756], + [149.5776607193635, -32.514709769700254], + [151.6262528041627, -32.08081674221862], + [152.15052873427666, -33.82243006904891], + ] + ], "type": "Polygon", } params = { @@ -582,9 +590,7 @@ async def test_search_filter_extension_cql2text_s_intersects_property(app_client @pytest.mark.asyncio async def test_search_filter_extension_cql2text_s_contains_property(app_client, ctx): - filter = ( - 'S_CONTAINS("geometry",POINT(150.04 -33.14))' - ) + filter = 'S_CONTAINS("geometry",POINT(150.04 -33.14))' params = { "filter": filter, "filter_lang": "cql2-text", From 8bbb5b08f06fdc9382717dadcf6739ed3f2b7714 Mon Sep 17 00:00:00 2001 From: Travis Harrison Date: Fri, 9 May 2025 07:17:05 -0500 Subject: [PATCH 3/4] fix within test --- stac_fastapi/tests/extensions/test_filter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stac_fastapi/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py index 7ef9a164..ae355c3a 100644 --- a/stac_fastapi/tests/extensions/test_filter.py +++ b/stac_fastapi/tests/extensions/test_filter.py @@ -530,11 +530,11 @@ async def test_search_filter_extension_s_within_property(app_client, ctx): within_geom = { "coordinates": [ [ - [152.15052873427666, -33.82243006904891], - [150.1000346138806, -34.257132625788756], - [149.5776607193635, -32.514709769700254], - [151.6262528041627, -32.08081674221862], - [152.15052873427666, -33.82243006904891], + [148.5776607193635, -35.257132625788756], + [153.15052873427666, -35.257132625788756], + [153.15052873427666, -31.080816742218623], + [148.5776607193635, -31.080816742218623], + [148.5776607193635, -35.257132625788756], ] ], "type": "Polygon", @@ -603,7 +603,7 @@ async def test_search_filter_extension_cql2text_s_contains_property(app_client, @pytest.mark.asyncio async def test_search_filter_extension_cql2text_s_within_property(app_client, ctx): - filter = 'S_WITHIN("geometry",POLYGON((152.15052873427666 -33.82243006904891, 150.1000346138806 -34.257132625788756, 149.5776607193635 -32.514709769700254, 151.6262528041627 -32.08081674221862, 152.15052873427666 -33.82243006904891)))' + filter = 'S_WITHIN("geometry",POLYGON((148.5776607193635 -35.257132625788756, 153.15052873427666 -35.257132625788756, 153.15052873427666 -31.080816742218623, 148.5776607193635 -31.080816742218623, 148.5776607193635 -35.257132625788756)))' params = { "filter": filter, "filter_lang": "cql2-text", From 57e75b68885b861917abebb8b31865fd25e080a3 Mon Sep 17 00:00:00 2001 From: Jonathan Healy Date: Sat, 10 May 2025 12:07:41 +0800 Subject: [PATCH 4/4] Move changelog entry to unreleased section --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf5f3ad..5b9d4a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Added configurable landing page ID `STAC_FASTAPI_LANDING_PAGE_ID` [#352](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/352) +- Added support for `S_CONTAINS`, `S_WITHIN`, `S_DISJOINT` spatial filter operations [#371](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/371) ### Changed @@ -22,7 +23,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added logging to bulk insertion methods to provide detailed feedback on errors encountered during operations. [#364](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/364) - Introduced the `RAISE_ON_BULK_ERROR` environment variable to control whether bulk insertion methods raise exceptions on errors (`true`) or log warnings and continue processing (`false`). [#364](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/364) - Added code coverage reporting to the test suite using pytest-cov. [#87](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/87) -- Added support for `S_CONTAINS`, `S_WITHIN`, `S_DISJOINT` spatial filter operations [#371](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/371) ### Changed pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy