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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "wlts.py"
version="1.3.1"
version="1.4.0"
description = "."
readme = "README.rst"
requires-python = ">=3.8"
Expand Down
135 changes: 135 additions & 0 deletions wlts/allen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#
# This file is part of Python Client Library for the WLTS.
# Copyright (C) 2025 INPE.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
#
"""Python Client Library for WLTS.

This module introduces a class named ``WLTS`` that can be used to retrieve
trajectories for a given location.
"""

import pandas as pd


def before_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""The function represets a < b."""
if (a["date"].max() < b["date"].min()):
return a
return pd.DataFrame(columns=a.columns)


def after_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""The function represets a > b."""
if (a["date"].min() > b["date"].max()):
return a
return pd.DataFrame(columns=a.columns)


def equals_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""The function represets a == b."""
if set(a["date"]) & set(b["date"]):
return a[a["date"].isin(b["date"])]
return pd.DataFrame(columns=a.columns)

def meets_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a meets b (a == b - 1)."""
if any(a["date"].isin(b["date"] - pd.Timedelta(days=1))):
return a
return pd.DataFrame(columns=a.columns)


def met_by_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a met_by b (a == b + 1)."""
if any(a["date"].isin(b["date"] + pd.Timedelta(days=1))):
return a
return pd.DataFrame(columns=a.columns)


def overlaps_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a overlaps b (a <= b & a + 1 >= b)."""
if any((a["date"].min() <= b["date"]) & ((a["date"].max() + pd.Timedelta(days=1)) >= b["date"])):
return a
return pd.DataFrame(columns=a.columns)


def overlapped_by_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a overlapped_by b (a <= b & a + 1 <= b)."""
if any((a["date"].min() <= b["date"]) & ((a["date"].max() + pd.Timedelta(days=1)) <= b["date"])):
return a
return pd.DataFrame(columns=a.columns)


def during_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a during b (a >= b & a + 1 <= b)."""
if (a["date"].min() >= b["date"].min()) and ((a["date"].max() + pd.Timedelta(days=1)) <= b["date"].max()):
return a
return pd.DataFrame(columns=a.columns)


def contains_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a contains b (a <= b & a + 1 >= b)."""
if (a["date"].min() <= b["date"].min()) and ((a["date"].max() + pd.Timedelta(days=1)) >= b["date"].max()):
return a
return pd.DataFrame(columns=a.columns)


def starts_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a starts b (a == b & a + 1 <= b)."""
if (a["date"].min() == b["date"].min()) and ((a["date"].max() + pd.Timedelta(days=1)) <= b["date"].max()):
return a
return pd.DataFrame(columns=a.columns)


def started_by_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a started_by b (a == b & a + 1 >= b)."""
if (a["date"].min() == b["date"].min()) and ((a["date"].max() + pd.Timedelta(days=1)) >= b["date"].max()):
return a
return pd.DataFrame(columns=a.columns)


def finishes_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a finishes b (a <= b & a + 1 == b)."""
if (a["date"].min() <= b["date"].min()) and ((a["date"].max() + pd.Timedelta(days=1)) == b["date"].max()):
return a
return pd.DataFrame(columns=a.columns)


def finished_by_relation(a: pd.DataFrame, b: pd.DataFrame) -> pd.DataFrame:
"""Represents a finished_by b (a <= b & a + 1 == b)."""
if (a["date"].min() <= b["date"].min()) and ((a["date"].max() + pd.Timedelta(days=1)) == b["date"].max()):
return a
return pd.DataFrame(columns=a.columns)



# TODO: add other relations (RECUR, CONVERT and EVOLVE)
ALLEN_RELATIONS = {
"before": before_relation,
"after": after_relation,
"equals": equals_relation,
"meets": meets_relation,
"met_by": met_by_relation,
"overlaps": overlaps_relation,
"overlapped_by": overlapped_by_relation,
"during": during_relation,
"contains": contains_relation,
"starts": starts_relation,
"started_by": started_by_relation,
"finishes": finishes_relation,
"finished_by": finished_by_relation,
}


100 changes: 96 additions & 4 deletions wlts/wlts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
trajectories for a given location.
"""
import json
from typing import Dict
from typing import Dict, List, Optional

import httpx
import lccs
import numpy as np
import pandas as pd
import requests

from .allen import ALLEN_RELATIONS
from .collection import Collections
from .trajectories import Trajectories
from .trajectory import Trajectory
Expand Down Expand Up @@ -384,7 +386,7 @@ def plot(self, dataframe, **parameters):
collections=list(df["collection"].unique())
)

# --- Scatter ---
# --- Scatter --- #
if parameters["type"] == "scatter":
if len(df.point_id.unique()) == 1:
df["label"] = (
Expand Down Expand Up @@ -424,7 +426,7 @@ def plot(self, dataframe, **parameters):
"The scatter plot is for one point only! Please try another type: bar plot."
)

# --- Bar (uma coleção) ---
# --- Bar --- #
if parameters["type"] == "bar":
if len(df.collection.unique()) == 1 and len(df.point_id.unique()) >= 1:
df_group = df.groupby(["date", "class"]).count()["point_id"].unstack()
Expand Down Expand Up @@ -456,7 +458,7 @@ def plot(self, dataframe, **parameters):
)
return fig

# --- Bar (várias coleções) ---
# --- Bar --- #
elif len(df.collection.unique()) >= 1 and len(df.point_id.unique()) >= 1:
df_group = (
df.groupby(["collection", "date", "class"], observed=False)
Expand Down Expand Up @@ -566,3 +568,93 @@ def _get(self, url, op, **params):
raise ValueError(f"HTTP Response is not JSON: Content-Type: {content_type}")

return response.json()

@staticmethod
def temporal_filter(
df: pd.DataFrame,
target_classes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
relation_op: str = "contains",
) -> pd.DataFrame:
"""
Filter a WLTS trajectory dataframe based on target classes and time range.

Parameters
----------
df : pd.DataFrame
WLTS trajectory with columns: ["class", "collection", "date", "point_id"].
target_classes : List[str]
Land use/cover classes of interest.
start_date : str, optional
Start date (YYYY or YYYY-MM-DD).
end_date : str, optional
End date (YYYY or YYYY-MM-DD).
relation_op : str, optional
Relationship operator: "contains" or "equals".

Returns
-------
pd.DataFrame
Filtered trajectory dataframe.
"""
if not {"class", "collection", "date", "point_id"}.issubset(df.columns):
raise ValueError("Input dataframe must have columns: class, collection, date, point_id")

if start_date is None:
start_date = df["date"].min()
if end_date is None:
end_date = df["date"].max()

# Convert date column to datetime or int
if not pd.api.types.is_datetime64_any_dtype(df["date"]):
df["date"] = pd.to_datetime(df["date"], format="%Y", errors="coerce")

start_date = pd.to_datetime(start_date, errors="coerce")
end_date = pd.to_datetime(end_date, errors="coerce")

traj = df[(df["date"] >= start_date) & (df["date"] <= end_date)].copy()


traj.loc[~traj["class"].isin(target_classes), "class"] = pd.NA

def op_fn(x):
if relation_op == "equals":
return x.notna().all()
else: # contains
return x.notna().any()

mask = (
traj.groupby("point_id")
.filter(lambda g: op_fn(g["class"]))
.dropna(subset=["class"])
)

return mask

@staticmethod
def temporal_relation(
a: pd.DataFrame,
b: pd.DataFrame,
temp_fn: str = "before",
) -> pd.DataFrame:
"""Allen Relations."""
fn = ALLEN_RELATIONS.get(temp_fn)
if fn is None:
raise ValueError(f"Invalid relation '{temp_fn}'. Options: {list(ALLEN_RELATIONS)}")

all_ids = set(a["point_id"]) & set(b["point_id"])
a = a[a["point_id"].isin(all_ids)]
b = b[b["point_id"].isin(all_ids)]

results = []
for pid in all_ids:
a_id = a[a["point_id"] == pid]
b_id = b[b["point_id"] == pid]
res = fn(a_id, b_id)
if not res.empty:
results.append(res)

if results:
return pd.concat(results).sort_values(["point_id", "date"])
return pd.DataFrame(columns=a.columns)