Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify count trajectories and add tests #63

Merged
merged 8 commits into from
Nov 4, 2024
Merged
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: 0 additions & 2 deletions fvh3t/core/gate_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ def create_gates(self) -> None:
counts_negative_field_idx: int = self.__layer.fields().indexOf(self.__counts_negative_field)
counts_positive_field_idx: int = self.__layer.fields().indexOf(self.__counts_positive_field)

# TODO: Check that these are bool fields

gates: list[Gate] = []

for feature in self.__layer.getFeatures():
Expand Down
11 changes: 8 additions & 3 deletions fvh3t/core/trajectory_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__(
length_field: str,
height_field: str,
timestamp_unit: QgsUnitTypes.TemporalUnit = QgsUnitTypes.TemporalUnit.TemporalUnknownUnit,
extra_filter_expression: str | None = None,
) -> None:
self.__layer: QgsVectorLayer = layer
self.__id_field: str = id_field
Expand Down Expand Up @@ -99,7 +100,7 @@ def __init__(
self.__timestamp_units = QgsUnitTypes.TemporalUnit.TemporalSeconds

self.__trajectories: tuple[Trajectory, ...] = ()
self.create_trajectories()
self.create_trajectories(extra_filter_expression)

# TODO: should the class of traveler be handled here?

Expand Down Expand Up @@ -133,7 +134,7 @@ def trajectories(self) -> tuple[Trajectory, ...]:
def crs(self) -> QgsCoordinateReferenceSystem:
return self.__layer.crs()

def create_trajectories(self) -> None:
def create_trajectories(self, filter_expression: str | None) -> None:
id_field_idx: int = self.__layer.fields().indexOf(self.__id_field)
timestamp_field_idx: int = self.__layer.fields().indexOf(self.__timestamp_field)
width_field_idx: int = self.__layer.fields().indexOf(self.__width_field)
Expand All @@ -145,7 +146,11 @@ def create_trajectories(self) -> None:
trajectories: list[Trajectory] = []

for identifier in unique_ids:
expression = QgsExpression(f'"{self.__id_field}" = {identifier}')
expression_str = f'("{self.__id_field}" = {identifier})'
if filter_expression:
expression_str += f" and ({filter_expression})"

expression = QgsExpression(expression_str)
request = QgsFeatureRequest(expression)

order_clause = QgsFeatureRequest.OrderByClause(self.__timestamp_field, ascending=True)
Expand Down
73 changes: 21 additions & 52 deletions fvh3t/fvh3t_processing/count_trajectories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from typing import Any

from qgis.core import (
QgsFeature,
QgsFeatureSink,
QgsProcessing,
QgsProcessingAlgorithm,
Expand All @@ -13,9 +12,8 @@
QgsProcessingParameterFeatureSink,
QgsProcessingParameterVectorLayer,
QgsUnitTypes,
QgsVectorLayer,
)
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtCore import QCoreApplication, QDateTime

from fvh3t.core.gate_layer import GateLayer
from fvh3t.core.trajectory_layer import TrajectoryLayer
Expand Down Expand Up @@ -111,8 +109,20 @@ def processAlgorithm( # noqa N802
feedback = QgsProcessingFeedback()

point_layer = self.parameterAsVectorLayer(parameters, self.INPUT_POINTS, context)
start_time = self.parameterAsDateTime(parameters, self.START_TIME, context)
end_time = self.parameterAsDateTime(parameters, self.END_TIME, context)
start_time: QDateTime = self.parameterAsDateTime(parameters, self.START_TIME, context)
end_time: QDateTime = self.parameterAsDateTime(parameters, self.END_TIME, context)

# the datetime widget doesn't allow the user to set the seconds and they
# are being set seemingly randomly leading to odd results...
# so set 0 seconds manually

zero_s_start_time = start_time.time()
zero_s_start_time.setHMS(zero_s_start_time.hour(), zero_s_start_time.minute(), 0)
start_time.setTime(zero_s_start_time)

zero_s_end_time = end_time.time()
zero_s_end_time.setHMS(zero_s_end_time.hour(), zero_s_end_time.minute(), 0)
end_time.setTime(zero_s_end_time)

## CREATE TRAJECTORIES

Expand All @@ -137,55 +147,20 @@ def processAlgorithm( # noqa N802
msg = "Set start and/or end timestamps are out of data's range."
raise ValueError(msg)

# Prepare a memory layer for filtered points
fields = point_layer.fields()
filtered_layer = QgsVectorLayer("Point?crs=" + point_layer.crs().authid(), "Filtered points", "memory")
filtered_layer.dataProvider().addAttributes(fields)
filtered_layer.updateFields()

id_count = {}
for feature in point_layer.getFeatures():
timestamp = feature["timestamp"]
feature_id = feature["id"]

# Filter features based on timestamp
if start_time_unix <= timestamp <= end_time_unix:
new_feature = QgsFeature(feature)
filtered_layer.dataProvider().addFeature(new_feature)

# Count ids
if feature_id not in id_count:
id_count[feature_id] = 0
id_count[feature_id] += 1

feedback.pushInfo(f"Filtered {filtered_layer.featureCount()} features based on timestamp range.")

# Prepare another memory layer for features with non-unique id after filtering time
non_unique_layer = QgsVectorLayer(
"Point?crs=" + point_layer.crs().authid(), "Non-unique filtered points", "memory"
)
non_unique_layer.dataProvider().addAttributes(fields)
non_unique_layer.updateFields()

for feature in filtered_layer.getFeatures():
feature_id = feature["id"]
# Add only features with non-unique id
if id_count.get(feature_id, 0) > 1:
new_feature = QgsFeature(feature)
non_unique_layer.dataProvider().addFeature(new_feature)

feedback.pushInfo(
f"Final filtered point layer contains {non_unique_layer.featureCount()} features with non-unique IDs."
)
# If start or end time was given, filter the nodes outside the time range
filter_expression: str | None = None
if start_time_unix != min_timestamp or end_time_unix != max_timestamp:
filter_expression = f'"timestamp" BETWEEN {start_time_unix} AND {end_time_unix}'

trajectory_layer = TrajectoryLayer(
non_unique_layer,
point_layer,
"id",
"timestamp",
"size_x",
"size_y",
"size_z",
QgsUnitTypes.TemporalUnit.TemporalMilliseconds,
filter_expression,
)

exported_traj_layer = trajectory_layer.as_line_layer()
Expand Down Expand Up @@ -236,10 +211,4 @@ def processAlgorithm( # noqa N802
for feature in exported_gate_layer.getFeatures():
sink.addFeature(feature, QgsFeatureSink.FastInsert)

# Return the results of the algorithm. In this case our only result is
# the feature sink which contains the processed features, but some
# algorithms may return multiple feature sinks, calculated numeric
# statistics, etc. These should all be included in the returned
# dictionary, with keys matching the feature corresponding parameter
# or output names.
return {self.OUTPUT_TRAJECTORIES: traj_dest_id, self.OUTPUT_GATES: gate_dest_id}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fvh3t.fvh3t_processing.count_trajectories import CountTrajectories


class Provider(QgsProcessingProvider):
class TTTProvider(QgsProcessingProvider):
def __init__(self) -> None:
super().__init__()

Expand Down
4 changes: 2 additions & 2 deletions fvh3t/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from qgis.PyQt.QtWidgets import QAction, QWidget
from qgis.utils import iface

from fvh3t.fvh3t_processing.traffic_trajectory_toolkit_provider import Provider
from fvh3t.fvh3t_processing.traffic_trajectory_toolkit_provider import TTTProvider
from fvh3t.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger
from fvh3t.qgis_plugin_tools.tools.i18n import setup_translation
from fvh3t.qgis_plugin_tools.tools.resources import plugin_name
Expand Down Expand Up @@ -103,7 +103,7 @@ def add_action(
return action

def initProcessing(self): # noqa N802
self.provider = Provider()
self.provider = TTTProvider()
QgsApplication.processingRegistry().addProvider(self.provider)

def initGui(self) -> None: # noqa N802
Expand Down
36 changes: 35 additions & 1 deletion tests/core/test_trajectory_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import TYPE_CHECKING

import pytest
from qgis.core import QgsUnitTypes
from qgis.core import QgsUnitTypes, QgsVectorLayer

from fvh3t.core.exceptions import InvalidLayerException
from fvh3t.core.trajectory_layer import TrajectoryLayer
Expand Down Expand Up @@ -148,6 +148,40 @@ def test_is_field_valid(qgis_point_layer_no_additional_fields, qgis_point_layer_
)


def test_create_trajectory_layer_extra_filter_expression(qgis_point_layer: QgsVectorLayer):
filter_expression = '"timestamp" BETWEEN 200 AND 600'

layer = TrajectoryLayer(
qgis_point_layer,
"id",
"timestamp",
"width",
"length",
"height",
QgsUnitTypes.TemporalUnit.TemporalMilliseconds,
filter_expression,
)

trajectories = layer.trajectories()

assert len(trajectories) == 2

traj1 = trajectories[0]
traj2 = trajectories[1]

traj1nodes = traj1.nodes()
traj2nodes = traj2.nodes()

assert len(traj1nodes) == 2
assert len(traj2nodes) == 2

assert traj1nodes[0].timestamp.timestamp() == 0.2
assert traj1nodes[1].timestamp.timestamp() == 0.3

assert traj2nodes[0].timestamp.timestamp() == 0.5
assert traj2nodes[1].timestamp.timestamp() == 0.6


def test_create_trajectory_layer_single_trajectory_node(qgis_single_point_layer, caplog):
caplog.set_level(logging.INFO)

Expand Down
Empty file added tests/processing/__init__.py
Empty file.
Loading
Loading