Convert coordinates to storage crs when filtering via cql (#1489)

* Allow either URL or URN CRS URIs

* Implemented transformation of geometries in CQL filter

* Fixed flake8 issue

* Removed commented out code

* Implemented support for the filter-crs query parameter

* Remove unneeded print() calls
This commit is contained in:
Ricardo Garcia Silva
2024-01-31 01:16:12 +00:00
committed by GitHub
parent de787b0f22
commit 2d0fc5df3e
6 changed files with 404 additions and 159 deletions
+25
View File
@@ -40,6 +40,15 @@ Using Elasticsearch the following type of queries are supported right now:
* Logical ``and`` query with ``between`` and ``eq`` expression
* Spatial query with ``bbox``
Note that when using a spatial operator in your filter expression, geometries are by default interpreted as being
in the OGC:CRS84 Coordinate Reference System. If you wish to provide geometries in other CRS, use the ``filter-crs``
query parameter with a suitable value.
Alternatively, a geometry's CRS may also be included using Extended Well-Known Text, in which case it will override
the value of ``filter-crs`` (if any) - this can be useful if your filtering expression is complex enough to
need multiple geometries expressed in different CRSs. The standard way of providing ``filter-crs`` as an additional
query parameter is preferable for most cases.
Examples
^^^^^^^^
@@ -93,4 +102,20 @@ A ``CROSSES`` example via an HTTP GET request. The CQL text is passed via the `
curl "http://localhost:5000/collections/hot_osm_waterways/items?f=json&filter=CROSSES(foo_geom,%20LINESTRING(28%20-2,%2030%20-4))"
A ``DWITHIN`` example via HTTP GET and using a custom CRS for the filter geometry:
.. code-block:: bash
curl "http://localhost:5000/collections/beni/items?filter=DWITHIN(geometry,POINT(1392921%205145517),100,meters)&filter-crs=http://www.opengis.net/def/crs/EPSG/0/3857"
The same example, but this time providing a geometry in EWKT format:
.. code-block:: bash
curl "http://localhost:5000/collections/beni/items?filter=DWITHIN(geometry,SRID=3857;POINT(1392921%205145517),100,meters)"
Note that the CQL text has been URL encoded. This is required in curl commands but when entering in a browser, plain text can be used e.g. ``CROSSES(foo_geom, LINESTRING(28 -2, 30 -4))``.
+30 -9
View File
@@ -85,7 +85,8 @@ from pygeoapi.util import (dategetter, RequestedProcessExecutionMode,
json_serial, render_j2_template, str2bool,
TEMPLATES, to_json, get_api_rules, get_base_url,
get_crs_from_uri, get_supported_crs_list,
CrsTransformSpec, transform_bbox)
modify_pygeofilter, CrsTransformSpec,
transform_bbox)
LOGGER = logging.getLogger(__name__)
@@ -1501,7 +1502,7 @@ class API:
reserved_fieldnames = ['bbox', 'bbox-crs', 'crs', 'f', 'lang', 'limit',
'offset', 'resulttype', 'datetime', 'sortby',
'properties', 'skipGeometry', 'q',
'filter', 'filter-lang']
'filter', 'filter-lang', 'filter-crs']
collections = filter_dict_by_key_value(self.config['resources'],
'type', 'collection')
@@ -1714,11 +1715,19 @@ class API:
else:
skip_geometry = False
LOGGER.debug('Processing filter-crs parameter')
filter_crs_uri = request.params.get('filter-crs', DEFAULT_CRS)
LOGGER.debug('processing filter parameter')
cql_text = request.params.get('filter')
if cql_text is not None:
try:
filter_ = parse_ecql_text(cql_text)
filter_ = modify_pygeofilter(
filter_,
filter_crs_uri=filter_crs_uri,
storage_crs_uri=provider_def.get('storage_crs'),
geometry_column_name=provider_def.get('geom_field'),
)
except Exception as err:
LOGGER.error(err)
msg = f'Bad CQL string : {cql_text}'
@@ -1736,7 +1745,6 @@ class API:
return self.get_exception(
HTTPStatus.BAD_REQUEST, headers, request.format,
'InvalidParameterValue', msg)
# Get provider locale (if any)
prv_locale = l10n.get_plugin_locale(provider_def, request.raw_locale)
@@ -1755,7 +1763,9 @@ class API:
LOGGER.debug(f'language: {prv_locale}')
LOGGER.debug(f'q: {q}')
LOGGER.debug(f'cql_text: {cql_text}')
LOGGER.debug(f'filter_: {filter_}')
LOGGER.debug(f'filter-lang: {filter_lang}')
LOGGER.debug(f'filter-crs: {filter_crs_uri}')
try:
content = p.query(offset=offset, limit=limit,
@@ -1925,7 +1935,7 @@ class API:
reserved_fieldnames = ['bbox', 'f', 'limit', 'offset',
'resulttype', 'datetime', 'sortby',
'properties', 'skipGeometry', 'q',
'filter-lang']
'filter-lang', 'filter-crs']
collections = filter_dict_by_key_value(self.config['resources'],
'type', 'collection')
@@ -2012,19 +2022,21 @@ class API:
LOGGER.debug('Loading provider')
try:
p = load_plugin('provider', get_provider_by_type(
collections[dataset]['providers'], 'feature'))
provider_def = get_provider_by_type(
collections[dataset]['providers'], 'feature')
except ProviderTypeError:
try:
p = load_plugin('provider', get_provider_by_type(
collections[dataset]['providers'], 'record'))
provider_def = get_provider_by_type(
collections[dataset]['providers'], 'record')
except ProviderTypeError:
msg = 'Invalid provider type'
return self.get_exception(
HTTPStatus.BAD_REQUEST, headers, request.format,
'NoApplicableCode', msg)
try:
p = load_plugin('provider', provider_def)
except ProviderGenericError as err:
LOGGER.error(err)
return self.get_exception(
err.http_status_code, headers, request.format,
err.ogc_exception_code, err.message)
@@ -2086,6 +2098,8 @@ class API:
else:
skip_geometry = False
LOGGER.debug('Processing filter-crs parameter')
filter_crs = request.params.get('filter-crs', DEFAULT_CRS)
LOGGER.debug('Processing filter-lang parameter')
filter_lang = request.params.get('filter-lang')
if filter_lang != 'cql-json': # @TODO add check from the configuration
@@ -2105,6 +2119,7 @@ class API:
LOGGER.debug(f'skipGeometry: {skip_geometry}')
LOGGER.debug(f'q: {q}')
LOGGER.debug(f'filter-lang: {filter_lang}')
LOGGER.debug(f'filter-crs: {filter_crs}')
LOGGER.debug('Processing headers')
@@ -2142,6 +2157,12 @@ class API:
LOGGER.debug('processing PostgreSQL CQL_JSON data')
try:
filter_ = parse_cql_json(data)
filter_ = modify_pygeofilter(
filter_,
filter_crs_uri=filter_crs,
storage_crs_uri=provider_def.get('storage_crs'),
geometry_column_name=provider_def.get('geom_field')
)
except Exception as err:
LOGGER.error(err)
msg = f'Bad CQL string : {data}'
+1 -40
View File
@@ -55,7 +55,6 @@ from geoalchemy2 import Geometry # noqa - this isn't used explicitly but is nee
from geoalchemy2.functions import ST_MakeEnvelope
from geoalchemy2.shape import to_shape
from pygeofilter.backends.sqlalchemy.evaluate import to_filter
import pygeofilter.ast
import pyproj
import shapely
from sqlalchemy import create_engine, MetaData, PrimaryKeyConstraint, asc, desc
@@ -139,8 +138,7 @@ class PostgreSQLProvider(BaseProvider):
LOGGER.debug('Preparing filters')
property_filters = self._get_property_filters(properties)
modified_filterq = self._modify_pygeofilter(filterq)
cql_filters = self._get_cql_filters(modified_filterq)
cql_filters = self._get_cql_filters(filterq)
bbox_filter = self._get_bbox_filter(bbox)
order_by_clauses = self._get_order_by_clauses(sortby, self.table_model)
selected_properties = self._select_properties_clause(select_properties,
@@ -497,40 +495,3 @@ class PostgreSQLProvider(BaseProvider):
else:
crs_transform = None
return crs_transform
def _modify_pygeofilter(
self,
ast_tree: pygeofilter.ast.Node,
) -> pygeofilter.ast.Node:
"""
Prepare the input pygeofilter for querying the database.
Returns a new ``pygeofilter.ast.Node`` object that can be used for
querying the database.
"""
new_tree = deepcopy(ast_tree)
_inplace_replace_geometry_filter_name(new_tree, self.geom)
return new_tree
def _inplace_replace_geometry_filter_name(
node: pygeofilter.ast.Node,
geometry_column_name: str
):
"""Recursively traverse node tree and rename nodes of type ``Attribute``.
Nodes of type ``Attribute`` named ``geometry`` are renamed to the value of
the ``geometry_column_name`` parameter.
"""
try:
sub_nodes = node.get_sub_nodes()
except AttributeError:
pass
else:
for sub_node in sub_nodes:
is_attribute_node = isinstance(sub_node, pygeofilter.ast.Attribute)
if is_attribute_node and sub_node.name == "geometry":
sub_node.name = geometry_column_name
else:
_inplace_replace_geometry_filter_name(
sub_node, geometry_column_name)
+148 -29
View File
@@ -30,6 +30,7 @@
"""Generic util functions used in the code"""
import base64
from copy import deepcopy
from filelock import FileLock
import json
import logging
@@ -44,7 +45,7 @@ from decimal import Decimal
from enum import Enum
import pathlib
from pathlib import Path
from typing import Any, IO, Union, List, Callable
from typing import Any, IO, Union, List, Optional, Callable
from urllib.parse import urlparse
from urllib.request import urlopen
@@ -66,6 +67,8 @@ from shapely.geometry import (
import yaml
from babel.support import Translations
from jinja2 import Environment, FileSystemLoader, select_autoescape
import pygeofilter.ast
import pygeofilter.values
import pyproj
from pyproj.exceptions import CRSError
from requests import Session
@@ -84,21 +87,6 @@ DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
THISDIR = Path(__file__).parent.resolve()
TEMPLATES = THISDIR / 'templates'
CRS_AUTHORITY = [
"AUTO",
"EPSG",
"OGC",
]
# Global to compile only once
CRS_URI_PATTERN = re.compile(
(
rf"^http://www.opengis\.net/def/crs/"
rf"(?P<auth>{'|'.join(CRS_AUTHORITY)})/"
rf"[\d|\.]+?/(?P<code>\w+?)$"
)
)
# Type for Shapely geometrical objects.
GeomObject = Union[
@@ -686,7 +674,9 @@ def get_crs_from_uri(uri: str) -> pyproj.CRS:
Author: @MTachon
:param uri: Uniform resource identifier of the coordinate
reference system.
reference system. In accordance with
https://docs.ogc.org/pol/09-048r5.html#_naming_rule URIs can
take either the form of a URL or a URN
:type uri: str
:raises `CRSError`: Error raised if no CRS could be identified from the
@@ -696,23 +686,26 @@ def get_crs_from_uri(uri: str) -> pyproj.CRS:
:rtype: `pyproj.CRS`
"""
# normalize the input `uri` to a URL first
url = uri.replace(
"urn:ogc:def:crs",
"http://www.opengis.net/def/crs"
).replace(":", "/")
try:
crs = pyproj.CRS.from_authority(*CRS_URI_PATTERN.search(uri).groups())
except CRSError:
authority, code = url.rsplit("/", maxsplit=3)[1::2]
crs = pyproj.CRS.from_authority(authority, code)
except ValueError:
msg = (
f"CRS could not be identified from URI {uri!r} "
f"(Authority: {CRS_URI_PATTERN.search(uri).group('auth')!r}, "
f"Code: {CRS_URI_PATTERN.search(uri).group('code')!r})."
f"CRS could not be identified from URI {uri!r}. CRS URIs must "
"follow one of two formats: "
"'http://www.opengis.net/def/crs/{authority}/{version}/{code}' or "
"'urn:ogc:def:crs:{authority}:{version}:{code}' "
"(see https://docs.opengeospatial.org/is/18-058r1/18-058r1.html#crs-overview)." # noqa
)
LOGGER.error(msg)
raise CRSError(msg)
except AttributeError:
msg = (
f"CRS could not be identified from URI {uri!r}. CRS URIs must "
"follow the format "
"'http://www.opengis.net/def/crs/{authority}/{version}/{code}' "
"(see https://docs.opengeospatial.org/is/18-058r1/18-058r1.html#crs-overview)." # noqa
)
except CRSError:
msg = f"CRS could not be identified from URI {uri!r}"
LOGGER.error(msg)
raise CRSError(msg)
else:
@@ -886,3 +879,129 @@ def bbox2geojsongeometry(bbox: list) -> dict:
b = box(*bbox, ccw=False)
return geom_to_geojson(b)
def modify_pygeofilter(
ast_tree: pygeofilter.ast.Node,
*,
filter_crs_uri: str,
storage_crs_uri: Optional[str] = None,
geometry_column_name: Optional[str] = None
) -> pygeofilter.ast.Node:
"""
Modifies the input pygeofilter with information from the provider.
:param ast_tree: `pygeofilter.ast.Node` representing the
already parsed pygeofilter expression
:param filter_crs_uri: URI of the CRS being used in the filtering
expression
:param storage_crs_uri: An optional string containing the URI of
the provider's storage CRS
:param geometry_column_name: An optional string containing the
actual name of the provider's geometry field
:returns: A new pygeofilter.ast.Node, with the modified filter
expression
This function modifies the parsed pygeofilter that contains the raw
filter expression provided by an external client. It performs the
following modifications:
- if the filter includes any spatial coordinates and they are being
provided in a different CRS from the provider's storage CRS, the
corresponding geometries are transformed into the storage CRS
- if the filter includes the generic 'geometry' name as a reference to
the actual geometry of features, it is replaced by the actual name
of the geometry field, as specified by the provider
"""
new_tree = deepcopy(ast_tree)
if storage_crs_uri:
storage_crs = get_crs_from_uri(storage_crs_uri)
filter_crs = get_crs_from_uri(filter_crs_uri)
_inplace_transform_filter_geometries(new_tree, filter_crs, storage_crs)
if geometry_column_name:
_inplace_replace_geometry_filter_name(new_tree, geometry_column_name)
return new_tree
def _inplace_transform_filter_geometries(
node: pygeofilter.ast.Node,
filter_crs: pyproj.CRS,
storage_crs: pyproj.CRS
):
"""
Recursively traverse node tree and convert coordinates to the storage CRS.
This function modifies nodes in the already-parsed filter in order to find
any geometry literals that may be used in the filter and, if necessary,
proceeds to convert spatial coordinates to the CRS used by the provider.
"""
try:
sub_nodes = node.get_sub_nodes()
except AttributeError:
pass
else:
for sub_node in sub_nodes:
is_geometry_node = isinstance(
sub_node, pygeofilter.values.Geometry)
if is_geometry_node:
# NOTE1: To be flexible, and since pygeofilter
# already supports it, in addition to supporting
# the `filter-crs` parameter, we also support having a
# geometry defined in EWKT, meaning the CRS is provided
# inline, like this `SRID=<CRS_CODE>;<WKT>` - If provided,
# this overrides the value of `filter-crs`. This enables
# supporting, for example, an exotic filter expression with
# multiple geometries specified in different CRSs
# NOTE2: We specify a default CRS using a URI of type URN
# because this is what pygeofilter uses internally too
crs_urn_provided_in_ewkt = sub_node.geometry.get(
'crs', {}).get('properties', {}).get('name')
if crs_urn_provided_in_ewkt is not None:
crs = get_crs_from_uri(crs_urn_provided_in_ewkt)
else:
crs = filter_crs
if crs != storage_crs:
# convert geometry coordinates to storage crs
geom = geojson_to_geom(sub_node.geometry)
coord_transformer = pyproj.Transformer.from_crs(
crs_from=crs, crs_to=storage_crs).transform
transformed_geom = ops.transform(coord_transformer, geom)
sub_node.geometry = geom_to_geojson(transformed_geom)
# ensure the crs is encoded in the sub-node, otherwise
# pygeofilter will assign it its own default CRS
authority, code = storage_crs.to_authority()
sub_node.geometry['crs'] = {
'properties': {
'name': f'urn:ogc:def:crs:{authority}::{code}'
}
}
else:
_inplace_transform_filter_geometries(
sub_node, filter_crs, storage_crs)
def _inplace_replace_geometry_filter_name(
node: pygeofilter.ast.Node,
geometry_column_name: str
):
"""Recursively traverse node tree and rename nodes of type ``Attribute``.
Nodes of type ``Attribute`` named ``geometry`` are renamed to the value of
the ``geometry_column_name`` parameter.
"""
try:
sub_nodes = node.get_sub_nodes()
except AttributeError:
pass
else:
for sub_node in sub_nodes:
is_attribute_node = isinstance(sub_node, pygeofilter.ast.Attribute)
if is_attribute_node and sub_node.name == "geometry":
sub_node.name = geometry_column_name
else:
_inplace_replace_geometry_filter_name(
sub_node, geometry_column_name)
-68
View File
@@ -44,9 +44,7 @@ import pytest
import pyproj
from http import HTTPStatus
import pygeofilter.ast
from pygeofilter.parsers.ecql import parse
from pygeofilter.values import Geometry
from pygeoapi.api import API
@@ -750,69 +748,3 @@ def test_get_collection_items_postgresql_automap_naming_conflicts(pg_api_):
assert code == HTTPStatus.OK
features = json.loads(response).get('features')
assert len(features) == 0
@pytest.mark.parametrize('original_filter, expected', [
pytest.param(
"INTERSECTS(geometry, POINT(1 1))",
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
id='unnested-geometry'
),
pytest.param(
"some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))",
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
),
id='nested-geometry'
),
pytest.param(
"(some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))) OR "
"DWITHIN(geometry, POINT(2 2), 10, meters)",
pygeofilter.ast.Or(
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
),
pygeofilter.ast.DistanceWithin(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (2, 2)}),
distance=10,
units='meters',
)
),
id='complex-filter'
),
])
def test_modify_pygeofilter(original_filter, expected):
class _CustomPostgreSqlProvider(PostgreSQLProvider):
"""This is a subclass of the original PostgreSQLProvider.
The current test is only interested in verifying the correctness of
the logic that modifies the parsed filter. As such, in order
to simplify instantiating the postgresql pygeoapi provider, and
in order to avoid dealing with mocking out the sqlalchemy table
reflection mechanism, this class overrides the __init__() method
and can be used to test the implementation of the base class'
`self._modify_pygeofilter()` method, which is really all we want
to test here.
"""
def __init__(self):
self.geom = 'custom_geom_name'
provider = _CustomPostgreSqlProvider()
parsed_filter = parse(original_filter)
result = provider._modify_pygeofilter(parsed_filter)
assert result == expected
+200 -13
View File
@@ -29,10 +29,14 @@
from datetime import datetime, date, time
from decimal import Decimal
from contextlib import nullcontext as does_not_raise
from copy import deepcopy
import pytest
from pyproj.exceptions import CRSError
import pygeofilter.ast
from pygeofilter.parsers.ecql import parse
from pygeofilter.values import Geometry
from shapely.geometry import Point
from pygeoapi import util
@@ -267,19 +271,28 @@ def test_get_supported_crs_list():
assert DUTCH_CRS in crs_list
def test_get_crs_from_uri():
with pytest.raises(CRSError):
util.get_crs_from_uri('http://www.opengis.net/not/a/valid/crs/uri')
with pytest.raises(CRSError):
util.get_crs_from_uri('http://www.opengis.net/def/crs/EPSG/0/0')
CRS_DICT = {
'http://www.opengis.net/def/crs/OGC/1.3/CRS84': 'OGC:CRS84',
'http://www.opengis.net/def/crs/EPSG/0/4326': 'EPSG:4326',
'http://www.opengis.net/def/crs/EPSG/0/28992': 'EPSG:28992'
}
for key in CRS_DICT:
crs_obj = util.get_crs_from_uri(key)
assert crs_obj.srs == CRS_DICT[key]
@pytest.mark.parametrize('uri, expected_raise, expected', [
pytest.param('http://www.opengis.net/not/a/valid/crs/uri', pytest.raises(CRSError), None), # noqa
pytest.param('http://www.opengis.net/def/crs/EPSG/0/0', pytest.raises(CRSError), None), # noqa
pytest.param('http://www.opengis.net/def/crs/OGC/1.3/CRS84', does_not_raise(), 'OGC:CRS84'), # noqa
pytest.param('http://www.opengis.net/def/crs/EPSG/0/4326', does_not_raise(), 'EPSG:4326'), # noqa
pytest.param('http://www.opengis.net/def/crs/EPSG/0/28992', does_not_raise(), 'EPSG:28992'), # noqa
pytest.param('urn:ogc:def:crs:not:a:valid:crs:urn', pytest.raises(CRSError), None), # noqa
pytest.param('urn:ogc:def:crs:epsg:0:0', pytest.raises(CRSError), None),
pytest.param('urn:ogc:def:crs:epsg::0', pytest.raises(CRSError), None),
pytest.param('urn:ogc:def:crs:OGC::0', pytest.raises(CRSError), None),
pytest.param('urn:ogc:def:crs:OGC:0:0', pytest.raises(CRSError), None),
pytest.param('urn:ogc:def:crs:OGC:0:CRS84', does_not_raise(), "OGC:CRS84"),
pytest.param('urn:ogc:def:crs:OGC::CRS84', does_not_raise(), "OGC:CRS84"),
pytest.param('urn:ogc:def:crs:EPSG:0:4326', does_not_raise(), "EPSG:4326"),
pytest.param('urn:ogc:def:crs:EPSG::4326', does_not_raise(), "EPSG:4326"),
pytest.param('urn:ogc:def:crs:epsg:0:4326', does_not_raise(), "EPSG:4326"),
pytest.param('urn:ogc:def:crs:epsg:0:28992', does_not_raise(), "EPSG:28992"), # noqa
])
def test_get_crs_from_uri(uri, expected_raise, expected):
with expected_raise:
crs = util.get_crs_from_uri(uri)
assert crs.srs.upper() == expected
def test_transform_bbox():
@@ -316,3 +329,177 @@ def test_prefetcher():
headers = prefetcher.get_headers(headers['location'])
assert int(headers.get('content-length', 0)) == length
assert headers.get('content-type') == 'image/png'
@pytest.mark.parametrize('original_filter, filter_crs, storage_crs, geometry_colum_name, expected', [ # noqa
pytest.param(
'INTERSECTS(geometry, POINT(1 1))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
None,
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
id='passthrough'
),
pytest.param(
"INTERSECTS(geometry, POINT(1 1))",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
'custom_geom_name',
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
id='unnested-geometry-name'
),
pytest.param(
"some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
'custom_geom_name',
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
),
id='nested-geometry-name'
),
pytest.param(
"(some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))) OR "
"DWITHIN(geometry, POINT(2 2), 10, meters)",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
'custom_geom_name',
pygeofilter.ast.Or(
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
),
pygeofilter.ast.DistanceWithin(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (2, 2)}),
distance=10,
units='meters',
)
),
id='complex-filter-name'
),
pytest.param(
"INTERSECTS(geometry, POINT(12.512829 41.896698))",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2313682.387730346, 4641308.550187246)}) # noqa
),
id='unnested-geometry-transformed-coords'
),
pytest.param(
"some_attribute = 10 AND INTERSECTS(geometry, POINT(12.512829 41.896698))", # noqa
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2313682.387730346, 4641308.550187246)}) # noqa
),
),
id='nested-geometry-transformed-coords'
),
pytest.param(
"(some_attribute = 10 AND INTERSECTS(geometry, POINT(12.512829 41.896698))) OR " # noqa
"DWITHIN(geometry, POINT(12 41), 10, meters)",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
pygeofilter.ast.Or(
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2313682.387730346, 4641308.550187246)}) # noqa
),
),
pygeofilter.ast.DistanceWithin(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2267681.8892602, 4543101.513292163)}), # noqa
distance=10,
units='meters',
)
),
id='complex-filter-transformed-coords'
),
pytest.param(
"INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2313681.8086284213, 4641307.939955416)}) # noqa
),
id='unnested-geometry-transformed-coords-explicit-input-crs-ewkt'
),
pytest.param(
"INTERSECTS(geometry, POINT(1392921 5145517))",
'http://www.opengis.net/def/crs/EPSG/0/3857',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2313681.8086284213, 4641307.939955416)}) # noqa
),
id='unnested-geometry-transformed-coords-explicit-input-crs-filter-crs'
),
pytest.param(
"INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2313681.8086284213, 4641307.939955416)}) # noqa
),
id='unnested-geometry-transformed-coords-ewkt-crs-overrides-filter-crs'
),
pytest.param(
"INTERSECTS(geometry, POINT(12.512829 41.896698))",
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
'custom_geom_name',
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (2313682.387730346, 4641308.550187246)}) # noqa
),
id='unnested-geometry-name-and-transformed-coords'
),
])
def test_modify_pygeofilter(
original_filter,
filter_crs,
storage_crs,
geometry_colum_name,
expected
):
parsed_filter = parse(original_filter)
result = util.modify_pygeofilter(
parsed_filter,
filter_crs_uri=filter_crs,
storage_crs_uri=storage_crs,
geometry_column_name=geometry_colum_name
)
assert result == expected