Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions pygeofilter/parsers/cql2_json/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ def walk_cql_json(node: JsonType): # noqa: C901
return values.Geometry(node)

elif "bbox" in node:
return values.Envelope(*node["bbox"])
# GeoJSON bbox order is [minx, miny, maxx, maxy] while Envelope is
# (x1, x2, y1, y2) == (minx, maxx, miny, maxy).
b = node["bbox"]
return values.Envelope(b[0], b[2], b[1], b[3])

elif "date" in node:
return parse_date(node["date"])
Expand Down Expand Up @@ -137,10 +140,19 @@ def walk_cql_json(node: JsonType): # noqa: C901
return ast.IsNull(cast(ast.Node, walk_cql_json(args)), not_=False)

elif op == "between":
# OGC 21-065r2 defines BETWEEN with a flat 3-element args array
# [operand, low, high]. The earlier draft nested the bounds as
# [operand, [low, high]]; accept both for compatibility.
if isinstance(args[1], list):
low = cast(ast.ScalarAstType, walk_cql_json(args[1][0]))
high = cast(ast.ScalarAstType, walk_cql_json(args[1][1]))
else:
low = cast(ast.ScalarAstType, walk_cql_json(args[1]))
high = cast(ast.ScalarAstType, walk_cql_json(args[2]))
return ast.Between(
cast(ast.Node, walk_cql_json(args[0])),
cast(ast.ScalarAstType, walk_cql_json(args[1][0])),
cast(ast.ScalarAstType, walk_cql_json(args[1][1])),
low,
high,
not_=False,
)

Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/parsers/cql2_text/grammar.lark
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ DATETIME: /[0-9]{4}-?[0-1][0-9]-?[0-3][0-9][T ][0-2][0-9]:?[0-5][0-9]:?[0-5][0-9
?interval: "INTERVAL" "(" "'" DATETIME "'" "," "'" DATETIME "'" ")"
?date: "DATE" "(" "'" DATE "'" ")"

attribute: /[a-zA-Z][a-zA-Z_:0-9.]+/
attribute: /[a-zA-Z][a-zA-Z_:0-9.]*/
| DOUBLE_QUOTED


Expand Down
28 changes: 28 additions & 0 deletions tests/parsers/cql2_json/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,3 +745,31 @@ def test_function_attr_string_arg():
],
),
)


def test_bbox_envelope_axis_order():
# https://github.com/geopython/pygeofilter/issues/163
# GeoJSON bbox is [minx, miny, maxx, maxy]; Envelope is
# (x1, x2, y1, y2) == (minx, maxx, miny, maxy). The two orderings must
# not be conflated, otherwise longitudes and latitudes are scrambled.
result = parse({"bbox": [-74.1, 40.5, -73.9, 41.0]})
assert result == values.Envelope(-74.1, -73.9, 40.5, 41.0)


def test_bbox_envelope_global():
# The global bbox used by cql2-text and the SQLAlchemy backend.
result = parse({"bbox": [-180.0, -90.0, 180.0, 90.0]})
assert result == values.Envelope(-180.0, 180.0, -90.0, 90.0)


def test_between_flat_args_final_spec():
# https://github.com/geopython/pygeofilter/issues/162
# OGC 21-065r2 defines BETWEEN with a flat 3-element args array.
result = parse({"op": "between", "args": [{"property": "height"}, 2, 5]})
assert result == ast.Between(ast.Attribute("height"), 2, 5, not_=False)


def test_between_nested_args_draft_compat():
# The earlier draft nested the bounds as [operand, [low, high]].
result = parse({"op": "between", "args": [{"property": "height"}, [2, 5]]})
assert result == ast.Between(ast.Attribute("height"), 2, 5, not_=False)
18 changes: 18 additions & 0 deletions tests/parsers/cql2_text/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,21 @@ def test_not_lt():
def test_not_eq():
result = parse("NOT(attr = 2)")
assert result == ast.Not(ast.Equal(ast.Attribute("attr"), 2))


def test_single_letter_attribute():
# https://github.com/geopython/pygeofilter/issues/165
# single-letter property names must parse
result = parse("a = 1")
assert result == ast.Equal(ast.Attribute("a"), 1)

result = parse("x > 5")
assert result == ast.GreaterThan(ast.Attribute("x"), 5)


def test_single_letter_attribute_in_function():
# https://github.com/geopython/pygeofilter/issues/165
result = parse("S_INTERSECTS(g, BBOX(-180, -90, 180, 90))")
assert result == ast.GeometryIntersects(
ast.Attribute("g"), values.Envelope(-180, 180, -90, 90)
)