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
8 changes: 8 additions & 0 deletions pygeofilter/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ def get_template(self) -> str:
# interval T2,then the beginning of T1 is after the end of T2, or the end of
# T1 is before the beginning of T2, i.e. the intervals do not overlap in any
# way, but their ordering relationship is not known.
# TINTERSECTS: The union of all other temporal relationships; two temporal
# instances intersect if they are not disjoint.

# https://github.com/geotools/geotools/blob/main/modules/library/cql/ECQL.md#temporal-predicate
# BEFORE_OR_DURING <----->
Expand All @@ -329,6 +331,7 @@ class TemporalComparisonOp(Enum):
METBY = "METBY"
TOVERLAPS = "TOVERLAPS"
OVERLAPPEDBY = "OVERLAPPEDBY"
TINTERSECTS = "TINTERSECTS"

BEFORE_OR_DURING = "BEFORE OR DURING"
DURING_OR_AFTER = "DURING OR AFTER"
Expand Down Expand Up @@ -420,6 +423,11 @@ class TimeOverlappedBy(TemporalPredicate):


@dataclass
@dataclass
class TimeIntersects(TemporalPredicate):
op: ClassVar[TemporalComparisonOp] = TemporalComparisonOp.TINTERSECTS


class TimeBeforeOrDuring(TemporalPredicate):
op: ClassVar[TemporalComparisonOp] = TemporalComparisonOp.BEFORE_OR_DURING

Expand Down
15 changes: 8 additions & 7 deletions pygeofilter/cql2.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@
"t_before": ast.TimeBefore,
"t_after": ast.TimeAfter,
"t_meets": ast.TimeMeets,
"t_metby": ast.TimeMetBy,
"t_metBy": ast.TimeMetBy,
"t_overlaps": ast.TimeOverlaps,
"t_overlappedby": ast.TimeOverlappedBy,
"t_begins": ast.TimeBegins,
"t_begunby": ast.TimeBegunBy,
"t_overlappedBy": ast.TimeOverlappedBy,
"t_starts": ast.TimeBegins,
"t_startedBy": ast.TimeBegunBy,
"t_during": ast.TimeDuring,
"t_contains": ast.TimeContains,
"t_ends": ast.TimeEnds,
"t_endedby": ast.TimeEndedBy,
"t_finishes": ast.TimeEnds,
"t_finishedBy": ast.TimeEndedBy,
"t_equals": ast.TimeEquals,
"t_intersects": ast.TimeOverlaps,
"t_intersects": ast.TimeIntersects,
"t_disjoint": ast.TimeDisjoint,
}


Expand Down
16 changes: 10 additions & 6 deletions pygeofilter/parsers/cql2_text/grammar.lark
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,23 @@
| temporal_predicate


?temporal_predicate: expression _binary_temporal_predicate_func expression -> binary_temporal_predicate
?temporal_predicate: _binary_temporal_predicate_func "(" expression "," expression ")" -> binary_temporal_predicate

!_binary_temporal_predicate_func: "T_BEFORE"i
| "T_AFTER"i
| "T_MEETS"i
| "T_METBY"i
| "T_OVERLAPS"i
| "T_OVERLAPPEDBY"i
| "T_BEGINS"i
| "T_BEGUNBY"i
| "T_STARTS"i
| "T_STARTEDBY"i
| "T_DURING"i
| "T_CONTAINS"i
| "T_ENDS"i
| "T_ENDEDBY"i
| "T_FINISHES"i
| "T_FINISHEDBY"i
| "T_EQUALS"i
| "T_INTERSECTS"i
| "T_DISJOINT"i


?spatial_predicate: _binary_spatial_predicate_func "(" expression "," expression ")" -> binary_spatial_predicate
Expand Down Expand Up @@ -140,7 +141,10 @@ SINGLE_QUOTED: "'" /.*?/ "'"
DATE: /[0-9]{4}-?[0-1][0-9]-?[0-3][0-9]/
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](\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?/
?timestamp: "TIMESTAMP" "(" "'" DATETIME "'" ")"
?interval: "INTERVAL" "(" "'" DATETIME "'" "," "'" DATETIME "'" ")"
?interval: "INTERVAL" "(" instant_parameter "," instant_parameter ")"
instant_parameter: "'" DATETIME "'" -> instant_datetime
| "'" DATE "'" -> instant_date
| "'" ".." "'" -> instant_open
?date: "DATE" "(" "'" DATE "'" ")"

attribute: /[a-zA-Z][a-zA-Z_:0-9.]+/
Expand Down
18 changes: 15 additions & 3 deletions pygeofilter/parsers/cql2_text/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,12 @@ def binary_spatial_predicate(self, op, lhs, rhs):
op = op.lower()
return SPATIAL_PREDICATES_MAP[op](lhs, rhs)

def binary_temporal_predicate(self, lhs, op, rhs):
op = op.lower()
return TEMPORAL_PREDICATES_MAP[op](lhs, rhs)
def binary_temporal_predicate(self, op, lhs, rhs):
op_lower = op.lower()
for key, cls in TEMPORAL_PREDICATES_MAP.items():
if key.lower() == op_lower:
return cls(lhs, rhs)
raise ValueError(f"Unknown temporal predicate: {op}")

def relate_spatial_predicate(self, lhs, rhs, pattern):
return ast.Relate(lhs, rhs, pattern)
Expand Down Expand Up @@ -193,6 +196,15 @@ def geometry(self, value):
def bbox(self, x1, y1, x2, y2):
return values.Envelope(x1, x2, y1, y2)

def instant_datetime(self, value):
return value

def instant_date(self, value):
return value

def instant_open(self):
return None

def interval(self, start, end):
return values.Interval(start, end)

Expand Down
2 changes: 1 addition & 1 deletion tests/parsers/cql2_json/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"json": "{\"filter-lang\": \"cql2-json\", \"filter\": {\"op\": \"=\", \"args\": [{\"property\": \"prop1\"}, {\"property\": \"prop2\"}]}}"
},
"Example 6": {
"text": "filter=datetime T_INTERSECTS INTERVAL('2020-11-11T00:00:00Z', '2020-11-12T00:00:00Z')",
"text": "filter=T_INTERSECTS(datetime, INTERVAL('2020-11-11T00:00:00Z', '2020-11-12T00:00:00Z'))",
"json": "{\"filter-lang\": \"cql2-json\", \"filter\": {\"op\": \"t_intersects\", \"args\": [{\"property\": \"datetime\"}, {\"interval\": [\"2020-11-11T00:00:00Z\", \"2020-11-12T00:00:00Z\"]}]}}"
},
"Example 7": {
Expand Down
4 changes: 2 additions & 2 deletions tests/parsers/cql2_json/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def test_meets_dt_dr():
def test_attribute_metby_dr_dt():
result = parse(
{
"op": "t_metby",
"op": "t_metBy",
"args": [
{"property": "attr"},
{"interval": ["PT4S", "2000-01-01T00:00:03Z"]},
Expand Down Expand Up @@ -278,7 +278,7 @@ def test_attribute_toverlaps_open_dt():
def test_attribute_overlappedby_dt_open():
result = parse(
{
"op": "t_overlappedby",
"op": "t_overlappedBy",
"args": [
{"property": "attr"},
{"interval": ["2000-01-01T00:00:03Z", ".."]},
Expand Down
51 changes: 41 additions & 10 deletions tests/parsers/cql2_text/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,7 @@ def test_attribute_is_null():


def test_attribute_before():
# Using TIMESTAMP function to properly wrap the timestamp
result = parse("attr T_BEFORE TIMESTAMP('2000-01-01T00:00:01Z')")
result = parse("T_BEFORE(attr, TIMESTAMP('2000-01-01T00:00:01Z'))")
assert result == ast.TimeBefore(
ast.Attribute("attr"),
datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))),
Expand All @@ -222,11 +221,10 @@ def test_attribute_lt_date():
)

def test_attribute_t_intersects():
# Using INTERVAL function with properly quoted timestamps
result = parse(
"attr T_INTERSECTS INTERVAL('2000-01-01T00:00:00Z', '2000-01-01T00:00:01Z')"
"T_INTERSECTS(attr, INTERVAL('2000-01-01T00:00:00Z', '2000-01-01T00:00:01Z'))"
)
assert result == ast.TimeOverlaps(
assert result == ast.TimeIntersects(
ast.Attribute("attr"),
values.Interval(
datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))),
Expand All @@ -235,15 +233,48 @@ def test_attribute_t_intersects():
)


def test_attribute_tintersects_dt_dr():
def test_t_disjoint():
result = parse(
"attr T_INTERSECTS INTERVAL('2000-01-01T00:00:03Z', '2000-01-01T00:00:04Z')"
"T_DISJOINT(attr, TIMESTAMP('2000-01-01T00:00:00Z'))"
)
assert result == ast.TimeOverlaps(
assert result == ast.TimeDisjoint(
ast.Attribute("attr"),
datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))),
)


def test_t_starts():
result = parse(
"T_STARTS(attr, INTERVAL('2000-01-01T00:00:00Z', '2000-01-01T00:00:01Z'))"
)
assert result == ast.TimeBegins(
ast.Attribute("attr"),
values.Interval(
datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))),
datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))),
),
)


def test_interval_with_date():
result = parse(
"T_DURING(attr, INTERVAL('2000-01-01', '2001-01-01'))"
)
assert result == ast.TimeDuring(
ast.Attribute("attr"),
values.Interval(date(2000, 1, 1), date(2001, 1, 1)),
)


def test_interval_open_ended():
result = parse(
"T_DURING(attr, INTERVAL('..', '2001-01-01T00:00:00Z'))"
)
assert result == ast.TimeDuring(
ast.Attribute("attr"),
values.Interval(
datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))),
datetime(2000, 1, 1, 0, 0, 4, tzinfo=StaticTzInfo("Z", timedelta(0))),
None,
datetime(2001, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))),
),
)

Expand Down
Loading