diff --git a/fvh3t/core/area.py b/fvh3t/core/area.py new file mode 100644 index 0000000..dbfc582 --- /dev/null +++ b/fvh3t/core/area.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from fvh3t.core.trajectory import Trajectory + from fvh3t.core.trajectory_layer import TrajectoryLayer + +from qgis.core import ( + QgsGeometry, + QgsWkbTypes, +) + +from fvh3t.core.exceptions import InvalidGeometryTypeException + + +class Area: + """ + A wrapper class around a QgsGeometry which represents a + polygon through which trajectories can pass. The geometry + must be a polygon. + """ + + def __init__( + self, + geom: QgsGeometry, + name: str, + ) -> None: + if geom.type() != QgsWkbTypes.GeometryType.PolygonGeometry: + msg = "Area must be created from a polygon geometry!" + raise InvalidGeometryTypeException(msg) + + self.__geom: QgsGeometry = geom + self.__name: str = name + self.__trajectory_count: int = 0 + self.__average_speed: float = 0.0 + + def geometry(self) -> QgsGeometry: + return self.__geom + + def name(self) -> str: + return self.__name + + def trajectory_count(self) -> int: + return self.__trajectory_count + + def average_speed(self) -> float: + return self.__average_speed + + def intersects(self, traj: Trajectory) -> bool: + return self.__geom.intersects(traj.as_geometry()) + + def count_trajectories_from_layer(self, layer: TrajectoryLayer) -> None: + self.count_trajectories(layer.trajectories()) + + def count_trajectories( + self, + trajectories: tuple[Trajectory, ...], + ) -> None: + speed = 0.0 + + for trajectory in trajectories: + if self.intersects(trajectory): + traj_speed = trajectory.average_speed() + speed += traj_speed + + self.__trajectory_count += 1 + + if self.__trajectory_count > 0: + self.__average_speed = speed / self.__trajectory_count diff --git a/fvh3t/core/area_layer.py b/fvh3t/core/area_layer.py new file mode 100644 index 0000000..e0c8251 --- /dev/null +++ b/fvh3t/core/area_layer.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from qgis.core import QgsFeature, QgsFeatureSource, QgsField, QgsVectorLayer, QgsWkbTypes +from qgis.PyQt.QtCore import QDateTime, QMetaType, QVariant + +from fvh3t.core.area import Area +from fvh3t.core.exceptions import InvalidFeatureException, InvalidLayerException + +if TYPE_CHECKING: + from fvh3t.core.trajectory_layer import TrajectoryLayer + + +class AreaLayer: + """ + Wrapper around a QgsVectorLayer object from which areas + can be instantiated, i.e. + + 1. is a polygon layer + 2. has a name field + 3. has features + """ + + def __init__( + self, + layer: QgsVectorLayer, + id_field: str, + name_field: str, + ) -> None: + self.__layer: QgsVectorLayer = layer + self.__name_field = name_field + self.__id_field = id_field + + if self.is_valid(): + self.__areas: tuple[Area, ...] = () + self.create_areas() + + def create_areas(self) -> None: + name_field_idx: int = self.__layer.fields().indexOf(self.__name_field) + areas: list[Area] = [] + + for feature in self.__layer.getFeatures(): + name: str = feature[name_field_idx] + area = Area( + feature.geometry(), + name=name, + ) + + areas.append(area) + + self.__areas = tuple(areas) + + def count_trajectories_from_layer(self, layer: TrajectoryLayer) -> None: + for area in self.__areas: + area.count_trajectories_from_layer(layer) + + def areas(self) -> tuple[Area, ...]: + return self.__areas + + def as_polygon_layer( + self, traveler_class: str | None, start_time: QDateTime, end_time: QDateTime + ) -> QgsVectorLayer | None: + polygon_layer = QgsVectorLayer("Polygon", "Polygon Layer", "memory") + polygon_layer.setCrs(self.__layer.crs()) + + polygon_layer.startEditing() + + polygon_layer.addAttribute(QgsField("fid", QVariant.Int)) + polygon_layer.addAttribute(QgsField("name", QVariant.String)) + polygon_layer.addAttribute(QgsField("class", QVariant.String)) + polygon_layer.addAttribute(QgsField("interval_start", QVariant.DateTime)) + polygon_layer.addAttribute(QgsField("interval_end", QVariant.DateTime)) + polygon_layer.addAttribute(QgsField("vehicle_count", QVariant.Int)) + polygon_layer.addAttribute(QgsField("speed_avg", QVariant.Double)) + + fields = polygon_layer.fields() + + for i, area in enumerate(self.__areas, 1): + feature = QgsFeature(fields) + + feature.setAttributes( + [ + i, + area.name(), + traveler_class if traveler_class else "all", + start_time, + end_time, + area.trajectory_count(), + round(area.average_speed(), 2), + ] + ) + feature.setGeometry(area.geometry()) + + if not feature.isValid(): + raise InvalidFeatureException + + polygon_layer.addFeature(feature) + + polygon_layer.commitChanges() + + return polygon_layer + + def is_field_valid(self, field_name: str, *, accepted_types: list[QMetaType.Type]) -> bool: + """ + Check that a field 1) exists and 2) has an + acceptable type. Leave type list empty to + allow any type. + """ + field_id: int = self.__layer.fields().indexFromName(field_name) + + if field_id == -1: + return False + + if not accepted_types: # means all types are accepted + return True + + field: QgsField = self.__layer.fields().field(field_id) + field_type: QMetaType.Type = field.type() + + return field_type in accepted_types + + def is_valid(self) -> bool: + is_layer_valid: bool = self.__layer.isValid() + if not is_layer_valid: + msg = "Layer is not valid." + raise InvalidLayerException(msg) + + is_polygon_layer: bool = self.__layer.geometryType() == QgsWkbTypes.GeometryType.PolygonGeometry + if not is_polygon_layer: + msg = "Layer is not a polygon layer." + raise InvalidLayerException(msg) + + has_features: bool = self.__layer.hasFeatures() == QgsFeatureSource.FeatureAvailability.FeaturesAvailable + if not has_features: + msg = "Layer has no features." + raise InvalidLayerException(msg) + + if not self.is_field_valid(self.__name_field, accepted_types=[QMetaType.Type.QString]): + msg = "Name field either not found or of incorrect type." + raise InvalidLayerException(msg) + + if not self.is_field_valid(self.__id_field, accepted_types=[]): + msg = "ID field not found." + raise InvalidLayerException(msg) + + return True diff --git a/fvh3t/core/gate_layer.py b/fvh3t/core/gate_layer.py index f50b130..e776d5d 100644 --- a/fvh3t/core/gate_layer.py +++ b/fvh3t/core/gate_layer.py @@ -59,7 +59,9 @@ def create_gates(self) -> None: def gates(self) -> tuple[Gate, ...]: return self.__gates - def as_line_layer(self, traveler_class: str, start_time: QDateTime, end_time: QDateTime) -> QgsVectorLayer | None: + def as_line_layer( + self, traveler_class: str | None, start_time: QDateTime, end_time: QDateTime + ) -> QgsVectorLayer | None: line_layer = QgsVectorLayer("LineString", "Line Layer", "memory") line_layer.setCrs(self.__layer.crs()) @@ -78,21 +80,21 @@ def as_line_layer(self, traveler_class: str, start_time: QDateTime, end_time: QD fields = line_layer.fields() - for i, gate in enumerate(self.__gates): + for i, gate in enumerate(self.__gates, 1): feature = QgsFeature(fields) feature.setAttributes( [ i, gate.name(), - traveler_class, + traveler_class if traveler_class else "all", start_time, end_time, gate.counts_negative(), gate.counts_positive(), gate.trajectory_count(), - gate.average_speed(), - gate.average_acceleration(), + round(gate.average_speed(), 2), + round(gate.average_acceleration(), 2), ] ) feature.setGeometry(gate.geometry()) diff --git a/fvh3t/core/qgis_layer_utils.py b/fvh3t/core/qgis_layer_utils.py index 5f00153..6b3558f 100644 --- a/fvh3t/core/qgis_layer_utils.py +++ b/fvh3t/core/qgis_layer_utils.py @@ -1,5 +1,6 @@ from qgis.core import ( QgsCoordinateReferenceSystem, + QgsDefaultValue, QgsFeatureRenderer, QgsField, QgsFieldConstraints, @@ -14,6 +15,16 @@ class QgisLayerUtils: + @staticmethod + def set_area_style(layer: QgsVectorLayer) -> None: + doc = QDomDocument() + + with open(resources_path("style", "area_style.xml")) as style_file: + doc.setContent(style_file.read()) + + renderer = QgsFeatureRenderer.load(doc.documentElement(), QgsReadWriteContext()) + layer.setRenderer(renderer) + @staticmethod def set_gate_style(layer: QgsVectorLayer) -> None: doc = QDomDocument() @@ -43,3 +54,28 @@ def create_gate_layer(crs: QgsCoordinateReferenceSystem) -> QgsVectorLayer: QgisLayerUtils.set_gate_style(layer) return layer + + @staticmethod + def create_area_layer(crs: QgsCoordinateReferenceSystem) -> QgsVectorLayer: + layer = QgsVectorLayer("Polygon", "Area Layer", "memory") + layer.setCrs(crs) + + with edit(layer): + layer.addAttribute(QgsField("fid", QVariant.Int)) + layer.addAttribute(QgsField("name", QVariant.String)) + + layer.setFieldConstraint(0, QgsFieldConstraints.Constraint.ConstraintNotNull) + + def_value = QgsDefaultValue( + """with_variable('feat_id', (maximum("fid") + 1), if (@feat_id is NULL, 1, @feat_id))""" + ) + layer.setDefaultValueDefinition(0, def_value) + + efc = layer.editFormConfig() + efc.setReadOnly(0) + + layer.setEditFormConfig(efc) + + QgisLayerUtils.set_area_style(layer) + + return layer diff --git a/fvh3t/core/trajectory_layer.py b/fvh3t/core/trajectory_layer.py index 456c753..318f1f4 100644 --- a/fvh3t/core/trajectory_layer.py +++ b/fvh3t/core/trajectory_layer.py @@ -102,8 +102,6 @@ def __init__( self.__trajectories: tuple[Trajectory, ...] = () self.create_trajectories(extra_filter_expression) - # TODO: should the class of traveler be handled here? - def layer(self) -> QgsVectorLayer: return self.__layer @@ -134,21 +132,27 @@ def trajectories(self) -> tuple[Trajectory, ...]: def crs(self) -> QgsCoordinateReferenceSystem: return self.__layer.crs() - def create_trajectories(self, filter_expression: str | None) -> None: + def create_trajectories(self, extra_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) length_field_idx: int = self.__layer.fields().indexOf(self.__length_field) height_field_idx: int = self.__layer.fields().indexOf(self.__height_field) + id_is_string: bool = self.__layer.fields().field(id_field_idx).type() == QMetaType.Type.QString + unique_ids: set[Any] = self.__layer.uniqueValues(id_field_idx) trajectories: list[Trajectory] = [] for identifier in unique_ids: - expression_str = f'("{self.__id_field}" = {identifier})' - if filter_expression: - expression_str += f" and ({filter_expression})" + if id_is_string: + expression_str = f"(\"{self.__id_field}\" = '{identifier}')" + else: + expression_str = f'("{self.__id_field}" = {identifier})' + + if extra_filter_expression: + expression_str += f" and ({extra_filter_expression})" expression = QgsExpression(expression_str) request = QgsFeatureRequest(expression) @@ -207,7 +211,7 @@ def as_line_layer(self) -> QgsVectorLayer | None: fields = line_layer.fields() - for i, trajectory in enumerate(self.__trajectories): + for i, trajectory in enumerate(self.__trajectories, 1): feature = QgsFeature(fields) min_size_x, min_size_y, min_size_z = trajectory.minimum_size() diff --git a/fvh3t/fvh3t_processing/count_trajectories_area.py b/fvh3t/fvh3t_processing/count_trajectories_area.py new file mode 100644 index 0000000..325cdd5 --- /dev/null +++ b/fvh3t/fvh3t_processing/count_trajectories_area.py @@ -0,0 +1,290 @@ +from __future__ import annotations + +try: + import processing +except ImportError: + from qgis import processing + +from typing import Any + +from qgis.core import ( + QgsFeatureRequest, + QgsFeatureSink, + QgsField, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingFeedback, + QgsProcessingParameterDateTime, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterString, + QgsProcessingParameterVectorLayer, + QgsProcessingUtils, + QgsUnitTypes, + QgsVectorLayer, + edit, +) +from qgis.PyQt.QtCore import QCoreApplication, QDateTime, QVariant + +from fvh3t.core.area_layer import AreaLayer +from fvh3t.core.qgis_layer_utils import QgisLayerUtils +from fvh3t.core.trajectory_layer import TrajectoryLayer +from fvh3t.fvh3t_processing.utils import ProcessingUtils + + +class CountTrajectoriesArea(QgsProcessingAlgorithm): + INPUT_POINTS = "INPUT_POINTS" + INPUT_AREAS = "INPUT_AREAS" + TRAVELER_CLASS = "TRAVELER_CLASS" + START_TIME = "START_TIME" + END_TIME = "END_TIME" + OUTPUT_AREAS = "OUTPUT_AREAS" + OUTPUT_TRAJECTORIES = "OUTPUT_TRAJECTORIES" + + area_dest_id: str | None = None + traj_dest_id: str | None = None + + def __init__(self) -> None: + super().__init__() + + self._name = "count_trajectories_area" + self._display_name = "Count trajectories (areas)" + + def tr(self, string) -> str: + return QCoreApplication.translate("Processing", string) + + def createInstance(self): # noqa N802 + return CountTrajectoriesArea() + + def name(self) -> str: + return self._name + + def displayName(self) -> str: # noqa N802 + return self.tr(self._display_name) + + def initAlgorithm(self, config=None): # noqa N802 + self.addParameter( + QgsProcessingParameterVectorLayer( + name=self.INPUT_POINTS, + description="Input point layer", + types=[QgsProcessing.SourceType.TypeVectorPoint], + ) + ) + + self.addParameter( + QgsProcessingParameterVectorLayer( + name=self.INPUT_AREAS, + description="Areas", + types=[QgsProcessing.SourceType.TypeVectorPolygon], + ) + ) + + self.addParameter( + QgsProcessingParameterString( + name=self.TRAVELER_CLASS, + description="Class of traveler", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterDateTime( + name=self.START_TIME, + description="Start time", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterDateTime( + name=self.END_TIME, + description="End time", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + name=self.OUTPUT_AREAS, + description="Areas", + type=QgsProcessing.SourceType.TypeVectorPolygon, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + name=self.OUTPUT_TRAJECTORIES, + description="Trajectories - Areas", + type=QgsProcessing.SourceType.TypeVectorLine, + ) + ) + + def processAlgorithm( # noqa N802 + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict: + """ + Here is where the processing itself takes place. + """ + + # Initialize feedback if it is None + if feedback is None: + feedback = QgsProcessingFeedback() + + point_layer = self.parameterAsVectorLayer(parameters, self.INPUT_POINTS, context) + area_vector_layer = self.parameterAsVectorLayer(parameters, self.INPUT_AREAS, context) + traveler_class = self.parameterAsString(parameters, self.TRAVELER_CLASS, context) + start_time: QDateTime = self.parameterAsDateTime(parameters, self.START_TIME, context) + end_time: QDateTime = self.parameterAsDateTime(parameters, self.END_TIME, context) + + # create area layer already so it'll check for validity and terminate if + # it's invalid + + area_layer_fid_field = "fid" + + feedback.pushInfo(f"Area layer has {area_vector_layer.featureCount()} features.") + area_layer = AreaLayer(area_vector_layer, area_layer_fid_field, "name") + + # 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 + ProcessingUtils.normalize_datetimes(start_time, end_time) + + ## CREATE TRAJECTORIES + + total_features: int = point_layer.featureCount() + feedback.pushInfo(f"Original point layer has {total_features} features.") + + # Get min and max timestamps from the data + min_timestamp, max_timestamp = ProcessingUtils.get_min_and_max_timestamps(point_layer, "timestamp") + start_time_unix, end_time_unix = ProcessingUtils.get_start_and_end_timestamps( + start_time, end_time, min_timestamp, max_timestamp + ) + + filter_expression: str | None = ProcessingUtils.get_filter_expression_time_and_class( + start_time_unix, + end_time_unix, + traveler_class, + min_timestamp, + max_timestamp, + ) + + if filter_expression is None: + filter_expression = "" + else: + filter_expression += " AND " + + filter_expression += f"(overlay_within('{area_vector_layer.id()}'))" + + req = QgsFeatureRequest().setFilterExpression(filter_expression) + filtered_points = point_layer.materialize(req) + + total_filtered_points: int = filtered_points.featureCount() + feedback.pushInfo(f"Filtered {total_features - total_filtered_points} features out.") + feedback.pushInfo(f"Creating trajectories for {total_filtered_points} points out of {total_features}.") + + # add area id to points to enable grouping them by area + join_result = processing.run( + "native:joinattributesbylocation", + { + "INPUT": filtered_points, + "PREDICATE": [0], + "JOIN": area_vector_layer, + "JOIN_FIELDS": ["fid"], + "METHOD": 1, + "DISCARD_NONMATCHING": False, + "PREFIX": "area_", + "OUTPUT": "TEMPORARY_OUTPUT", + }, + ) + + filtered_and_grouped_points: QgsVectorLayer = join_result["OUTPUT"] + + # convert id to string and concatenate the area_fid to it + # to group the points by id AND the area they're in + grouped_id_field = QgsField("grouped_id", QVariant.String) + + with edit(filtered_and_grouped_points): + filtered_and_grouped_points.addAttribute(grouped_id_field) + id_field_idx: int = filtered_and_grouped_points.fields().indexOf("id") + area_id_field_idx: int = filtered_and_grouped_points.fields().indexOf("area_fid") + grouped_id_field_idx: int = filtered_and_grouped_points.fields().indexOf("grouped_id") + + for feature in filtered_and_grouped_points.getFeatures(): + idx: int = feature[id_field_idx] + area_id: int = feature[area_id_field_idx] + + grouped_id = f"{area_id}_{idx}" + + filtered_and_grouped_points.changeAttributeValue( + feature.id(), + grouped_id_field_idx, + grouped_id, + ) + + trajectory_layer = TrajectoryLayer( + filtered_and_grouped_points, + "grouped_id", + "timestamp", + "size_x", + "size_y", + "size_z", + QgsUnitTypes.TemporalUnit.TemporalMilliseconds, + ) + + exported_traj_layer = trajectory_layer.as_line_layer() + + if exported_traj_layer is None: + msg = "Trajectory layer is None." + raise ValueError(msg) + + (sink, self.traj_dest_id) = self.parameterAsSink( + parameters, + self.OUTPUT_TRAJECTORIES, + context, + exported_traj_layer.fields(), + exported_traj_layer.wkbType(), + exported_traj_layer.sourceCrs(), + ) + + for feature in exported_traj_layer.getFeatures(): + sink.addFeature(feature, QgsFeatureSink.Flag.FastInsert) + + # CREATE AREAS + + area_layer.count_trajectories_from_layer(trajectory_layer) + + if not start_time: + start_time = QDateTime.fromMSecsSinceEpoch(int(min_timestamp)) + if not end_time: + end_time = QDateTime.fromMSecsSinceEpoch(int(max_timestamp)) + exported_area_layer = area_layer.as_polygon_layer( + traveler_class=traveler_class, start_time=start_time, end_time=end_time + ) + + if exported_area_layer is None: + msg = "Polygon layer is None" + raise ValueError(msg) + + (sink, self.area_dest_id) = self.parameterAsSink( + parameters, + self.OUTPUT_AREAS, + context, + exported_area_layer.fields(), + exported_area_layer.wkbType(), + exported_area_layer.sourceCrs(), + ) + + for feature in exported_area_layer.getFeatures(): + sink.addFeature(feature, QgsFeatureSink.Flag.FastInsert) + + return {self.OUTPUT_TRAJECTORIES: self.traj_dest_id, self.OUTPUT_AREAS: self.area_dest_id} + + def postProcessAlgorithm(self, context: QgsProcessingContext, feedback: QgsProcessingFeedback) -> dict[str, Any]: # noqa: N802 + if self.area_dest_id: + layer = QgsProcessingUtils.mapLayerFromString(self.area_dest_id, context) + QgisLayerUtils.set_area_style(layer) + + return super().postProcessAlgorithm(context, feedback) diff --git a/fvh3t/fvh3t_processing/count_trajectories.py b/fvh3t/fvh3t_processing/count_trajectories_gate.py similarity index 71% rename from fvh3t/fvh3t_processing/count_trajectories.py rename to fvh3t/fvh3t_processing/count_trajectories_gate.py index 937286c..e7714d4 100644 --- a/fvh3t/fvh3t_processing/count_trajectories.py +++ b/fvh3t/fvh3t_processing/count_trajectories_gate.py @@ -3,6 +3,7 @@ from typing import Any from qgis.core import ( + QgsFeatureRequest, QgsFeatureSink, QgsProcessing, QgsProcessingAlgorithm, @@ -20,9 +21,10 @@ from fvh3t.core.gate_layer import GateLayer from fvh3t.core.qgis_layer_utils import QgisLayerUtils from fvh3t.core.trajectory_layer import TrajectoryLayer +from fvh3t.fvh3t_processing.utils import ProcessingUtils -class CountTrajectories(QgsProcessingAlgorithm): +class CountTrajectoriesGate(QgsProcessingAlgorithm): INPUT_POINTS = "INPUT_POINTS" INPUT_LINES = "INPUT_LINES" TRAVELER_CLASS = "TRAVELER_CLASS" @@ -32,18 +34,19 @@ class CountTrajectories(QgsProcessingAlgorithm): OUTPUT_TRAJECTORIES = "OUTPUT_TRAJECTORIES" gate_dest_id: str | None = None + traj_dest_id: str | None = None def __init__(self) -> None: super().__init__() - self._name = "count_trajectories" - self._display_name = "Count trajectories" + self._name = "count_trajectories_gate" + self._display_name = "Count trajectories (gates)" def tr(self, string) -> str: return QCoreApplication.translate("Processing", string) def createInstance(self): # noqa N802 - return CountTrajectories() + return CountTrajectoriesGate() def name(self) -> str: return self._name @@ -103,7 +106,7 @@ def initAlgorithm(self, config=None): # noqa N802 self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUTPUT_TRAJECTORIES, - description="Trajectories", + description="Trajectories - Gates", type=QgsProcessing.TypeVectorLine, ) ) @@ -123,64 +126,57 @@ def processAlgorithm( # noqa N802 feedback = QgsProcessingFeedback() point_layer = self.parameterAsVectorLayer(parameters, self.INPUT_POINTS, context) - traveler_class = self.parameterAsString(parameters, self.TRAVELER_CLASS, context) + traveler_class: str | None = self.parameterAsString(parameters, self.TRAVELER_CLASS, context) start_time: QDateTime = self.parameterAsDateTime(parameters, self.START_TIME, context) end_time: QDateTime = self.parameterAsDateTime(parameters, self.END_TIME, context) + # create gate layer already, so we check that it's valid + line_layer = self.parameterAsVectorLayer(parameters, self.INPUT_LINES, context) + feedback.pushInfo(f"Line layer has {line_layer.featureCount()} features.") + gate_layer = GateLayer(line_layer, "name", "counts_negative", "counts_positive") + # 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) + ProcessingUtils.normalize_datetimes(start_time, end_time) ## CREATE TRAJECTORIES - feedback.pushInfo(f"Original point layer has {point_layer.featureCount()} features.") + total_features: int = point_layer.featureCount() + feedback.pushInfo(f"Original point layer has {total_features} features.") # Get min and max timestamps from the data - timestamp_field_id = point_layer.fields().indexOf("timestamp") - min_timestamp, max_timestamp = point_layer.minimumAndMaximumValue(timestamp_field_id) + min_timestamp, max_timestamp = ProcessingUtils.get_min_and_max_timestamps(point_layer, "timestamp") + start_time_unix, end_time_unix = ProcessingUtils.get_start_and_end_timestamps( + start_time, end_time, min_timestamp, max_timestamp + ) - if min_timestamp is None or max_timestamp is None: - msg = "No valid timestamps found in the point layer." - raise ValueError(msg) + filter_expression: str | None = ProcessingUtils.get_filter_expression_time_and_class( + start_time_unix, + end_time_unix, + traveler_class, + min_timestamp, + max_timestamp, + ) - # Check if start and end times are empty. If yes, use min and max timestamps. If not, convert to unix time. - start_time_unix = start_time.toMSecsSinceEpoch() if start_time.isValid() else min_timestamp - end_time_unix = end_time.toMSecsSinceEpoch() if end_time.isValid() else max_timestamp + req = QgsFeatureRequest() + if filter_expression: + req.setFilterExpression(filter_expression) - # Check that the set start and end times are in data's range - if not (min_timestamp <= start_time_unix <= max_timestamp) or not ( - min_timestamp <= end_time_unix <= max_timestamp - ): - msg = "Set start and/or end timestamps are out of data's range." - raise ValueError(msg) + filtered_layer = point_layer.materialize(req) - # 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}' - if not filter_expression: - if traveler_class: - filter_expression = f"\"label\" = '{traveler_class}'" - elif traveler_class: - filter_expression += f" AND \"label\" = '{traveler_class}'" + total_filtered_points: int = filtered_layer.featureCount() + feedback.pushInfo(f"Filtered {total_features - total_filtered_points} features out.") + feedback.pushInfo(f"Creating trajectories for {total_filtered_points} points out of {total_features}.") trajectory_layer = TrajectoryLayer( - point_layer, + filtered_layer, "id", "timestamp", "size_x", "size_y", "size_z", QgsUnitTypes.TemporalUnit.TemporalMilliseconds, - filter_expression, ) exported_traj_layer = trajectory_layer.as_line_layer() @@ -189,7 +185,7 @@ def processAlgorithm( # noqa N802 msg = "Trajectory layer is None." raise ValueError(msg) - (sink, traj_dest_id) = self.parameterAsSink( + (sink, self.traj_dest_id) = self.parameterAsSink( parameters, self.OUTPUT_TRAJECTORIES, context, @@ -202,12 +198,6 @@ def processAlgorithm( # noqa N802 sink.addFeature(feature, QgsFeatureSink.FastInsert) # CREATE GATES - line_layer = self.parameterAsVectorLayer(parameters, self.INPUT_LINES, context) - - feedback.pushInfo(f"Line layer has {line_layer.featureCount()} features.") - - gate_layer = GateLayer(line_layer, "name", "counts_negative", "counts_positive") - gates = gate_layer.gates() for gate in gates: @@ -237,7 +227,7 @@ def processAlgorithm( # noqa N802 for feature in exported_gate_layer.getFeatures(): sink.addFeature(feature, QgsFeatureSink.FastInsert) - return {self.OUTPUT_TRAJECTORIES: traj_dest_id, self.OUTPUT_GATES: self.gate_dest_id} + return {self.OUTPUT_TRAJECTORIES: self.traj_dest_id, self.OUTPUT_GATES: self.gate_dest_id} def postProcessAlgorithm(self, context: QgsProcessingContext, feedback: QgsProcessingFeedback) -> dict[str, Any]: # noqa: N802 if self.gate_dest_id: diff --git a/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py index 447e6a3..74f5bcf 100644 --- a/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py +++ b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py @@ -1,6 +1,7 @@ from qgis.core import QgsProcessingProvider -from fvh3t.fvh3t_processing.count_trajectories import CountTrajectories +from fvh3t.fvh3t_processing.count_trajectories_area import CountTrajectoriesArea +from fvh3t.fvh3t_processing.count_trajectories_gate import CountTrajectoriesGate from fvh3t.fvh3t_processing.export_to_json import ExportToJSON @@ -41,5 +42,6 @@ def loadAlgorithms(self) -> None: # noqa N802 """ Adds individual processing algorithms to the provider. """ - self.addAlgorithm(CountTrajectories()) + self.addAlgorithm(CountTrajectoriesGate()) + self.addAlgorithm(CountTrajectoriesArea()) self.addAlgorithm(ExportToJSON()) diff --git a/fvh3t/fvh3t_processing/utils.py b/fvh3t/fvh3t_processing/utils.py new file mode 100644 index 0000000..00fc8bc --- /dev/null +++ b/fvh3t/fvh3t_processing/utils.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from qgis.core import QgsVectorLayer + from qgis.PyQt.QtCore import QDateTime + + +class ProcessingUtils: + @staticmethod + def get_start_and_end_timestamps( + start_time: QDateTime, + end_time: QDateTime, + min_timestamp: int, + max_timestamp: int, + ) -> tuple[int, int]: + """ + Checks that start and end time are in the correct range + and returns them as a UNIX timestamp (milliseconds). + """ + start_time_unix = start_time.toMSecsSinceEpoch() if start_time.isValid() else min_timestamp + end_time_unix = end_time.toMSecsSinceEpoch() if end_time.isValid() else max_timestamp + + # Check that the set start and end times are in data's range + if not (min_timestamp <= start_time_unix <= max_timestamp) or not ( + min_timestamp <= end_time_unix <= max_timestamp + ): + msg = "Set start and/or end timestamps are out of data's range." + raise ValueError(msg) + + return start_time_unix, end_time_unix + + @staticmethod + def get_min_and_max_timestamps(layer: QgsVectorLayer, timestamp_field: str) -> tuple[int, int]: + field_id = layer.fields().indexOf(timestamp_field) + min_timestamp, max_timestamp = layer.minimumAndMaximumValue(field_id) + + if min_timestamp is None or max_timestamp is None: + msg = "No valid timestamps found in the point layer." + raise ValueError(msg) + + return min_timestamp, max_timestamp + + @staticmethod + def normalize_datetimes(*args: QDateTime) -> None: + """ + Sets the seconds to zero in place for all QDateTime objects + entered into this function. + """ + + for date_time in args: + zero_s_time = date_time.time() + zero_s_time.setHMS(zero_s_time.hour(), zero_s_time.minute(), 0) + date_time.setTime(zero_s_time) + + @staticmethod + def get_filter_expression_time_and_class( + start_timestamp: int, + end_timestamp: int, + traveler_class: str | None, + min_timestamp: int, + max_timestamp: int, + ) -> str | None: + """ + Constructs the filter expression from beginning and ending + time stamps and traveler class, which can be passed + to TrajectoryLayer. + """ + filter_expression: str | None = None + if start_timestamp != min_timestamp or end_timestamp != max_timestamp: + filter_expression = f'"timestamp" BETWEEN {start_timestamp} AND {end_timestamp}' + if not filter_expression: + if traveler_class: + filter_expression = f"\"label\" = '{traveler_class}'" + elif traveler_class: + filter_expression += f" AND \"label\" = '{traveler_class}'" + + return filter_expression diff --git a/fvh3t/plugin.py b/fvh3t/plugin.py index b277c02..8a4f7d8 100644 --- a/fvh3t/plugin.py +++ b/fvh3t/plugin.py @@ -116,6 +116,14 @@ def initGui(self) -> None: # noqa N802 parent=iface.mainWindow(), add_to_toolbar=True, ) + + self.add_action( + resources_path("icons", "add_area.png"), + text=tr("Create area layer"), + callback=self.create_area_layer, + parent=iface.mainWindow(), + add_to_toolbar=True, + ) self.initProcessing() def onClosePlugin(self) -> None: # noqa N802 @@ -133,3 +141,8 @@ def create_gate_layer(self) -> None: layer: QgsVectorLayer = QgisLayerUtils.create_gate_layer(QgsProject.instance().crs()) QgsProject.instance().addMapLayer(layer) + + def create_area_layer(self) -> None: + layer: QgsVectorLayer = QgisLayerUtils.create_area_layer(QgsProject.instance().crs()) + + QgsProject.instance().addMapLayer(layer) diff --git a/fvh3t/resources/icons/add_area.png b/fvh3t/resources/icons/add_area.png new file mode 100644 index 0000000..b0647c9 Binary files /dev/null and b/fvh3t/resources/icons/add_area.png differ diff --git a/fvh3t/resources/style/area_style.xml b/fvh3t/resources/style/area_style.xml new file mode 100644 index 0000000..75eff50 --- /dev/null +++ b/fvh3t/resources/style/area_style.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/conftest.py b/tests/conftest.py index 0028d6a..9b365cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ from qgis.core import QgsFeature, QgsField, QgsGeometry, QgsPointXY, QgsVectorLayer from qgis.PyQt.QtCore import QVariant +from fvh3t.core.area import Area from fvh3t.core.gate import Gate from fvh3t.core.trajectory import Trajectory, TrajectoryNode, TrajectorySegment @@ -407,3 +408,67 @@ def qgis_gate_line_layer_wrong_field_type(): layer.commitChanges() return layer + + +@pytest.fixture +def four_point_area(): + return Area( + QgsGeometry.fromPolygonXY( + [ + [ + QgsPointXY(-0.5, -0.5), + QgsPointXY(0.5, -0.5), + QgsPointXY(0.5, 0.5), + QgsPointXY(-0.5, 0.5), + ] + ] + ), + "polygon", + ) + + +@pytest.fixture +def qgis_area_polygon_layer(): + layer = QgsVectorLayer("Polygon?crs=EPSG:3857", "Polygon Layer", "memory") + + layer.startEditing() + + layer.addAttribute(QgsField("fid", QVariant.Int)) + layer.addAttribute(QgsField("name", QVariant.String)) + + area1 = QgsFeature(layer.fields()) + area1.setAttributes([1, "area1"]) + area1.setGeometry( + QgsGeometry.fromPolygonXY( + [ + [ + QgsPointXY(1, 2), + QgsPointXY(2, 2), + QgsPointXY(2, 2.5), + QgsPointXY(1, 2.5), + ], + ] + ) + ) + + area2 = QgsFeature(layer.fields()) + area2.setAttributes([2, "area2"]) + area2.setGeometry( + QgsGeometry.fromPolygonXY( + [ + [ + QgsPointXY(0, 0), + QgsPointXY(1, 0), + QgsPointXY(1, 1), + QgsPointXY(0, 1), + ], + ] + ) + ) + + layer.addFeature(area1) + layer.addFeature(area2) + + layer.commitChanges() + + return layer diff --git a/tests/core/test_area.py b/tests/core/test_area.py new file mode 100644 index 0000000..3591b7e --- /dev/null +++ b/tests/core/test_area.py @@ -0,0 +1,14 @@ +from fvh3t.core.area import Area +from fvh3t.core.trajectory import Trajectory + + +def test_area_trajetory_count( + four_point_area: Area, two_node_trajectory: Trajectory, three_node_trajectory: Trajectory +): + four_point_area.count_trajectories((two_node_trajectory, three_node_trajectory)) + assert four_point_area.trajectory_count() == 2 + + +def test_area_average_speed(four_point_area: Area, two_node_trajectory: Trajectory, three_node_trajectory: Trajectory): + four_point_area.count_trajectories((two_node_trajectory, three_node_trajectory)) + assert four_point_area.average_speed() == 36.0 diff --git a/tests/core/test_area_layer.py b/tests/core/test_area_layer.py new file mode 100644 index 0000000..5473ae7 --- /dev/null +++ b/tests/core/test_area_layer.py @@ -0,0 +1,19 @@ +from fvh3t.core.area_layer import AreaLayer + + +def test_area_layer_create_areas(qgis_area_polygon_layer): + area_layer = AreaLayer( + qgis_area_polygon_layer, + "fid", + "name", + ) + + areas = area_layer.areas() + + assert len(areas) == 2 + + area1 = areas[0] + area2 = areas[1] + + assert area1.geometry().asWkt() == "Polygon ((1 2, 2 2, 2 2.5, 1 2.5, 1 2))" + assert area2.geometry().asWkt() == "Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))" diff --git a/tests/processing/test_count_trajectories_area.py b/tests/processing/test_count_trajectories_area.py new file mode 100644 index 0000000..9977752 --- /dev/null +++ b/tests/processing/test_count_trajectories_area.py @@ -0,0 +1,191 @@ +try: + import processing +except ImportError: + from qgis import processing + +import pytest +from qgis.core import QgsFeature, QgsField, QgsGeometry, QgsPointXY, QgsProject, 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_point_layer_for_algorithm(): + layer = QgsVectorLayer("Point?crs=EPSG:3857", "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)) + layer.addAttribute(QgsField("label", QVariant.String)) + + traj1_f1 = QgsFeature(layer.fields()) + traj1_f1.setAttributes([1, 0, 1, 1, 1, "car"]) + traj1_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0, -0.5))) + + traj1_f2 = QgsFeature(layer.fields()) + traj1_f2.setAttributes([1, 1000, 2, 2, 2, "car"]) + traj1_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.25, 0.25))) + + traj1_f3 = QgsFeature(layer.fields()) + traj1_f3.setAttributes([1, 2000, 1, 1, 1, "car"]) + traj1_f3.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, 0.5))) + + traj1_f4 = QgsFeature(layer.fields()) + traj1_f4.setAttributes([1, 3000, 2, 2, 2, "car"]) + traj1_f4.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.75, 0.75))) + + traj1_f5 = QgsFeature(layer.fields()) + traj1_f5.setAttributes([1, 4000, 2, 2, 2, "car"]) + traj1_f5.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 1.5))) + + traj2_f1 = QgsFeature(layer.fields()) + traj2_f1.setAttributes([2, 6000000, 1, 1, 1, "car"]) + traj2_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, -0.5))) + + traj2_f2 = QgsFeature(layer.fields()) + traj2_f2.setAttributes([2, 6001000, 2, 2, 2, "car"]) + traj2_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, 0.25))) + + traj2_f3 = QgsFeature(layer.fields()) + traj2_f3.setAttributes([2, 6002000, 1, 1, 1, "car"]) + traj2_f3.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, 0.75))) + + traj2_f4 = QgsFeature(layer.fields()) + traj2_f4.setAttributes([2, 6003000, 2, 2, 2, "car"]) + traj2_f4.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.5, 1.5))) + + traj3_f1 = QgsFeature(layer.fields()) + traj3_f1.setAttributes([3, 6000000, 1, 1, 1, "car"]) + traj3_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(0.75, 2.25))) + + traj3_f2 = QgsFeature(layer.fields()) + traj3_f2.setAttributes([3, 6001000, 2, 2, 2, "car"]) + traj3_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.25, 2.25))) + + traj3_f3 = QgsFeature(layer.fields()) + traj3_f3.setAttributes([3, 6002000, 1, 1, 1, "car"]) + traj3_f3.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.5, 2.25))) + + traj3_f4 = QgsFeature(layer.fields()) + traj3_f4.setAttributes([3, 6003000, 2, 2, 2, "car"]) + traj3_f4.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.75, 2.25))) + + traj4_f1 = QgsFeature(layer.fields()) + traj4_f1.setAttributes([4, 1000, 1, 1, 1, "pedestrian"]) + traj4_f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.5, 0.5))) + + traj4_f2 = QgsFeature(layer.fields()) + traj4_f2.setAttributes([4, 2000, 2, 2, 2, "pedestrian"]) + traj4_f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1.5, 1))) + + layer.addFeature(traj1_f1) + layer.addFeature(traj1_f2) + layer.addFeature(traj1_f3) + layer.addFeature(traj1_f4) + layer.addFeature(traj1_f5) + + layer.addFeature(traj2_f1) + layer.addFeature(traj2_f2) + layer.addFeature(traj2_f3) + layer.addFeature(traj2_f4) + + layer.addFeature(traj3_f1) + layer.addFeature(traj3_f2) + layer.addFeature(traj3_f3) + layer.addFeature(traj3_f4) + + layer.addFeature(traj4_f1) + layer.addFeature(traj4_f2) + + layer.commitChanges() + + return layer + + +def test_count_trajectories_area( + qgis_app, + qgis_processing, # noqa: ARG001 + qgis_area_polygon_layer: QgsVectorLayer, + input_point_layer_for_algorithm: QgsVectorLayer, +): + provider = TTTProvider() + + qgis_app.processingRegistry().addProvider(provider) + + ## TEST CASE 1 - NO FILTERING + + # script requires layers to be added to the project + QgsProject.instance().addMapLayers([qgis_area_polygon_layer, input_point_layer_for_algorithm]) + + params = { + "INPUT_POINTS": input_point_layer_for_algorithm, + "INPUT_AREAS": qgis_area_polygon_layer, + "TRAVELER_CLASS": None, + "START_TIME": None, + "END_TIME": None, + "OUTPUT_AREAS": "TEMPORARY_OUTPUT", + "OUTPUT_TRAJECTORIES": "TEMPORARY_OUTPUT", + } + + result = processing.run( + "traffic_trajectory_toolkit:count_trajectories_area", + params, + ) + + output_areas: QgsVectorLayer = result["OUTPUT_AREAS"] + output_trajectories: QgsVectorLayer = result["OUTPUT_TRAJECTORIES"] + + assert output_areas.featureCount() == 2 + assert output_trajectories.featureCount() == 3 + + area1: QgsFeature = output_areas.getFeature(1) + area2: QgsFeature = output_areas.getFeature(2) + + assert area1.geometry().asWkt() == "Polygon ((1 2, 2 2, 2 2.5, 1 2.5, 1 2))" + assert area2.geometry().asWkt() == "Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))" + + assert area1.attribute("vehicle_count") == 1 + assert area2.attribute("vehicle_count") == 2 + + assert area1.attribute("speed_avg") == 0.9 + assert round(area2.attribute("speed_avg"), 2) == 1.54 + + ### TEST CASE 2 - FILTER BY TIME + + case2_params = { + "INPUT_POINTS": input_point_layer_for_algorithm, + "INPUT_AREAS": qgis_area_polygon_layer, + "TRAVELER_CLASS": "car", # filter by class too + "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_AREAS": "TEMPORARY_OUTPUT", + "OUTPUT_TRAJECTORIES": "TEMPORARY_OUTPUT", + } + + case2_result = processing.run( + "traffic_trajectory_toolkit:count_trajectories_area", + case2_params, + ) + + case2_output_areas: QgsVectorLayer = case2_result["OUTPUT_AREAS"] + case2_output_trajectories: QgsVectorLayer = case2_result["OUTPUT_TRAJECTORIES"] + + assert case2_output_areas.featureCount() == 2 + assert case2_output_trajectories.featureCount() == 1 + + case2area1: QgsFeature = case2_output_areas.getFeature(1) + case2area2: QgsFeature = case2_output_areas.getFeature(2) + + assert case2area1.attribute("vehicle_count") == 0 + assert case2area2.attribute("vehicle_count") == 1 + + traj = case2_output_trajectories.getFeature(1) + + assert traj.geometry().asWkt() == "LineString (0.25 0.25, 0.5 0.5, 0.75 0.75)" + + qgis_app.processingRegistry().removeProvider(provider.id()) diff --git a/tests/processing/test_count_trajectories.py b/tests/processing/test_count_trajectories_gate.py similarity index 97% rename from tests/processing/test_count_trajectories.py rename to tests/processing/test_count_trajectories_gate.py index 97a8854..833da55 100644 --- a/tests/processing/test_count_trajectories.py +++ b/tests/processing/test_count_trajectories_gate.py @@ -4,7 +4,7 @@ from qgis import processing import pytest -from qgis.core import QgsFeature, QgsField, QgsGeometry, QgsPointXY, QgsVectorLayer +from qgis.core import QgsApplication, 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 @@ -157,8 +157,8 @@ def input_point_layer_for_algorithm(): return layer -def test_count_trajectories( - qgis_app, +def test_count_trajectories_gate( + qgis_app: QgsApplication, qgis_processing, # noqa: ARG001 input_point_layer_for_algorithm: QgsVectorLayer, input_gate_layer_for_algorithm: QgsVectorLayer, @@ -180,7 +180,7 @@ def test_count_trajectories( } result = processing.run( - "traffic_trajectory_toolkit:count_trajectories", + "traffic_trajectory_toolkit:count_trajectories_gate", params, ) @@ -313,7 +313,7 @@ def test_count_trajectories( } case2_result = processing.run( - "traffic_trajectory_toolkit:count_trajectories", + "traffic_trajectory_toolkit:count_trajectories_gate", case2_params, ) @@ -342,3 +342,5 @@ def test_count_trajectories( 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)" + + qgis_app.processingRegistry().removeProvider(provider.id()) diff --git a/tests/processing/test_export_to_json.py b/tests/processing/test_export_to_json.py index fddca1e..46306af 100644 --- a/tests/processing/test_export_to_json.py +++ b/tests/processing/test_export_to_json.py @@ -10,7 +10,7 @@ from qgis.core import QgsVectorLayer from fvh3t.fvh3t_processing.traffic_trajectory_toolkit_provider import TTTProvider -from tests.processing.test_count_trajectories import ( # noqa: F401 +from tests.processing.test_count_trajectories_gate import ( # noqa: F401 input_gate_layer_for_algorithm, input_point_layer_for_algorithm, ) @@ -37,7 +37,7 @@ def test_export_to_json( } result = processing.run( - "traffic_trajectory_toolkit:count_trajectories", + "traffic_trajectory_toolkit:count_trajectories_gate", params, )