diff --git a/pygeofilter/parsers/cql2_json/parser.py b/pygeofilter/parsers/cql2_json/parser.py index 5686947..9b77f62 100644 --- a/pygeofilter/parsers/cql2_json/parser.py +++ b/pygeofilter/parsers/cql2_json/parser.py @@ -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"]) @@ -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, ) diff --git a/pygeofilter/parsers/cql2_text/grammar.lark b/pygeofilter/parsers/cql2_text/grammar.lark index 703cb43..47429a9 100644 --- a/pygeofilter/parsers/cql2_text/grammar.lark +++ b/pygeofilter/parsers/cql2_text/grammar.lark @@ -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 diff --git a/tests/parsers/cql2_json/test_parser.py b/tests/parsers/cql2_json/test_parser.py index dd55fc9..7b3910e 100644 --- a/tests/parsers/cql2_json/test_parser.py +++ b/tests/parsers/cql2_json/test_parser.py @@ -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) diff --git a/tests/parsers/cql2_text/test_parser.py b/tests/parsers/cql2_text/test_parser.py index 5bceec7..b85b343 100644 --- a/tests/parsers/cql2_text/test_parser.py +++ b/tests/parsers/cql2_text/test_parser.py @@ -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) + )