Skip to content

Commit 7c5835e

Browse files
committed
Add generalizers, cleaners, and smoother
1 parent 1fc07dc commit 7c5835e

File tree

5 files changed

+403
-4
lines changed

5 files changed

+403
-4
lines changed

metadata.txt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name=Trajectools
33
description=Processing tools for handling trajectory data
44
about=Trajectools provides a collection of tools for handling trajectory data. This plugin depends on MovingPandas! See the plugin homepage for installation recommendations. Sample data for testing the functionality is provided with the plugin download.
55
category=Plugins
6-
version=2.2
6+
version=2.3
77
qgisMinimumVersion=3.0
88
qgisMaximumVersion=4.0
99

@@ -20,9 +20,13 @@ repository=https://github.com/movingpandas/qgis-processing-trajectory
2020
experimental=False
2121
deprecated=False
2222

23-
changelog=2.2
23+
changelog=2.3
24+
- Added generalizers and cleaners
25+
- Added smoother (requires stonesoup)
26+
- Fixed stop detection double input, fixes #40
27+
2.2
2428
- Added new logo
25-
- Maded skmob and gtfs_functions optional dependencies, fixes #30
29+
- Added skmob and gtfs_functions optional dependencies, fixes #30
2630
- Updated API docs link, fixes #32
2731
2.1
2832
- Added Trajectory overlay algorithm to intersect trajectories with polygon layer

qgis_processing/cleaningAlgorithm.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import sys
2+
import pandas as pd
3+
4+
from movingpandas import (
5+
OutlierCleaner
6+
)
7+
8+
from qgis.core import (
9+
QgsProcessingParameterString,
10+
QgsProcessingParameterEnum,
11+
QgsProcessingParameterNumber,
12+
)
13+
14+
sys.path.append("..")
15+
16+
from .trajectoriesAlgorithm import TrajectoryManipulationAlgorithm
17+
18+
19+
class CleaningAlgorithm(TrajectoryManipulationAlgorithm):
20+
21+
def __init__(self):
22+
super().__init__()
23+
24+
def group(self):
25+
return self.tr("Trajectory cleaning")
26+
27+
def groupId(self):
28+
return "TrajectoryCleaning"
29+
30+
31+
class OutlierCleanerAlgorithm(CleaningAlgorithm):
32+
TOLERANCE = "TOLERANCE"
33+
34+
def __init__(self):
35+
super().__init__()
36+
37+
def initAlgorithm(self, config=None):
38+
super().initAlgorithm(config)
39+
self.addParameter(
40+
QgsProcessingParameterNumber(
41+
name=self.TOLERANCE,
42+
description=self.tr("Speed threshold"),
43+
defaultValue=10.0,
44+
type=QgsProcessingParameterNumber.Double,
45+
)
46+
)
47+
48+
def name(self):
49+
return "clean_vmax"
50+
51+
def displayName(self):
52+
return self.tr("Remove speed above threshold")
53+
54+
def shortHelpString(self):
55+
return self.tr(
56+
"<p>Speed-based outlier cleaner that cuts away spikes in the trajectory when "
57+
"the speed exceeds the provided speed threshold value </p>"
58+
"<p>For more info see: "
59+
"https://movingpandas.readthedocs.io/en/main/api/trajectorycleaner.html</p>"
60+
"<p><b>Speed</b> is calculated based on the input layer CRS information and "
61+
"converted to the desired speed units. For more info on the supported units, "
62+
"see https://movingpandas.org/units</p>"
63+
"<p><b>Direction</b> is calculated between consecutive locations. Direction "
64+
"values are in degrees, starting North turning clockwise.</p>"
65+
)
66+
67+
def processTc(self, tc, parameters, context):
68+
v_max = self.parameterAsDouble(parameters, self.TOLERANCE, context)
69+
generalized = OutlierCleaner(tc).clean(v_max=v_max, units=tuple(self.speed_units))
70+
self.tc_to_sink(generalized)
71+
for traj in generalized:
72+
self.traj_to_sink(traj)
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import sys
2+
import pandas as pd
3+
4+
from movingpandas import (
5+
DouglasPeuckerGeneralizer,
6+
MinDistanceGeneralizer,
7+
MinTimeDeltaGeneralizer,
8+
TopDownTimeRatioGeneralizer,
9+
)
10+
11+
from qgis.core import (
12+
QgsProcessingParameterString,
13+
QgsProcessingParameterEnum,
14+
QgsProcessingParameterNumber,
15+
)
16+
17+
sys.path.append("..")
18+
19+
from .trajectoriesAlgorithm import TrajectoryManipulationAlgorithm
20+
21+
22+
class GeneralizeTrajectoriesAlgorithm(TrajectoryManipulationAlgorithm):
23+
TOLERANCE = "TOLERANCE"
24+
25+
def __init__(self):
26+
super().__init__()
27+
28+
def group(self):
29+
return self.tr("Trajectory generalization")
30+
31+
def groupId(self):
32+
return "TrajectoryGeneralization"
33+
34+
35+
class DouglasPeuckerGeneralizerAlgorithm(GeneralizeTrajectoriesAlgorithm):
36+
def __init__(self):
37+
super().__init__()
38+
39+
def initAlgorithm(self, config=None):
40+
super().initAlgorithm(config)
41+
self.addParameter(
42+
QgsProcessingParameterNumber(
43+
name=self.TOLERANCE,
44+
description=self.tr("Distance tolerance in trajectory CRS units"),
45+
defaultValue=10.0,
46+
type=QgsProcessingParameterNumber.Double,
47+
)
48+
)
49+
50+
def name(self):
51+
return "generalize_dp"
52+
53+
def displayName(self):
54+
return self.tr("Douglas-Peucker generalization")
55+
56+
def shortHelpString(self):
57+
return self.tr(
58+
"<p>Generalizes trajectories using Douglas-Peucker algorithm "
59+
"(as implemented in shapely/Geos). </p>"
60+
"<p>For more info see: "
61+
"https://movingpandas.readthedocs.io/en/main/api/trajectorygeneralizer.html</p>"
62+
"<p><b>Speed</b> is calculated based on the input layer CRS information and "
63+
"converted to the desired speed units. For more info on the supported units, "
64+
"see https://movingpandas.org/units</p>"
65+
"<p><b>Direction</b> is calculated between consecutive locations. Direction "
66+
"values are in degrees, starting North turning clockwise.</p>"
67+
)
68+
69+
def processTc(self, tc, parameters, context):
70+
tolerance = self.parameterAsDouble(parameters, self.TOLERANCE, context)
71+
generalized = DouglasPeuckerGeneralizer(tc).generalize(tolerance)
72+
self.tc_to_sink(generalized)
73+
for traj in generalized:
74+
self.traj_to_sink(traj)
75+
76+
77+
class MinDistanceGeneralizerAlgorithm(GeneralizeTrajectoriesAlgorithm):
78+
def __init__(self):
79+
super().__init__()
80+
81+
def initAlgorithm(self, config=None):
82+
super().initAlgorithm(config)
83+
self.addParameter(
84+
QgsProcessingParameterNumber(
85+
name=self.TOLERANCE,
86+
description=self.tr("Distance tolerance"),
87+
defaultValue=10.0,
88+
type=QgsProcessingParameterNumber.Double,
89+
)
90+
)
91+
92+
def name(self):
93+
return "generalize_min_dist"
94+
95+
def displayName(self):
96+
return self.tr("Distance-based generalization")
97+
98+
def shortHelpString(self):
99+
return self.tr(
100+
"This generalization ensures that consecutive locations are at least a "
101+
"certain distance apart. "
102+
"Distance is calculated using CRS units, except if the CRS is geographic "
103+
"(e.g. EPSG:4326 WGS84) then distance is calculated in metres. </p>"
104+
"<p>For more info see: "
105+
"https://movingpandas.readthedocs.io/en/main/api/trajectorygeneralizer.html</p>"
106+
"<p><b>Speed</b> is calculated based on the input layer CRS information and "
107+
"converted to the desired speed units. For more info on the supported units, "
108+
"see https://movingpandas.org/units</p>"
109+
"<p><b>Direction</b> is calculated between consecutive locations. Direction "
110+
"values are in degrees, starting North turning clockwise.</p>"
111+
)
112+
113+
def processTc(self, tc, parameters, context):
114+
tolerance = self.parameterAsDouble(parameters, self.TOLERANCE, context)
115+
generalized = MinDistanceGeneralizer(tc).generalize(tolerance)
116+
self.tc_to_sink(generalized)
117+
for traj in generalized:
118+
self.traj_to_sink(traj)
119+
120+
121+
class MinTimeDeltaGeneralizerAlgorithm(GeneralizeTrajectoriesAlgorithm):
122+
def __init__(self):
123+
super().__init__()
124+
125+
def initAlgorithm(self, config=None):
126+
super().initAlgorithm(config)
127+
self.addParameter(
128+
QgsProcessingParameterString(
129+
name=self.TOLERANCE,
130+
description=self.tr(
131+
"Time tolerance (timedelta, e.g. 1 hours, 15 minutes)"
132+
),
133+
defaultValue="2 minutes",
134+
optional=False,
135+
)
136+
)
137+
138+
def name(self):
139+
return "generalize_min_timedelta"
140+
141+
def displayName(self):
142+
return self.tr("Temporal generalization")
143+
144+
def shortHelpString(self):
145+
return self.tr(
146+
"This generalization ensures that consecutive rows are at least a certain "
147+
"timedelta apart. </p>"
148+
"<p>For more info see: "
149+
"https://movingpandas.readthedocs.io/en/main/api/trajectorygeneralizer.html</p>"
150+
"<p><b>Speed</b> is calculated based on the input layer CRS information and "
151+
"converted to the desired speed units. For more info on the supported units, "
152+
"see https://movingpandas.org/units</p>"
153+
"<p><b>Direction</b> is calculated between consecutive locations. Direction "
154+
"values are in degrees, starting North turning clockwise.</p>"
155+
)
156+
157+
def processTc(self, tc, parameters, context):
158+
tolerance = self.parameterAsString(parameters, self.TOLERANCE, context)
159+
tolerance = pd.Timedelta(tolerance).to_pytimedelta()
160+
generalized = MinTimeDeltaGeneralizer(tc).generalize(tolerance)
161+
self.tc_to_sink(generalized)
162+
for traj in generalized:
163+
self.traj_to_sink(traj)
164+
165+
166+
class TopDownTimeRatioGeneralizerAlgorithm(GeneralizeTrajectoriesAlgorithm):
167+
def __init__(self):
168+
super().__init__()
169+
170+
def initAlgorithm(self, config=None):
171+
super().initAlgorithm(config)
172+
self.addParameter(
173+
QgsProcessingParameterNumber(
174+
name=self.TOLERANCE,
175+
description=self.tr("Distance tolerance in trajectory CRS units"),
176+
defaultValue=10.0,
177+
type=QgsProcessingParameterNumber.Double,
178+
)
179+
)
180+
181+
def name(self):
182+
return "generalize_topdown"
183+
184+
def displayName(self):
185+
return self.tr("Spatiotemporal generalization (TDTR)")
186+
187+
def shortHelpString(self):
188+
return self.tr(
189+
"Generalizes using Top-Down Time Ratio algorithm proposed by "
190+
"Meratnia & de By (2004). "
191+
"This is a spatiotemporal trajectory generalization algorithm. "
192+
"Where Douglas-Peucker simply measures the spatial distance between points "
193+
"and original line geometry, Top-Down Time Ratio (TDTR) measures the distance "
194+
"between points and their spatiotemporal projection on the trajectory. "
195+
"These projections are calculated based on the ratio of travel times between "
196+
"the segment start and end times and the point time. </p>"
197+
"<p>For more info see: "
198+
"https://movingpandas.readthedocs.io/en/main/api/trajectorygeneralizer.html</p>"
199+
"<p><b>Speed</b> is calculated based on the input layer CRS information and "
200+
"converted to the desired speed units. For more info on the supported units, "
201+
"see https://movingpandas.org/units</p>"
202+
"<p><b>Direction</b> is calculated between consecutive locations. Direction "
203+
"values are in degrees, starting North turning clockwise.</p>"
204+
)
205+
206+
def processTc(self, tc, parameters, context):
207+
tolerance = self.parameterAsDouble(parameters, self.TOLERANCE, context)
208+
generalized = TopDownTimeRatioGeneralizer(tc).generalize(tolerance)
209+
self.tc_to_sink(generalized)
210+
for traj in generalized:
211+
self.traj_to_sink(traj)

qgis_processing/smoothingAlgorithm.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import sys
2+
import pandas as pd
3+
4+
from movingpandas import (
5+
KalmanSmootherCV,
6+
)
7+
8+
from qgis.core import (
9+
QgsProcessingParameterString,
10+
QgsProcessingParameterEnum,
11+
QgsProcessingParameterNumber,
12+
)
13+
14+
sys.path.append("..")
15+
16+
from .trajectoriesAlgorithm import TrajectoryManipulationAlgorithm
17+
18+
19+
class SmoothingAlgorithm(TrajectoryManipulationAlgorithm):
20+
def __init__(self):
21+
super().__init__()
22+
23+
def group(self):
24+
return self.tr("Trajectory smoothing")
25+
26+
def groupId(self):
27+
return "TrajectorySmoothing"
28+
29+
30+
class KalmanSmootherAlgorithm(SmoothingAlgorithm):
31+
PROCESS_NOISE = "PROCESS_NOISE"
32+
MEASURE_NOISE = "MEASURE_NOISE"
33+
34+
def __init__(self):
35+
super().__init__()
36+
37+
def initAlgorithm(self, config=None):
38+
super().initAlgorithm(config)
39+
self.addParameter(
40+
QgsProcessingParameterNumber(
41+
name=self.PROCESS_NOISE,
42+
description=self.tr("Process (acceleration) noise standard deviation."),
43+
defaultValue=0.1,
44+
type=QgsProcessingParameterNumber.Double,
45+
)
46+
)
47+
self.addParameter(
48+
QgsProcessingParameterNumber(
49+
name=self.MEASURE_NOISE,
50+
description=self.tr("Measurement noise standard deviation"),
51+
defaultValue=1,
52+
type=QgsProcessingParameterNumber.Double,
53+
)
54+
)
55+
56+
def name(self):
57+
return "smooth_kalman"
58+
59+
def displayName(self):
60+
return self.tr("Kalman filter with constant velocity model")
61+
62+
def shortHelpString(self):
63+
return self.tr(
64+
"<p>Smooths trajectories using a Kalman Filter with a Constant Velocity model. "
65+
"The Constant Velocity model assumes that the speed between consecutive "
66+
"locations is nearly constant. For trajectories where traj.is_latlon = True "
67+
"the smoother converts to EPSG:3395 (World Mercator) internally to perform "
68+
"filtering and smoothing. "
69+
"<p>Higher <b>Process noise</b> values lead to less-smooth trajectories.</p>"
70+
"<p>Higher <b>Measurement noise</b> values lead to smoother trajectories.</p>"
71+
"<p>For more info see: "
72+
"https://movingpandas.readthedocs.io/en/main/api/trajectorysmoother.html</p>"
73+
"<p><b>Speed</b> is calculated based on the input layer CRS information and "
74+
"converted to the desired speed units. For more info on the supported units, "
75+
"see https://movingpandas.org/units</p>"
76+
"<p><b>Direction</b> is calculated between consecutive locations. Direction "
77+
"values are in degrees, starting North turning clockwise.</p>"
78+
)
79+
80+
def processTc(self, tc, parameters, context):
81+
pn = self.parameterAsDouble(parameters, self.PROCESS_NOISE, context)
82+
mn = self.parameterAsDouble(parameters, self.MEASURE_NOISE, context)
83+
smooth = KalmanSmootherCV(tc).smooth(process_noise_std=pn, measurement_noise_std=mn)
84+
self.tc_to_sink(smooth)
85+
for traj in smooth:
86+
self.traj_to_sink(traj)

0 commit comments

Comments
 (0)