diff --git a/fvh3t/core/gate_layer.py b/fvh3t/core/gate_layer.py index ed08630..9b1228b 100644 --- a/fvh3t/core/gate_layer.py +++ b/fvh3t/core/gate_layer.py @@ -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(): diff --git a/fvh3t/core/trajectory_layer.py b/fvh3t/core/trajectory_layer.py index 66efa1e..456c753 100644 --- a/fvh3t/core/trajectory_layer.py +++ b/fvh3t/core/trajectory_layer.py @@ -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 @@ -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? @@ -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) @@ -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) diff --git a/fvh3t/fvh3t_processing/count_trajectories.py b/fvh3t/fvh3t_processing/count_trajectories.py index 24477ad..9020e81 100644 --- a/fvh3t/fvh3t_processing/count_trajectories.py +++ b/fvh3t/fvh3t_processing/count_trajectories.py @@ -3,7 +3,6 @@ from typing import Any from qgis.core import ( - QgsFeature, QgsFeatureSink, QgsProcessing, QgsProcessingAlgorithm, @@ -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 @@ -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 @@ -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() @@ -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} diff --git a/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py index 7b3e518..235d165 100644 --- a/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py +++ b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py @@ -3,7 +3,7 @@ from fvh3t.fvh3t_processing.count_trajectories import CountTrajectories -class Provider(QgsProcessingProvider): +class TTTProvider(QgsProcessingProvider): def __init__(self) -> None: super().__init__() diff --git a/fvh3t/plugin.py b/fvh3t/plugin.py index 71ab609..2fd1d73 100644 --- a/fvh3t/plugin.py +++ b/fvh3t/plugin.py @@ -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 @@ -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 diff --git a/tests/core/test_trajectory_layer.py b/tests/core/test_trajectory_layer.py index 9894b07..dcf44be 100644 --- a/tests/core/test_trajectory_layer.py +++ b/tests/core/test_trajectory_layer.py @@ -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 @@ -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) diff --git a/tests/processing/__init__.py b/tests/processing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/processing/test_count_trajectories.py b/tests/processing/test_count_trajectories.py new file mode 100644 index 0000000..9b3cda0 --- /dev/null +++ b/tests/processing/test_count_trajectories.py @@ -0,0 +1,340 @@ +try: + import processing +except ImportError: + from qgis import processing + +import pytest +from qgis.core import QgsFeature, QgsField, QgsGeometry, QgsPointXY, QgsVectorLayer +from qgis.PyQt.QtCore import QDate, QDateTime, QTime, QTimeZone, QVariant + +from fvh3t.fvh3t_processing.traffic_trajectory_toolkit_provider import TTTProvider + + +@pytest.fixture +def input_gate_layer_for_algorithm(): + layer = QgsVectorLayer("LineString?crs=EPSG:3067", "Line Layer", "memory") + + layer.startEditing() + + layer.addAttribute(QgsField("counts_negative", QVariant.Bool)) + layer.addAttribute(QgsField("counts_positive", QVariant.Bool)) + + gate1 = QgsFeature(layer.fields()) + gate1.setAttributes([True, False]) + gate1.setGeometry( + QgsGeometry.fromPolylineXY( + [ + QgsPointXY(0, 0), + QgsPointXY(1, 0), + ] + ) + ) + + gate2 = QgsFeature(layer.fields()) + gate2.setAttributes([False, True]) + gate2.setGeometry( + QgsGeometry.fromPolylineXY( + [ + QgsPointXY(0, 1), + QgsPointXY(0, 2), + ] + ) + ) + + gate3 = QgsFeature(layer.fields()) + gate3.setAttributes([True, True]) + gate3.setGeometry( + QgsGeometry.fromPolylineXY( + [ + QgsPointXY(1, 1), + QgsPointXY(2, 1), + QgsPointXY(2, 2), + ] + ) + ) + + layer.addFeature(gate1) + layer.addFeature(gate2) + layer.addFeature(gate3) + + layer.commitChanges() + + return layer + + +@pytest.fixture +def input_point_layer_for_algorithm(): + layer = QgsVectorLayer("Point?crs=EPSG:3067", "Point Layer", "memory") + + layer.startEditing() + + layer.addAttribute(QgsField("id", QVariant.Int)) + layer.addAttribute(QgsField("timestamp", QVariant.Double)) + layer.addAttribute(QgsField("size_x", QVariant.Int)) + layer.addAttribute(QgsField("size_y", QVariant.Int)) + layer.addAttribute(QgsField("size_z", QVariant.Int)) + + traj1_f1 = QgsFeature(layer.fields()) + traj1_f1.setAttributes([1, 0, 1, 1, 1]) + traj1_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 1.5))) + + traj1_f2 = QgsFeature(layer.fields()) + traj1_f2.setAttributes([1, 1000, 2, 2, 2]) + traj1_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, 1.5))) + + traj1_f3 = QgsFeature(layer.fields()) + traj1_f3.setAttributes([1, 2000, 1, 1, 1]) + traj1_f3.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-0.5, 1.5))) + + traj1_f4 = QgsFeature(layer.fields()) + traj1_f4.setAttributes([1, 3000, 2, 2, 2]) + traj1_f4.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-1, 2))) + + traj2_f1 = QgsFeature(layer.fields()) + traj2_f1.setAttributes([2, 0, 1, 1, 1]) + traj2_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-0.5, 1))) + + traj2_f2 = QgsFeature(layer.fields()) + traj2_f2.setAttributes([2, 1000, 2, 2, 2]) + traj2_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, 2))) + + traj3_f1 = QgsFeature(layer.fields()) + traj3_f1.setAttributes([3, 0, 1, 1, 1]) + traj3_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.5, 0.5))) + + traj3_f2 = QgsFeature(layer.fields()) + traj3_f2.setAttributes([3, 1000, 2, 2, 2]) + traj3_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.5, 1.5))) + + traj4_f1 = QgsFeature(layer.fields()) + traj4_f1.setAttributes([4, 301000, 1, 1, 1]) + traj4_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.5, 2))) + + traj4_f2 = QgsFeature(layer.fields()) + traj4_f2.setAttributes([4, 302000, 2, 2, 2]) + traj4_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(2.5, 1))) + + traj5_f1 = QgsFeature(layer.fields()) + traj5_f1.setAttributes([5, 305000, 1, 1, 1]) + traj5_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 0.5))) + + traj5_f2 = QgsFeature(layer.fields()) + traj5_f2.setAttributes([5, 306000, 2, 2, 2]) + traj5_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0, -0.5))) + + traj6_f1 = QgsFeature(layer.fields()) + traj6_f1.setAttributes([6, 309000, 1, 1, 1]) + traj6_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, -0.5))) + + traj6_f2 = QgsFeature(layer.fields()) + traj6_f2.setAttributes([6, 310000, 2, 2, 2]) + traj6_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0, 0.5))) + + layer.addFeature(traj1_f1) + layer.addFeature(traj1_f2) + layer.addFeature(traj1_f3) + layer.addFeature(traj1_f4) + + layer.addFeature(traj2_f1) + layer.addFeature(traj2_f2) + + layer.addFeature(traj3_f1) + layer.addFeature(traj3_f2) + + layer.addFeature(traj4_f1) + layer.addFeature(traj4_f2) + + layer.addFeature(traj5_f1) + layer.addFeature(traj5_f2) + + layer.addFeature(traj6_f1) + layer.addFeature(traj6_f2) + + layer.commitChanges() + + return layer + + +def test_count_trajectories( + qgis_app, + qgis_processing, # noqa: ARG001 + input_point_layer_for_algorithm: QgsVectorLayer, + input_gate_layer_for_algorithm: QgsVectorLayer, +): + provider = TTTProvider() + + qgis_app.processingRegistry().addProvider(provider) + + ## TEST CASE 1 - NO FILTERING + + params = { + "INPUT_POINTS": input_point_layer_for_algorithm, + "INPUT_LINES": input_gate_layer_for_algorithm, + "START_TIME": None, + "END_TIME": None, + "OUTPUT_GATES": "TEMPORARY_OUTPUT", + "OUTPUT_TRAJECTORIES": "TEMPORARY_OUTPUT", + } + + result = processing.run( + "traffic_trajectory_toolkit:count_trajectories", + params, + ) + + output_gates: QgsVectorLayer = result["OUTPUT_GATES"] + output_trajectories: QgsVectorLayer = result["OUTPUT_TRAJECTORIES"] + + assert output_gates.featureCount() == 3 + assert output_trajectories.featureCount() == 6 + + gate1: QgsFeature = output_gates.getFeature(1) + gate2: QgsFeature = output_gates.getFeature(2) + gate3: QgsFeature = output_gates.getFeature(3) + + assert gate1.geometry().asWkt() == "LineString (0 0, 1 0)" + assert gate2.geometry().asWkt() == "LineString (0 1, 0 2)" + assert gate3.geometry().asWkt() == "LineString (1 1, 2 1, 2 2)" + + assert gate1.attribute("trajectory_count") == 1 + assert gate2.attribute("trajectory_count") == 1 + assert gate3.attribute("trajectory_count") == 2 + + traj1: QgsFeature = output_trajectories.getFeature(1) + traj2: QgsFeature = output_trajectories.getFeature(2) + traj3: QgsFeature = output_trajectories.getFeature(3) + traj4: QgsFeature = output_trajectories.getFeature(4) + traj5: QgsFeature = output_trajectories.getFeature(5) + traj6: QgsFeature = output_trajectories.getFeature(6) + + assert traj1.geometry().asWkt() == "LineString (1 1.5, 0.5 1.5, -0.5 1.5, -1 2)" + assert traj2.geometry().asWkt() == "LineString (-0.5 1, 0.5 2)" + assert traj3.geometry().asWkt() == "LineString (1.5 0.5, 1.5 1.5)" + assert traj4.geometry().asWkt() == "LineString (1.5 2, 2.5 1)" + assert traj5.geometry().asWkt() == "LineString (1 0.5, 0 -0.5)" + assert traj6.geometry().asWkt() == "LineString (0.5 -0.5, 0 0.5)" + + assert traj1.attribute("average_speed") == 2.65 + assert traj1.attribute("maximum_speed") == 3.6 + assert traj1.attribute("length") == 2.21 + assert traj1.attribute("duration") == 3 + assert traj1.attribute("minimum_size_x") == 1 + assert traj1.attribute("minimum_size_y") == 1 + assert traj1.attribute("minimum_size_z") == 1 + assert traj1.attribute("maximum_size_x") == 2 + assert traj1.attribute("maximum_size_y") == 2 + assert traj1.attribute("maximum_size_z") == 2 + assert traj1.attribute("average_size_x") == 1.5 + assert traj1.attribute("average_size_y") == 1.5 + assert traj1.attribute("average_size_z") == 1.5 + + assert traj2.attribute("average_speed") == 5.09 + assert traj2.attribute("maximum_speed") == 5.09 + assert traj2.attribute("length") == 1.41 + assert traj2.attribute("duration") == 1 + assert traj2.attribute("minimum_size_x") == 1 + assert traj2.attribute("minimum_size_y") == 1 + assert traj2.attribute("minimum_size_z") == 1 + assert traj2.attribute("maximum_size_x") == 2 + assert traj2.attribute("maximum_size_y") == 2 + assert traj2.attribute("maximum_size_z") == 2 + assert traj2.attribute("average_size_x") == 1.5 + assert traj2.attribute("average_size_y") == 1.5 + assert traj2.attribute("average_size_z") == 1.5 + + assert traj3.attribute("average_speed") == 3.6 + assert traj3.attribute("maximum_speed") == 3.6 + assert traj3.attribute("length") == 1 + assert traj3.attribute("duration") == 1 + assert traj3.attribute("minimum_size_x") == 1 + assert traj3.attribute("minimum_size_y") == 1 + assert traj3.attribute("minimum_size_z") == 1 + assert traj3.attribute("maximum_size_x") == 2 + assert traj3.attribute("maximum_size_y") == 2 + assert traj3.attribute("maximum_size_z") == 2 + assert traj3.attribute("average_size_x") == 1.5 + assert traj3.attribute("average_size_y") == 1.5 + assert traj3.attribute("average_size_z") == 1.5 + + assert traj4.attribute("average_speed") == 5.09 + assert traj4.attribute("maximum_speed") == 5.09 + assert traj4.attribute("length") == 1.41 + assert traj4.attribute("duration") == 1 + assert traj4.attribute("minimum_size_x") == 1 + assert traj4.attribute("minimum_size_y") == 1 + assert traj4.attribute("minimum_size_z") == 1 + assert traj4.attribute("maximum_size_x") == 2 + assert traj4.attribute("maximum_size_y") == 2 + assert traj4.attribute("maximum_size_z") == 2 + assert traj4.attribute("average_size_x") == 1.5 + assert traj4.attribute("average_size_y") == 1.5 + assert traj4.attribute("average_size_z") == 1.5 + + assert traj5.attribute("average_speed") == 5.09 + assert traj5.attribute("maximum_speed") == 5.09 + assert traj5.attribute("length") == 1.41 + assert traj5.attribute("duration") == 1 + assert traj5.attribute("minimum_size_x") == 1 + assert traj5.attribute("minimum_size_y") == 1 + assert traj5.attribute("minimum_size_z") == 1 + assert traj5.attribute("maximum_size_x") == 2 + assert traj5.attribute("maximum_size_y") == 2 + assert traj5.attribute("maximum_size_z") == 2 + assert traj5.attribute("average_size_x") == 1.5 + assert traj5.attribute("average_size_y") == 1.5 + assert traj5.attribute("average_size_z") == 1.5 + + assert traj6.attribute("average_speed") == 4.02 + assert traj6.attribute("maximum_speed") == 4.02 + assert traj6.attribute("length") == 1.12 + assert traj6.attribute("duration") == 1 + assert traj6.attribute("minimum_size_x") == 1 + assert traj6.attribute("minimum_size_y") == 1 + assert traj6.attribute("minimum_size_z") == 1 + assert traj6.attribute("maximum_size_x") == 2 + assert traj6.attribute("maximum_size_y") == 2 + assert traj6.attribute("maximum_size_z") == 2 + assert traj6.attribute("average_size_x") == 1.5 + assert traj6.attribute("average_size_y") == 1.5 + assert traj6.attribute("average_size_z") == 1.5 + + ### TEST CASE 2 - FILTER BY TIME + + case2_params = { + "INPUT_POINTS": input_point_layer_for_algorithm, + "INPUT_LINES": input_gate_layer_for_algorithm, + "START_TIME": QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0), QTimeZone.utc()), + "END_TIME": QDateTime(QDate(1970, 1, 1), QTime(0, 5, 0), QTimeZone.utc()), + "OUTPUT_GATES": "TEMPORARY_OUTPUT", + "OUTPUT_TRAJECTORIES": "TEMPORARY_OUTPUT", + } + + case2_result = processing.run( + "traffic_trajectory_toolkit:count_trajectories", + case2_params, + ) + + case2_output_gates: QgsVectorLayer = case2_result["OUTPUT_GATES"] + case2_output_trajectories: QgsVectorLayer = case2_result["OUTPUT_TRAJECTORIES"] + + assert case2_output_gates.featureCount() == 3 + assert case2_output_trajectories.featureCount() == 3 + + case2gate1: QgsFeature = case2_output_gates.getFeature(1) + case2gate2: QgsFeature = case2_output_gates.getFeature(2) + case2gate3: QgsFeature = case2_output_gates.getFeature(3) + + assert case2gate1.geometry().asWkt() == "LineString (0 0, 1 0)" + assert case2gate2.geometry().asWkt() == "LineString (0 1, 0 2)" + assert case2gate3.geometry().asWkt() == "LineString (1 1, 2 1, 2 2)" + + assert case2gate1.attribute("trajectory_count") == 0 + assert case2gate2.attribute("trajectory_count") == 1 + assert case2gate3.attribute("trajectory_count") == 1 + + case2traj1: QgsFeature = case2_output_trajectories.getFeature(1) + case2traj2: QgsFeature = case2_output_trajectories.getFeature(2) + case2traj3: QgsFeature = case2_output_trajectories.getFeature(3) + + assert case2traj1.geometry().asWkt() == "LineString (1 1.5, 0.5 1.5, -0.5 1.5, -1 2)" + assert case2traj2.geometry().asWkt() == "LineString (-0.5 1, 0.5 2)" + assert case2traj3.geometry().asWkt() == "LineString (1.5 0.5, 1.5 1.5)"