From fe0b06a8aeaa11f1cd81427e13e5f9bdac2171ef Mon Sep 17 00:00:00 2001 From: Juho Ervasti Date: Fri, 18 Oct 2024 17:24:20 +0300 Subject: [PATCH 1/4] Add initial versions of Trajectory and Gate --- fvh3t/core/__init__.py | 0 fvh3t/core/gate.py | 9 +++++++++ fvh3t/core/trajectory.py | 28 ++++++++++++++++++++++++++++ tests/conftest.py | 13 +++++++++++++ tests/core/test_trajectory.py | 13 +++++++++++++ tests/test_plugin.py | 2 +- 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 fvh3t/core/__init__.py create mode 100644 fvh3t/core/gate.py create mode 100644 fvh3t/core/trajectory.py create mode 100644 tests/core/test_trajectory.py diff --git a/fvh3t/core/__init__.py b/fvh3t/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fvh3t/core/gate.py b/fvh3t/core/gate.py new file mode 100644 index 0000000..28a2b2c --- /dev/null +++ b/fvh3t/core/gate.py @@ -0,0 +1,9 @@ +from qgis.core import QgsGeometry + +class Gate: + + def __init__(self, geom: QgsGeometry) -> None: + self.__geom: QgsGeometry = geom + + def geometry(self) -> QgsGeometry: + return self.__geom diff --git a/fvh3t/core/trajectory.py b/fvh3t/core/trajectory.py new file mode 100644 index 0000000..f9bfadf --- /dev/null +++ b/fvh3t/core/trajectory.py @@ -0,0 +1,28 @@ +from typing import Tuple, NamedTuple + +from qgis.core import QgsGeometry, QgsPointXY + +class TrajectoryNode(NamedTuple): + point: QgsPointXY + timestamp: int + + @classmethod + def from_coordinates(cls, x: float, y: float, timestamp: int): + return cls(QgsPointXY(x, y), timestamp) + + +class Trajectory: + + def __init__(self, nodes: Tuple[TrajectoryNode]) -> None: + self.__nodes: Tuple[TrajectoryNode] = nodes + + def as_geometry(self) -> QgsGeometry: + return QgsGeometry.fromPolylineXY([node.point for node in self.__nodes]) + + def intersects(self, other_geom: QgsGeometry) -> bool: + return self.as_geometry().intersects(other_geom) + + def average_speed(self) -> float: + # TODO: implement function + return 0.0 + diff --git a/tests/conftest.py b/tests/conftest.py index f420bbf..a607750 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,3 +13,16 @@ This should be used with tests that add stuff to QgsProject. """ + +import pytest + +from fvh3t.core.trajectory import Trajectory, TrajectoryNode +from qgis.core import QgsGeometry, QgsPointXY + +@pytest.fixture +def two_node_trajectory(): + return Trajectory((TrajectoryNode.from_coordinates(0, 0, 1000), TrajectoryNode.from_coordinates(0, 1, 2000))) + +@pytest.fixture +def two_point_gate(): + return QgsGeometry.fromPolylineXY([QgsPointXY(-0.5, 0.5), QgsPointXY(0.5, 0.5)]) diff --git a/tests/core/test_trajectory.py b/tests/core/test_trajectory.py new file mode 100644 index 0000000..f26e069 --- /dev/null +++ b/tests/core/test_trajectory.py @@ -0,0 +1,13 @@ +from fvh3t.core.trajectory import Trajectory +from qgis.core import QgsGeometry, QgsPointXY + +def test_trajectory_as_geometry(two_node_trajectory: Trajectory) -> None: + assert two_node_trajectory.as_geometry().asWkt() == "LineString (0 0, 0 1)" + +def test_trajectory_intersects(two_node_trajectory): + geom1 = QgsGeometry.fromPolylineXY([QgsPointXY(-0.5, 0.5), QgsPointXY(0.5, 0.5)]) + geom2 = QgsGeometry.fromPolylineXY([QgsPointXY(-1, -1), QgsPointXY(-2, -2)]) + + assert two_node_trajectory.intersects(geom1) + assert not two_node_trajectory.intersects(geom2) + diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 63bf0e2..3c5980d 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -2,4 +2,4 @@ def test_plugin_name(): - assert plugin_name() == "FVH-3T" + assert plugin_name() == "TrafficTrajectoryToolkit" From 4995950643666d75fe3fb31916eba5c21fc9f687 Mon Sep 17 00:00:00 2001 From: Juho Ervasti Date: Mon, 21 Oct 2024 08:43:03 +0300 Subject: [PATCH 2/4] Trajectory.intersects -> intersects_gate --- fvh3t/core/gate.py | 1 + fvh3t/core/trajectory.py | 6 ++++-- tests/core/test_trajectory.py | 10 +++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/fvh3t/core/gate.py b/fvh3t/core/gate.py index 28a2b2c..d34bb98 100644 --- a/fvh3t/core/gate.py +++ b/fvh3t/core/gate.py @@ -7,3 +7,4 @@ def __init__(self, geom: QgsGeometry) -> None: def geometry(self) -> QgsGeometry: return self.__geom + diff --git a/fvh3t/core/trajectory.py b/fvh3t/core/trajectory.py index f9bfadf..e47d0d6 100644 --- a/fvh3t/core/trajectory.py +++ b/fvh3t/core/trajectory.py @@ -2,6 +2,8 @@ from qgis.core import QgsGeometry, QgsPointXY +from fvh3t.core.gate import Gate + class TrajectoryNode(NamedTuple): point: QgsPointXY timestamp: int @@ -19,8 +21,8 @@ def __init__(self, nodes: Tuple[TrajectoryNode]) -> None: def as_geometry(self) -> QgsGeometry: return QgsGeometry.fromPolylineXY([node.point for node in self.__nodes]) - def intersects(self, other_geom: QgsGeometry) -> bool: - return self.as_geometry().intersects(other_geom) + def intersects_gate(self, other: Gate) -> bool: + return self.as_geometry().intersects(other.geometry()) def average_speed(self) -> float: # TODO: implement function diff --git a/tests/core/test_trajectory.py b/tests/core/test_trajectory.py index f26e069..e55b6e7 100644 --- a/tests/core/test_trajectory.py +++ b/tests/core/test_trajectory.py @@ -1,13 +1,17 @@ from fvh3t.core.trajectory import Trajectory +from fvh3t.core.gate import Gate from qgis.core import QgsGeometry, QgsPointXY def test_trajectory_as_geometry(two_node_trajectory: Trajectory) -> None: assert two_node_trajectory.as_geometry().asWkt() == "LineString (0 0, 0 1)" -def test_trajectory_intersects(two_node_trajectory): +def test_trajectory_intersects_gate(two_node_trajectory): geom1 = QgsGeometry.fromPolylineXY([QgsPointXY(-0.5, 0.5), QgsPointXY(0.5, 0.5)]) geom2 = QgsGeometry.fromPolylineXY([QgsPointXY(-1, -1), QgsPointXY(-2, -2)]) - assert two_node_trajectory.intersects(geom1) - assert not two_node_trajectory.intersects(geom2) + gate1 = Gate(geom1) + gate2 = Gate(geom2) + + assert two_node_trajectory.intersects_gate(gate1) + assert not two_node_trajectory.intersects_gate(gate2) From c8d23a202a339a0d460a5cdc15c32f0c727ac91e Mon Sep 17 00:00:00 2001 From: Juho Ervasti Date: Mon, 21 Oct 2024 09:45:31 +0300 Subject: [PATCH 3/4] Add TrajectoryLayer and docstrings --- fvh3t/core/gate.py | 5 +++++ fvh3t/core/trajectory.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/fvh3t/core/gate.py b/fvh3t/core/gate.py index d34bb98..a2f709f 100644 --- a/fvh3t/core/gate.py +++ b/fvh3t/core/gate.py @@ -1,6 +1,11 @@ from qgis.core import QgsGeometry class Gate: + """ + A wrapper class around a QgsGeometry which represents a + gate through which trajectories can pass. The geometry + must be a line. + """ def __init__(self, geom: QgsGeometry) -> None: self.__geom: QgsGeometry = geom diff --git a/fvh3t/core/trajectory.py b/fvh3t/core/trajectory.py index e47d0d6..87c7b1a 100644 --- a/fvh3t/core/trajectory.py +++ b/fvh3t/core/trajectory.py @@ -1,10 +1,15 @@ from typing import Tuple, NamedTuple -from qgis.core import QgsGeometry, QgsPointXY +from qgis.core import QgsGeometry, QgsPointXY, QgsVectorLayer, QgsWkbTypes from fvh3t.core.gate import Gate class TrajectoryNode(NamedTuple): + """ + A simple data container representing one node in a + trajectory. + """ + point: QgsPointXY timestamp: int @@ -14,6 +19,10 @@ def from_coordinates(cls, x: float, y: float, timestamp: int): class Trajectory: + """ + Class representing a trajectory which consists + of nodes which have a location and a timestamp + """ def __init__(self, nodes: Tuple[TrajectoryNode]) -> None: self.__nodes: Tuple[TrajectoryNode] = nodes @@ -28,3 +37,25 @@ def average_speed(self) -> float: # TODO: implement function return 0.0 + +class TrajectoryLayer: + """ + Wrapper around a QgsVectorLayer object from which trajectories + can be instantiated, i.e. + + 1. is a point layer + 2. has a valid identifier field + 3. has a valid timestamp field + """ + + def __init__(self, layer: QgsVectorLayer, id_field: str, timestamp_field: str) -> None: + self.__layer: QgsVectorLayer = layer + self.__id_field: str = id_field + self.__timestamp_field: str = timestamp_field + + def is_valid(self) -> bool: + is_point_layer: bool = self.__layer.geometryType() == QgsWkbTypes.GeometryType.PointGeometry + id_field_exists: bool = self.__layer.fields().indexFromName(self.__id_field) != -1 + timestamp_field_exists: bool = self.__layer.fields().indexFromName(self.__timestamp_field) != -1 + + return is_point_layer and id_field_exists and timestamp_field_exists From b3dab2bef317151149da78114181ab38357caa22 Mon Sep 17 00:00:00 2001 From: Juho Ervasti Date: Mon, 21 Oct 2024 10:52:14 +0300 Subject: [PATCH 4/4] Small fixes to typing --- fvh3t/core/trajectory.py | 6 +++--- tests/conftest.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fvh3t/core/trajectory.py b/fvh3t/core/trajectory.py index 87c7b1a..54e6825 100644 --- a/fvh3t/core/trajectory.py +++ b/fvh3t/core/trajectory.py @@ -1,4 +1,4 @@ -from typing import Tuple, NamedTuple +from typing import NamedTuple from qgis.core import QgsGeometry, QgsPointXY, QgsVectorLayer, QgsWkbTypes @@ -24,8 +24,8 @@ class Trajectory: of nodes which have a location and a timestamp """ - def __init__(self, nodes: Tuple[TrajectoryNode]) -> None: - self.__nodes: Tuple[TrajectoryNode] = nodes + def __init__(self, nodes: tuple[TrajectoryNode, ...]) -> None: + self.__nodes: tuple[TrajectoryNode, ...] = nodes def as_geometry(self) -> QgsGeometry: return QgsGeometry.fromPolylineXY([node.point for node in self.__nodes]) diff --git a/tests/conftest.py b/tests/conftest.py index a607750..09a726b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ """ import pytest +from fvh3t.core.gate import Gate from fvh3t.core.trajectory import Trajectory, TrajectoryNode from qgis.core import QgsGeometry, QgsPointXY @@ -25,4 +26,4 @@ def two_node_trajectory(): @pytest.fixture def two_point_gate(): - return QgsGeometry.fromPolylineXY([QgsPointXY(-0.5, 0.5), QgsPointXY(0.5, 0.5)]) + return Gate(QgsGeometry.fromPolylineXY([QgsPointXY(-0.5, 0.5), QgsPointXY(0.5, 0.5)]))