From 7dc05db98309af10e74d219208226ed21d8de481 Mon Sep 17 00:00:00 2001 From: msorvoja Date: Wed, 30 Oct 2024 12:25:18 +0200 Subject: [PATCH 1/9] Add count_trajectories.py draft --- fvh3t/build.py | 2 +- fvh3t/fvh3t_processing/count_trajectories.py | 205 ++++++++++++++++++ .../fvh3t_processing/processing_algorithm.py | 195 ----------------- ...=> traffic_trajectory_toolkit_provider.py} | 8 +- fvh3t/plugin.py | 2 +- 5 files changed, 211 insertions(+), 201 deletions(-) create mode 100644 fvh3t/fvh3t_processing/count_trajectories.py delete mode 100644 fvh3t/fvh3t_processing/processing_algorithm.py rename fvh3t/fvh3t_processing/{provider.py => traffic_trajectory_toolkit_provider.py} (82%) diff --git a/fvh3t/build.py b/fvh3t/build.py index 7e79ff4..c43032f 100755 --- a/fvh3t/build.py +++ b/fvh3t/build.py @@ -13,7 +13,7 @@ py_files = [fil for fil in glob.glob("**/*.py", recursive=True) if "test/" not in fil and "test\\" not in fil] locales = ["fi"] -profile = "default" +profile = "FVH-3T" ui_files = list(glob.glob("**/*.ui", recursive=True)) resources = list(glob.glob("**/*.qrc", recursive=True)) extra_dirs = ["resources"] diff --git a/fvh3t/fvh3t_processing/count_trajectories.py b/fvh3t/fvh3t_processing/count_trajectories.py new file mode 100644 index 0000000..32d725c --- /dev/null +++ b/fvh3t/fvh3t_processing/count_trajectories.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +from typing import Any + +from qgis import processing # noqa: TCH002 +from qgis.core import ( + QgsFeatureSink, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingFeedback, + QgsProcessingParameterDateTime, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterVectorLayer, + QgsUnitTypes, + QgsWkbTypes, +) +from qgis.PyQt.QtCore import QCoreApplication + +from fvh3t.core.trajectory_layer import TrajectoryLayer + + +class CountTrajectories(QgsProcessingAlgorithm): + def __init__(self) -> None: + super().__init__() + + self._name = "create_trajectories" + self._display_name = "Create trajectories" + + def tr(self, string) -> str: + return QCoreApplication.translate("Processing", string) + + def createInstance(self): # noqa N802 + return CountTrajectories() + + 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.alg_parameters = [ + "input_point_layer", + "input_line_layer", + "start_time", + "end_time", + "output_gate_layer", + "output_trajectory_layer", + ] + + self.addParameter( + QgsProcessingParameterVectorLayer( + name=self.alg_parameters[0], + description="Input point layer", + types=[QgsProcessing.TypeVectorPoint], + ) + ) + + self.addParameter( + QgsProcessingParameterVectorLayer( + name=self.alg_parameters[1], + description="Input line layer", + types=[QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterDateTime( + name=self.alg_parameters[2], + description="Start time", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterDateTime( + name=self.alg_parameters[3], + description="End time", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + name=self.alg_parameters[4], + description="Output gate layer", + type=QgsProcessing.TypeVectorLine, + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + name=self.alg_parameters[5], + description="Output trajectory layer", + type=QgsProcessing.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.alg_parameters[0], context) + point_layer.fields() + + # line_layer = self.parameterAsSource(parameters, self.alg_parameters[1], context) + # start_time = self.parameterAsDateTime(parameters, self.alg_parameters[2], context) + # end_time = self.parameterAsDateTime(parameters, self.alg_parameters[3], context) + + trajectory_layer = TrajectoryLayer( + point_layer, "id", "timestamp", "size_x", "size_y", "size_z", QgsUnitTypes.TemporalUnit.TemporalMilliseconds + ).as_line_layer() + + (sink, dest_id) = self.parameterAsSink( + parameters, + self.alg_parameters[5], + context, + point_layer.fields(), + QgsWkbTypes.LineString, + point_layer.sourceCrs(), + ) + + sink.addFeature(trajectory_layer, QgsFeatureSink.FastInsert) + # Send some information to the user + # feedback.pushInfo(f"CRS is {source.sourceCrs().authid()}") + + # # Compute the number of steps to display within the progress bar and + # # get features from source + # total = 100.0 / source.featureCount() if source.featureCount() else 0 + # features = source.getFeatures() + + # for current, feature in enumerate(features): + # # Stop the algorithm if cancel button has been clicked + # if feedback.isCanceled(): + # break + + # # Add a feature in the sink + # sink.addFeature(feature, QgsFeatureSink.FastInsert) + + # # Update the progress bar + # feedback.setProgress(int(current * total)) + + # gate_layer = GateLayer(line_layer, "counts_left", "counts_right") + + # # Send some information to the user + # feedback.pushInfo(f"CRS is {source.sourceCrs().authid()}") + + # # Compute the number of steps to display within the progress bar and + # # get features from source + # total = 100.0 / source.featureCount() if source.featureCount() else 0 + # features = source.getFeatures() + + # for current, feature in enumerate(features): + # # Stop the algorithm if cancel button has been clicked + # if feedback.isCanceled(): + # break + + # # Add a feature in the sink + # sink.addFeature(feature, QgsFeatureSink.FastInsert) + + # # Update the progress bar + # feedback.setProgress(int(current * total)) + + # To run another Processing algorithm as part of this algorithm, you can use + # processing.run(...). Make sure you pass the current context and feedback + # to processing.run to ensure that all temporary layer outputs are available + # to the executed algorithm, and that the executed algorithm can send feedback + # reports to the user (and correctly handle cancellation and progress reports!) + if False: + _buffered_layer = processing.run( + "native:buffer", + { + "INPUT": dest_id, + "DISTANCE": 1.5, + "SEGMENTS": 5, + "END_CAP_STYLE": 0, + "JOIN_STYLE": 0, + "MITER_LIMIT": 2, + "DISSOLVE": False, + "OUTPUT": "memory:", + }, + context=context, + feedback=feedback, + )["OUTPUT"] + + # 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: dest_id} diff --git a/fvh3t/fvh3t_processing/processing_algorithm.py b/fvh3t/fvh3t_processing/processing_algorithm.py deleted file mode 100644 index acb5683..0000000 --- a/fvh3t/fvh3t_processing/processing_algorithm.py +++ /dev/null @@ -1,195 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from qgis import processing # noqa: TCH002 -from qgis.core import ( - QgsFeatureSink, - QgsProcessing, - QgsProcessingAlgorithm, - QgsProcessingContext, - QgsProcessingFeedback, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterFeatureSource, -) -from qgis.PyQt.QtCore import QCoreApplication - - -class ProcessingAlgorithm(QgsProcessingAlgorithm): - """ - This is an example algorithm that takes a vector layer and - creates a new identical one. - - It is meant to be used as an example of how to create your own - algorithms and explain methods and variables used to do it. An - algorithm like this will be available in all elements, and there - is not need for additional work. - - All Processing algorithms should extend the QgsProcessingAlgorithm - class. - """ - - # Constants used to refer to parameters and outputs. They will be - # used when calling the algorithm from another algorithm, or when - # calling from the QGIS console. - - INPUT = "INPUT" - OUTPUT = "OUTPUT" - - def __init__(self) -> None: - super().__init__() - - self._name = "myprocessingalgorithm" - self._display_name = "My Processing Algorithm" - self._group_id = "" - self._group = "" - self._short_help_string = "" - - def tr(self, string) -> str: - """ - Returns a translatable string with the self.tr() function. - """ - return QCoreApplication.translate("Processing", string) - - def createInstance(self): # noqa N802 - return ProcessingAlgorithm() - - def name(self) -> str: - """ - Returns the algorithm name, used for identifying the algorithm. This - string should be fixed for the algorithm, and must not be localised. - The name should be unique within each provider. Names should contain - lowercase alphanumeric characters only and no spaces or other - formatting characters. - """ - return self._name - - def displayName(self) -> str: # noqa N802 - """ - Returns the translated algorithm name, which should be used for any - user-visible display of the algorithm name. - """ - return self.tr(self._display_name) - - def groupId(self) -> str: # noqa N802 - """ - Returns the unique ID of the group this algorithm belongs to. This - string should be fixed for the algorithm, and must not be localised. - The group id should be unique within each provider. Group id should - contain lowercase alphanumeric characters only and no spaces or other - formatting characters. - """ - return self._group_id - - def group(self) -> str: - """ - Returns the name of the group this algorithm belongs to. This string - should be localised. - """ - return self.tr(self._group) - - def shortHelpString(self) -> str: # noqa N802 - """ - Returns a localised short helper string for the algorithm. This string - should provide a basic description about what the algorithm does and the - parameters and outputs associated with it.. - """ - return self.tr(self._short_help_string) - - def initAlgorithm(self, config=None): # noqa N802 - """ - Here we define the inputs and output of the algorithm, along - with some other properties. - """ - - # We add the input vector features source. It can have any kind of - # geometry. - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT, - self.tr("Input layer"), - [QgsProcessing.TypeVectorAnyGeometry], - ) - ) - - # We add a feature sink in which to store our processed features (this - # usually takes the form of a newly created vector layer when the - # algorithm is run in QGIS). - self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr("Output layer"))) - - 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() - - # Retrieve the feature source and sink. The 'dest_id' variable is used - # to uniquely identify the feature sink, and must be included in the - # dictionary returned by the processAlgorithm function. - source = self.parameterAsSource(parameters, self.INPUT, context) - - (sink, dest_id) = self.parameterAsSink( - parameters, - self.OUTPUT, - context, - source.fields(), - source.wkbType(), - source.sourceCrs(), - ) - - # Send some information to the user - feedback.pushInfo(f"CRS is {source.sourceCrs().authid()}") - - # Compute the number of steps to display within the progress bar and - # get features from source - total = 100.0 / source.featureCount() if source.featureCount() else 0 - features = source.getFeatures() - - for current, feature in enumerate(features): - # Stop the algorithm if cancel button has been clicked - if feedback.isCanceled(): - break - - # Add a feature in the sink - sink.addFeature(feature, QgsFeatureSink.FastInsert) - - # Update the progress bar - feedback.setProgress(int(current * total)) - - # To run another Processing algorithm as part of this algorithm, you can use - # processing.run(...). Make sure you pass the current context and feedback - # to processing.run to ensure that all temporary layer outputs are available - # to the executed algorithm, and that the executed algorithm can send feedback - # reports to the user (and correctly handle cancellation and progress reports!) - if False: - _buffered_layer = processing.run( - "native:buffer", - { - "INPUT": dest_id, - "DISTANCE": 1.5, - "SEGMENTS": 5, - "END_CAP_STYLE": 0, - "JOIN_STYLE": 0, - "MITER_LIMIT": 2, - "DISSOLVE": False, - "OUTPUT": "memory:", - }, - context=context, - feedback=feedback, - )["OUTPUT"] - - # 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: dest_id} diff --git a/fvh3t/fvh3t_processing/provider.py b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py similarity index 82% rename from fvh3t/fvh3t_processing/provider.py rename to fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py index 496abfe..1e032f6 100644 --- a/fvh3t/fvh3t_processing/provider.py +++ b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py @@ -1,14 +1,14 @@ from qgis.core import QgsProcessingProvider -from fvh3t.fvh3t_processing.processing_algorithm import ProcessingAlgorithm +from fvh3t.fvh3t_processing.count_trajectories import CountTrajectories class Provider(QgsProcessingProvider): def __init__(self) -> None: super().__init__() - self._id = "myprovider" - self._name = "My provider" + self._id = "traffic_trajectory_toolkit_provider" + self._name = "Traffic trajectory toolkit provider" def id(self) -> str: """The ID of your plugin, used to identify the provider. @@ -40,5 +40,5 @@ def loadAlgorithms(self) -> None: # noqa N802 """ Adds individual processing algorithms to the provider. """ - alg = ProcessingAlgorithm() + alg = CountTrajectories() self.addAlgorithm(alg) diff --git a/fvh3t/plugin.py b/fvh3t/plugin.py index 1d58337..71ab609 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.provider import Provider +from fvh3t.fvh3t_processing.traffic_trajectory_toolkit_provider import Provider 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 From 84c4cda12cf5b7ce86ab09aa69624144fc5791e9 Mon Sep 17 00:00:00 2001 From: Juho Ervasti Date: Wed, 30 Oct 2024 12:39:26 +0200 Subject: [PATCH 2/9] Add a blanket option to check for numeric fields --- fvh3t/core/gate_layer.py | 5 ++++- fvh3t/core/trajectory_layer.py | 13 ++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/fvh3t/core/gate_layer.py b/fvh3t/core/gate_layer.py index fe289e7..6304867 100644 --- a/fvh3t/core/gate_layer.py +++ b/fvh3t/core/gate_layer.py @@ -66,8 +66,11 @@ def is_field_valid(self, field_name: str, *, accepted_types: list[str]) -> bool: return True field: QgsField = self.__layer.fields().field(field_id) - field_type: str = field.displayType() + if "numeric" in accepted_types and field.isNumeric(): + return True + + field_type: str = field.displayType() return field_type in accepted_types def is_valid(self) -> bool: diff --git a/fvh3t/core/trajectory_layer.py b/fvh3t/core/trajectory_layer.py index 2462939..662bfcb 100644 --- a/fvh3t/core/trajectory_layer.py +++ b/fvh3t/core/trajectory_layer.py @@ -235,8 +235,11 @@ def is_field_valid(self, field_name: str, *, accepted_types: list[str]) -> bool: return True field: QgsField = self.__layer.fields().field(field_id) - field_type: str = field.displayType() + if "numeric" in accepted_types and field.isNumeric(): + return True + + field_type: str = field.displayType() return field_type in accepted_types def is_valid(self) -> bool: @@ -259,19 +262,19 @@ def is_valid(self) -> bool: msg = "Id field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__timestamp_field, accepted_types=["integer", "double"]): + if not self.is_field_valid(self.__timestamp_field, accepted_types=["numeric"]): msg = "Timestamp field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__width_field, accepted_types=["integer", "double"]): + if not self.is_field_valid(self.__width_field, accepted_types=["numeric"]): msg = "Width field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__length_field, accepted_types=["integer", "double"]): + if not self.is_field_valid(self.__length_field, accepted_types=["numeric"]): msg = "Length field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__height_field, accepted_types=["integer", "double"]): + if not self.is_field_valid(self.__height_field, accepted_types=["numeric"]): msg = "Height field either not found or of incorrect type." raise InvalidLayerException(msg) From d9d94a7e21492ad4f2c924193a121c5152d9ac66 Mon Sep 17 00:00:00 2001 From: msorvoja Date: Wed, 30 Oct 2024 15:08:00 +0200 Subject: [PATCH 3/9] Implement ability to create trajectory (line) layer from point layer with desired time range --- fvh3t/fvh3t_processing/count_trajectories.py | 207 ++++++++++--------- 1 file changed, 112 insertions(+), 95 deletions(-) diff --git a/fvh3t/fvh3t_processing/count_trajectories.py b/fvh3t/fvh3t_processing/count_trajectories.py index 32d725c..ff07051 100644 --- a/fvh3t/fvh3t_processing/count_trajectories.py +++ b/fvh3t/fvh3t_processing/count_trajectories.py @@ -2,8 +2,8 @@ from typing import Any -from qgis import processing # noqa: TCH002 from qgis.core import ( + QgsFeature, QgsFeatureSink, QgsProcessing, QgsProcessingAlgorithm, @@ -13,7 +13,7 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterVectorLayer, QgsUnitTypes, - QgsWkbTypes, + QgsVectorLayer, ) from qgis.PyQt.QtCore import QCoreApplication @@ -21,11 +21,18 @@ class CountTrajectories(QgsProcessingAlgorithm): + INPUT_POINTS = "INPUT_POINTS" + INPUT_LINES = "INPUT_LINES" + START_TIME = "START_TIME" + END_TIME = "END_TIME" + OUTPUT_GATES = "OUTPUT_GATES" + OUTPUT_TRAJECTORIES = "OUTPUT_TRAJECTORIES" + def __init__(self) -> None: super().__init__() - self._name = "create_trajectories" - self._display_name = "Create trajectories" + self._name = "count_trajectories" + self._display_name = "Count trajectories" def tr(self, string) -> str: return QCoreApplication.translate("Processing", string) @@ -40,18 +47,9 @@ def displayName(self) -> str: # noqa N802 return self.tr(self._display_name) def initAlgorithm(self, config=None): # noqa N802 - self.alg_parameters = [ - "input_point_layer", - "input_line_layer", - "start_time", - "end_time", - "output_gate_layer", - "output_trajectory_layer", - ] - self.addParameter( QgsProcessingParameterVectorLayer( - name=self.alg_parameters[0], + name=self.INPUT_POINTS, description="Input point layer", types=[QgsProcessing.TypeVectorPoint], ) @@ -59,7 +57,7 @@ def initAlgorithm(self, config=None): # noqa N802 self.addParameter( QgsProcessingParameterVectorLayer( - name=self.alg_parameters[1], + name=self.INPUT_LINES, description="Input line layer", types=[QgsProcessing.TypeVectorLine], optional=True, @@ -68,7 +66,7 @@ def initAlgorithm(self, config=None): # noqa N802 self.addParameter( QgsProcessingParameterDateTime( - name=self.alg_parameters[2], + name=self.START_TIME, description="Start time", optional=True, ) @@ -76,7 +74,7 @@ def initAlgorithm(self, config=None): # noqa N802 self.addParameter( QgsProcessingParameterDateTime( - name=self.alg_parameters[3], + name=self.END_TIME, description="End time", optional=True, ) @@ -84,8 +82,8 @@ def initAlgorithm(self, config=None): # noqa N802 self.addParameter( QgsProcessingParameterFeatureSink( - name=self.alg_parameters[4], - description="Output gate layer", + name=self.OUTPUT_GATES, + description="Gates", type=QgsProcessing.TypeVectorLine, optional=True, ) @@ -93,8 +91,8 @@ def initAlgorithm(self, config=None): # noqa N802 self.addParameter( QgsProcessingParameterFeatureSink( - name=self.alg_parameters[5], - description="Output trajectory layer", + name=self.OUTPUT_TRAJECTORIES, + description="Trajectories", type=QgsProcessing.TypeVectorLine, ) ) @@ -113,88 +111,107 @@ def processAlgorithm( # noqa N802 if feedback is None: feedback = QgsProcessingFeedback() - point_layer = self.parameterAsVectorLayer(parameters, self.alg_parameters[0], context) - point_layer.fields() - - # line_layer = self.parameterAsSource(parameters, self.alg_parameters[1], context) - # start_time = self.parameterAsDateTime(parameters, self.alg_parameters[2], context) - # end_time = self.parameterAsDateTime(parameters, self.alg_parameters[3], context) + point_layer = self.parameterAsVectorLayer(parameters, self.INPUT_POINTS, context) + # line_layer = self.parameterAsVectorLayer(parameters, self.INPUT_LINES, context) + start_time = self.parameterAsDateTime(parameters, self.START_TIME, context) + end_time = self.parameterAsDateTime(parameters, self.END_TIME, context) + + # TODO: Remove later + feedback.pushInfo(f"Original point layer has {point_layer.featureCount()} features.") + + # Get min and max timestamps from the data + min_timestamp = None + max_timestamp = None + + for feature in point_layer.getFeatures(): + timestamp = feature["timestamp"] + + if min_timestamp is None or timestamp < min_timestamp: + min_timestamp = timestamp + if max_timestamp is None or timestamp > max_timestamp: + max_timestamp = timestamp + + if min_timestamp is None or max_timestamp is None: + msg = "No valid timestamps found in the point layer." + raise ValueError(msg) + + # 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.toSecsSinceEpoch() * 1000 if start_time.isValid() else min_timestamp + end_time_unix = end_time.toSecsSinceEpoch() * 1000 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) + + # 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 layer contains {non_unique_layer.featureCount()} features with non-unique IDs." + ) trajectory_layer = TrajectoryLayer( - point_layer, "id", "timestamp", "size_x", "size_y", "size_z", QgsUnitTypes.TemporalUnit.TemporalMilliseconds + non_unique_layer, + "id", + "timestamp", + "size_x", + "size_y", + "size_z", + QgsUnitTypes.TemporalUnit.TemporalMilliseconds, ).as_line_layer() + if trajectory_layer is None: + msg = "Trajectory layer is None." + raise ValueError(msg) + (sink, dest_id) = self.parameterAsSink( parameters, - self.alg_parameters[5], + self.OUTPUT_TRAJECTORIES, context, - point_layer.fields(), - QgsWkbTypes.LineString, - point_layer.sourceCrs(), + trajectory_layer.fields(), + trajectory_layer.wkbType(), + trajectory_layer.sourceCrs(), ) - sink.addFeature(trajectory_layer, QgsFeatureSink.FastInsert) - # Send some information to the user - # feedback.pushInfo(f"CRS is {source.sourceCrs().authid()}") - - # # Compute the number of steps to display within the progress bar and - # # get features from source - # total = 100.0 / source.featureCount() if source.featureCount() else 0 - # features = source.getFeatures() - - # for current, feature in enumerate(features): - # # Stop the algorithm if cancel button has been clicked - # if feedback.isCanceled(): - # break - - # # Add a feature in the sink - # sink.addFeature(feature, QgsFeatureSink.FastInsert) - - # # Update the progress bar - # feedback.setProgress(int(current * total)) - - # gate_layer = GateLayer(line_layer, "counts_left", "counts_right") - - # # Send some information to the user - # feedback.pushInfo(f"CRS is {source.sourceCrs().authid()}") - - # # Compute the number of steps to display within the progress bar and - # # get features from source - # total = 100.0 / source.featureCount() if source.featureCount() else 0 - # features = source.getFeatures() - - # for current, feature in enumerate(features): - # # Stop the algorithm if cancel button has been clicked - # if feedback.isCanceled(): - # break - - # # Add a feature in the sink - # sink.addFeature(feature, QgsFeatureSink.FastInsert) - - # # Update the progress bar - # feedback.setProgress(int(current * total)) - - # To run another Processing algorithm as part of this algorithm, you can use - # processing.run(...). Make sure you pass the current context and feedback - # to processing.run to ensure that all temporary layer outputs are available - # to the executed algorithm, and that the executed algorithm can send feedback - # reports to the user (and correctly handle cancellation and progress reports!) - if False: - _buffered_layer = processing.run( - "native:buffer", - { - "INPUT": dest_id, - "DISTANCE": 1.5, - "SEGMENTS": 5, - "END_CAP_STYLE": 0, - "JOIN_STYLE": 0, - "MITER_LIMIT": 2, - "DISSOLVE": False, - "OUTPUT": "memory:", - }, - context=context, - feedback=feedback, - )["OUTPUT"] + for feature in trajectory_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 @@ -202,4 +219,4 @@ def processAlgorithm( # noqa N802 # statistics, etc. These should all be included in the returned # dictionary, with keys matching the feature corresponding parameter # or output names. - return {self.OUTPUT: dest_id} + return {self.OUTPUT_TRAJECTORIES: dest_id} From 67ef6742798e741fa189ca0754a4065167a3ffa6 Mon Sep 17 00:00:00 2001 From: Juho Ervasti Date: Thu, 31 Oct 2024 08:48:01 +0200 Subject: [PATCH 4/9] Make field type checking provided-agnostic --- fvh3t/core/gate_layer.py | 12 +++++------- fvh3t/core/trajectory_layer.py | 31 +++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/fvh3t/core/gate_layer.py b/fvh3t/core/gate_layer.py index 6304867..27e0144 100644 --- a/fvh3t/core/gate_layer.py +++ b/fvh3t/core/gate_layer.py @@ -1,6 +1,7 @@ from __future__ import annotations from qgis.core import QgsFeatureSource, QgsField, QgsVectorLayer, QgsWkbTypes +from qgis.PyQt.QtCore import QMetaType from fvh3t.core.exceptions import InvalidLayerException from fvh3t.core.gate import Gate @@ -51,7 +52,7 @@ def create_gates(self) -> None: def gates(self) -> tuple[Gate, ...]: return self.__gates - def is_field_valid(self, field_name: str, *, accepted_types: list[str]) -> bool: + 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 @@ -66,11 +67,8 @@ def is_field_valid(self, field_name: str, *, accepted_types: list[str]) -> bool: return True field: QgsField = self.__layer.fields().field(field_id) + field_type: str = field.type() - if "numeric" in accepted_types and field.isNumeric(): - return True - - field_type: str = field.displayType() return field_type in accepted_types def is_valid(self) -> bool: @@ -89,11 +87,11 @@ def is_valid(self) -> bool: msg = "Layer has no features." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__counts_left_field, accepted_types=["boolean"]): + if not self.is_field_valid(self.__counts_left_field, accepted_types=[QMetaType.Type.Bool]): msg = "Counts left field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__counts_right_field, accepted_types=["boolean"]): + if not self.is_field_valid(self.__counts_right_field, accepted_types=[QMetaType.Type.Bool]): msg = "Counts right field either not found or of incorrect type." raise InvalidLayerException(msg) diff --git a/fvh3t/core/trajectory_layer.py b/fvh3t/core/trajectory_layer.py index 662bfcb..9a1e6e6 100644 --- a/fvh3t/core/trajectory_layer.py +++ b/fvh3t/core/trajectory_layer.py @@ -17,13 +17,27 @@ QgsVectorLayer, QgsWkbTypes, ) -from qgis.PyQt.QtCore import QVariant +from qgis.PyQt.QtCore import QMetaType, QVariant from fvh3t.core.exceptions import InvalidFeatureException, InvalidLayerException, InvalidTrajectoryException from fvh3t.core.trajectory import Trajectory, TrajectoryNode UNIX_TIMESTAMP_UNIT_THRESHOLD = 13 N_NODES_MIN = 2 +QT_NUMERIC_TYPES = [ + QMetaType.Type.Int, + QMetaType.Type.UInt, + QMetaType.Type.Double, + QMetaType.Type.Long, + QMetaType.Type.LongLong, + QMetaType.Type.ULong, + QMetaType.Type.ULongLong, + QMetaType.Type.Short, + QMetaType.Type.UShort, + QMetaType.Type.SChar, + QMetaType.Type.UChar, + QMetaType.Type.Float, +] def digits_in_timestamp_int(num: int): @@ -220,7 +234,7 @@ def as_line_layer(self) -> QgsVectorLayer | None: return line_layer - def is_field_valid(self, field_name: str, *, accepted_types: list[str]) -> bool: + 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 @@ -235,11 +249,8 @@ def is_field_valid(self, field_name: str, *, accepted_types: list[str]) -> bool: return True field: QgsField = self.__layer.fields().field(field_id) + field_type: str = field.type() - if "numeric" in accepted_types and field.isNumeric(): - return True - - field_type: str = field.displayType() return field_type in accepted_types def is_valid(self) -> bool: @@ -262,19 +273,19 @@ def is_valid(self) -> bool: msg = "Id field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__timestamp_field, accepted_types=["numeric"]): + if not self.is_field_valid(self.__timestamp_field, accepted_types=QT_NUMERIC_TYPES): msg = "Timestamp field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__width_field, accepted_types=["numeric"]): + if not self.is_field_valid(self.__width_field, accepted_types=QT_NUMERIC_TYPES): msg = "Width field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__length_field, accepted_types=["numeric"]): + if not self.is_field_valid(self.__length_field, accepted_types=QT_NUMERIC_TYPES): msg = "Length field either not found or of incorrect type." raise InvalidLayerException(msg) - if not self.is_field_valid(self.__height_field, accepted_types=["numeric"]): + if not self.is_field_valid(self.__height_field, accepted_types=QT_NUMERIC_TYPES): msg = "Height field either not found or of incorrect type." raise InvalidLayerException(msg) From 561afc52774da582f7621cd6ada324e03416a916 Mon Sep 17 00:00:00 2001 From: Juho Ervasti Date: Thu, 31 Oct 2024 08:50:38 +0200 Subject: [PATCH 5/9] Fix typing --- fvh3t/core/gate_layer.py | 2 +- fvh3t/core/trajectory_layer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fvh3t/core/gate_layer.py b/fvh3t/core/gate_layer.py index 27e0144..2222ca4 100644 --- a/fvh3t/core/gate_layer.py +++ b/fvh3t/core/gate_layer.py @@ -67,7 +67,7 @@ def is_field_valid(self, field_name: str, *, accepted_types: list[QMetaType.Type return True field: QgsField = self.__layer.fields().field(field_id) - field_type: str = field.type() + field_type: QMetaType.Type = field.type() return field_type in accepted_types diff --git a/fvh3t/core/trajectory_layer.py b/fvh3t/core/trajectory_layer.py index 9a1e6e6..19358d2 100644 --- a/fvh3t/core/trajectory_layer.py +++ b/fvh3t/core/trajectory_layer.py @@ -249,7 +249,7 @@ def is_field_valid(self, field_name: str, *, accepted_types: list[QMetaType.Type return True field: QgsField = self.__layer.fields().field(field_id) - field_type: str = field.type() + field_type: QMetaType.Type = field.type() return field_type in accepted_types From d9eb953a014b5815b749af7b2a7d38fb402118ee Mon Sep 17 00:00:00 2001 From: msorvoja Date: Thu, 31 Oct 2024 09:11:46 +0200 Subject: [PATCH 6/9] Add export function --- fvh3t/core/gate_layer.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/fvh3t/core/gate_layer.py b/fvh3t/core/gate_layer.py index 2222ca4..a903510 100644 --- a/fvh3t/core/gate_layer.py +++ b/fvh3t/core/gate_layer.py @@ -1,9 +1,9 @@ from __future__ import annotations -from qgis.core import QgsFeatureSource, QgsField, QgsVectorLayer, QgsWkbTypes -from qgis.PyQt.QtCore import QMetaType +from qgis.core import QgsFeature, QgsFeatureSource, QgsField, QgsVectorLayer, QgsWkbTypes +from qgis.PyQt.QtCore import QMetaType, QVariant -from fvh3t.core.exceptions import InvalidLayerException +from fvh3t.core.exceptions import InvalidFeatureException, InvalidLayerException from fvh3t.core.gate import Gate @@ -52,6 +52,40 @@ def create_gates(self) -> None: def gates(self) -> tuple[Gate, ...]: return self.__gates + def as_line_layer(self) -> QgsVectorLayer | None: + line_layer = QgsVectorLayer("LineString?crs=3067", "Line Layer", "memory") + + line_layer.startEditing() + + line_layer.addAttribute(QgsField("fid", QVariant.Int)) + line_layer.addAttribute(QgsField("counts_left", QVariant.Bool)) + line_layer.addAttribute(QgsField("counts_right", QVariant.Bool)) + line_layer.addAttribute(QgsField("trajectory_count", QVariant.Int)) + + fields = line_layer.fields() + + for i, gate in enumerate(self.__gates): + feature = QgsFeature(fields) + + feature.setAttributes( + [ + i, + gate.counts_left(), + gate.counts_right(), + gate.trajectory_count(), + ] + ) + feature.setGeometry(gate.geometry()) + + if not feature.isValid(): + raise InvalidFeatureException + + line_layer.addFeature(feature) + + line_layer.commitChanges() + + return line_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 From 58348c2506f286899de55639617c65f801ea233f Mon Sep 17 00:00:00 2001 From: msorvoja Date: Thu, 31 Oct 2024 09:12:21 +0200 Subject: [PATCH 7/9] Edit provider name --- fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py index 1e032f6..7b3e518 100644 --- a/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py +++ b/fvh3t/fvh3t_processing/traffic_trajectory_toolkit_provider.py @@ -7,8 +7,8 @@ class Provider(QgsProcessingProvider): def __init__(self) -> None: super().__init__() - self._id = "traffic_trajectory_toolkit_provider" - self._name = "Traffic trajectory toolkit provider" + self._id = "traffic_trajectory_toolkit" + self._name = "Traffic trajectory toolkit" def id(self) -> str: """The ID of your plugin, used to identify the provider. From f75bb1eeb3f34a2ef564155d6dde7a9ce0161c83 Mon Sep 17 00:00:00 2001 From: msorvoja Date: Thu, 31 Oct 2024 09:54:26 +0200 Subject: [PATCH 8/9] Export gate layer with trajectory count --- fvh3t/fvh3t_processing/count_trajectories.py | 56 ++++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/fvh3t/fvh3t_processing/count_trajectories.py b/fvh3t/fvh3t_processing/count_trajectories.py index ff07051..349e09e 100644 --- a/fvh3t/fvh3t_processing/count_trajectories.py +++ b/fvh3t/fvh3t_processing/count_trajectories.py @@ -17,6 +17,7 @@ ) from qgis.PyQt.QtCore import QCoreApplication +from fvh3t.core.gate_layer import GateLayer from fvh3t.core.trajectory_layer import TrajectoryLayer @@ -85,7 +86,6 @@ def initAlgorithm(self, config=None): # noqa N802 name=self.OUTPUT_GATES, description="Gates", type=QgsProcessing.TypeVectorLine, - optional=True, ) ) @@ -112,10 +112,11 @@ def processAlgorithm( # noqa N802 feedback = QgsProcessingFeedback() point_layer = self.parameterAsVectorLayer(parameters, self.INPUT_POINTS, context) - # line_layer = self.parameterAsVectorLayer(parameters, self.INPUT_LINES, context) start_time = self.parameterAsDateTime(parameters, self.START_TIME, context) end_time = self.parameterAsDateTime(parameters, self.END_TIME, context) + ## CREATE TRAJECTORIES + # TODO: Remove later feedback.pushInfo(f"Original point layer has {point_layer.featureCount()} features.") @@ -167,6 +168,7 @@ def processAlgorithm( # noqa N802 id_count[feature_id] = 0 id_count[feature_id] += 1 + # TODO: Remove later 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 @@ -183,6 +185,7 @@ def processAlgorithm( # noqa N802 new_feature = QgsFeature(feature) non_unique_layer.dataProvider().addFeature(new_feature) + # TODO: Remove later feedback.pushInfo( f"Final filtered layer contains {non_unique_layer.featureCount()} features with non-unique IDs." ) @@ -195,22 +198,55 @@ def processAlgorithm( # noqa N802 "size_y", "size_z", QgsUnitTypes.TemporalUnit.TemporalMilliseconds, - ).as_line_layer() + ) + + exported_traj_layer = trajectory_layer.as_line_layer() - if trajectory_layer is None: + if exported_traj_layer is None: msg = "Trajectory layer is None." raise ValueError(msg) - (sink, dest_id) = self.parameterAsSink( + (sink, traj_dest_id) = self.parameterAsSink( parameters, self.OUTPUT_TRAJECTORIES, context, - trajectory_layer.fields(), - trajectory_layer.wkbType(), - trajectory_layer.sourceCrs(), + exported_traj_layer.fields(), + exported_traj_layer.wkbType(), + exported_traj_layer.sourceCrs(), + ) + + for feature in exported_traj_layer.getFeatures(): + sink.addFeature(feature, QgsFeatureSink.FastInsert) + + # CREATE GATES + line_layer = self.parameterAsVectorLayer(parameters, self.INPUT_LINES, context) + + # TODO: Remove later + feedback.pushInfo(f"Line layer has {line_layer.featureCount()} features.") + + gate_layer = GateLayer(line_layer, "counts_left", "counts_right") + + gates = gate_layer.gates() + + for gate in gates: + gate.count_trajectories_from_layer(trajectory_layer) + + exported_gate_layer = gate_layer.as_line_layer() + + if exported_gate_layer is None: + msg = "Gate layer is None" + raise ValueError(msg) + + (sink, gate_dest_id) = self.parameterAsSink( + parameters, + self.OUTPUT_GATES, + context, + exported_gate_layer.fields(), + exported_gate_layer.wkbType(), + exported_gate_layer.sourceCrs(), ) - for feature in trajectory_layer.getFeatures(): + 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 @@ -219,4 +255,4 @@ def processAlgorithm( # noqa N802 # 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: dest_id} + return {self.OUTPUT_TRAJECTORIES: traj_dest_id, self.OUTPUT_GATES: gate_dest_id} From 61272feb73284104b55ca73e58ffb711ddcb362e Mon Sep 17 00:00:00 2001 From: msorvoja Date: Thu, 31 Oct 2024 13:25:39 +0200 Subject: [PATCH 9/9] Minor fixes --- fvh3t/fvh3t_processing/count_trajectories.py | 25 +++++--------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/fvh3t/fvh3t_processing/count_trajectories.py b/fvh3t/fvh3t_processing/count_trajectories.py index 349e09e..2fd172e 100644 --- a/fvh3t/fvh3t_processing/count_trajectories.py +++ b/fvh3t/fvh3t_processing/count_trajectories.py @@ -59,9 +59,8 @@ def initAlgorithm(self, config=None): # noqa N802 self.addParameter( QgsProcessingParameterVectorLayer( name=self.INPUT_LINES, - description="Input line layer", + description="Gates", types=[QgsProcessing.TypeVectorLine], - optional=True, ) ) @@ -117,28 +116,19 @@ def processAlgorithm( # noqa N802 ## CREATE TRAJECTORIES - # TODO: Remove later feedback.pushInfo(f"Original point layer has {point_layer.featureCount()} features.") # Get min and max timestamps from the data - min_timestamp = None - max_timestamp = None - - for feature in point_layer.getFeatures(): - timestamp = feature["timestamp"] - - if min_timestamp is None or timestamp < min_timestamp: - min_timestamp = timestamp - if max_timestamp is None or timestamp > max_timestamp: - max_timestamp = timestamp + timestamp_field_id = point_layer.fields().indexOf("timestamp") + min_timestamp, max_timestamp = point_layer.minimumAndMaximumValue(timestamp_field_id) if min_timestamp is None or max_timestamp is None: msg = "No valid timestamps found in the point layer." raise ValueError(msg) # 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.toSecsSinceEpoch() * 1000 if start_time.isValid() else min_timestamp - end_time_unix = end_time.toSecsSinceEpoch() * 1000 if end_time.isValid() else max_timestamp + 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 ( @@ -168,7 +158,6 @@ def processAlgorithm( # noqa N802 id_count[feature_id] = 0 id_count[feature_id] += 1 - # TODO: Remove later 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 @@ -185,9 +174,8 @@ def processAlgorithm( # noqa N802 new_feature = QgsFeature(feature) non_unique_layer.dataProvider().addFeature(new_feature) - # TODO: Remove later feedback.pushInfo( - f"Final filtered layer contains {non_unique_layer.featureCount()} features with non-unique IDs." + f"Final filtered point layer contains {non_unique_layer.featureCount()} features with non-unique IDs." ) trajectory_layer = TrajectoryLayer( @@ -221,7 +209,6 @@ def processAlgorithm( # noqa N802 # CREATE GATES line_layer = self.parameterAsVectorLayer(parameters, self.INPUT_LINES, context) - # TODO: Remove later feedback.pushInfo(f"Line layer has {line_layer.featureCount()} features.") gate_layer = GateLayer(line_layer, "counts_left", "counts_right")