Skip to content

Commit 4d34f1f

Browse files
committed
Add SplitTrajectoriesAlgorithm
1 parent 4fa4ee4 commit 4d34f1f

File tree

4 files changed

+246
-38
lines changed

4 files changed

+246
-38
lines changed

qgis_processing/createTrajectoriesAlgorithm.py

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
sys.path.append("..")
2727

28-
from .qgisUtils import tc_from_pt_layer, tc_to_sink
28+
from .qgisUtils import tc_from_pt_layer, tc_to_sink, get_pt_fields, get_traj_fields
2929

3030
pluginPath = os.path.dirname(__file__)
3131

@@ -64,10 +64,12 @@ def displayName(self):
6464

6565
def shortHelpString(self):
6666
return self.tr(
67-
"<p><b>Speed</b> is calculated based on the input layer CRS information and " +
68-
"converted to the desired speed units. For more info on the supported units, " +
69-
"see https://movingpandas.org/units </p>" +
70-
"<p><b>Direction</b> is calculated between consecutive locations. Direction " +
67+
"<p>Creates a trajectory point layers with speed and direction information "
68+
"as well as a trajectory line layer.</p>"
69+
"<p><b>Speed</b> is calculated based on the input layer CRS information and "
70+
"converted to the desired speed units. For more info on the supported units, "
71+
"see https://movingpandas.org/units</p>"
72+
"<p><b>Direction</b> is calculated between consecutive locations. Direction "
7173
"values are in degrees, starting North turning clockwise.</p>"
7274
)
7375

@@ -121,18 +123,18 @@ def initAlgorithm(self, config=None):
121123
type=QgsProcessing.TypeVectorLine))
122124

123125
def processAlgorithm(self, parameters, context, feedback):
124-
input_layer = self.parameterAsSource(parameters, self.INPUT, context)
125-
traj_id_field = self.parameterAsFields(parameters, self.TRAJ_ID_FIELD, context)[0]
126-
timestamp_field = self.parameterAsFields(parameters, self.TIMESTAMP_FIELD, context)[0]
126+
self.input_layer = self.parameterAsSource(parameters, self.INPUT, context)
127+
self.traj_id_field = self.parameterAsFields(parameters, self.TRAJ_ID_FIELD, context)[0]
128+
self.timestamp_field = self.parameterAsFields(parameters, self.TIMESTAMP_FIELD, context)[0]
127129
timestamp_format = self.parameterAsString(parameters, self.TIMESTAMP_FORMAT, context)
128130
speed_units = self.parameterAsString(parameters, self.SPEED_UNIT, context).split("/")
129131

130-
tc = tc_from_pt_layer(input_layer, timestamp_field, traj_id_field, timestamp_format)
131-
tc.add_speed(units=tuple(speed_units))
132-
tc.add_direction()
132+
tc = tc_from_pt_layer(self.input_layer, self.timestamp_field, self.traj_id_field, timestamp_format)
133+
tc.add_speed(units=tuple(speed_units), overwrite=True)
134+
tc.add_direction(overwrite=True)
133135

134-
self.dest_pts = self.create_points(parameters, context, input_layer, timestamp_field, tc)
135-
self.dest_trajs = self.create_trajectories(parameters, context, input_layer, traj_id_field, tc)
136+
self.dest_pts = self.create_points(parameters, context, tc)
137+
self.dest_trajs = self.create_trajectories(parameters, context, tc)
136138

137139
return {self.OUTPUT_PTS: self.dest_pts, self.OUTPUT_TRAJS: self.dest_trajs}
138140

@@ -141,29 +143,24 @@ def postProcessAlgorithm(self, context, feedback):
141143
processed_layer.loadNamedStyle(os.path.join(pluginPath, "styles", "pts.qml"))
142144
return {self.OUTPUT_PTS: self.dest_pts, self.OUTPUT_TRAJS: self.dest_trajs}
143145

144-
def create_points(self, parameters, context, input_layer, timestamp_field, tc):
145-
output_fields = input_layer.fields()
146-
self.drop_fid_field(output_fields)
147-
output_fields.append(QgsField(tc.get_speed_col(), QVariant.Double))
148-
output_fields.append(QgsField(tc.get_direction_col(), QVariant.Double))
146+
def create_points(self, parameters, context, tc):
147+
fields_pts = get_pt_fields(self.input_layer, self.traj_id_field)
148+
fields_pts.append(QgsField(tc.get_speed_col(), QVariant.Double))
149+
fields_pts.append(QgsField(tc.get_direction_col(), QVariant.Double))
150+
crs = self.input_layer.sourceCrs()
149151

150152
(sink, dest) = self.parameterAsSink(
151-
parameters, self.OUTPUT_PTS, context, output_fields, QgsWkbTypes.Point, input_layer.sourceCrs())
153+
parameters, self.OUTPUT_PTS, context, fields_pts, QgsWkbTypes.Point, crs)
152154

153-
tc_to_sink(tc, sink, output_fields, timestamp_field)
155+
tc_to_sink(tc, sink, fields_pts, self.timestamp_field)
154156
return dest
155157

156-
def drop_fid_field(self, output_fields):
157-
i = output_fields.indexFromName("fid")
158-
if i >= 0:
159-
output_fields.remove(i)
160-
161-
def create_trajectories(self, parameters, context, input_layer, traj_id_field, tc):
162-
output_fields_lines = QgsFields()
163-
output_fields_lines.append(QgsField(traj_id_field, QVariant.String))
158+
def create_trajectories(self, parameters, context, tc):
159+
output_fields_lines = get_traj_fields(self.input_layer, self.traj_id_field)
160+
crs = self.input_layer.sourceCrs()
164161

165162
(sink, dest) = self.parameterAsSink(
166-
parameters, self.OUTPUT_TRAJS, context, output_fields_lines, QgsWkbTypes.LineStringM, input_layer.sourceCrs())
163+
parameters, self.OUTPUT_TRAJS, context, output_fields_lines, QgsWkbTypes.LineStringM, crs)
167164

168165
for traj in tc.trajectories:
169166
line = QgsGeometry.fromWkt(traj.to_linestringm_wkt())
@@ -172,4 +169,4 @@ def create_trajectories(self, parameters, context, input_layer, traj_id_field, t
172169
f.setAttributes([traj.id])
173170
sink.addFeature(f, QgsFeatureSink.FastInsert)
174171
return dest
175-
172+

qgis_processing/qgisUtils.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
sys.path.append("..")
2929

3030
from movingpandas import TrajectoryCollection
31-
from qgis.core import QgsFeature, QgsGeometry, QgsPointXY, QgsFeatureSink
31+
from qgis.core import QgsFeature, QgsGeometry, QgsPointXY, QgsFeatureSink, QgsFields, QgsField
32+
from qgis.PyQt.QtCore import QVariant
3233

3334

3435
def trajectories_from_qgis_point_layer(layer, time_field_name, trajectory_id_field, time_format):
@@ -62,14 +63,38 @@ def feature_from_gdf_row(row):
6263
return f
6364

6465

65-
def tc_to_sink(tc, sink, output_fields, timestamp_field):
66+
def tc_to_sink(tc, sink, fields, timestamp_field):
6667
gdf = tc.to_point_gdf()
6768
gdf[timestamp_field] = gdf.index.astype(str)
68-
names = [fields.name() for fields in output_fields]
69+
names = [field.name() for field in fields]
6970
names.append('geometry')
7071
gdf = gdf[names]
7172

7273
for _, row in gdf.iterrows():
7374
f = feature_from_gdf_row(row)
7475
sink.addFeature(f, QgsFeatureSink.FastInsert)
75-
76+
77+
def traj_to_sink(traj, sink):
78+
line = QgsGeometry.fromWkt(traj.to_linestringm_wkt())
79+
f = QgsFeature()
80+
f.setGeometry(line)
81+
f.setAttributes([traj.id])
82+
sink.addFeature(f, QgsFeatureSink.FastInsert)
83+
84+
def get_pt_fields(input_layer, traj_id_field):
85+
fields = QgsFields()
86+
for field in input_layer.fields():
87+
if field.name() == "fid":
88+
continue
89+
elif field.name() == traj_id_field: # we need to make sure the ID field is String
90+
fields.append(QgsField(traj_id_field, QVariant.String))
91+
else:
92+
fields.append(field)
93+
return fields
94+
95+
def get_traj_fields(input_layer, traj_id_field):
96+
fields = QgsFields()
97+
fields.append(QgsField(traj_id_field, QVariant.String))
98+
return fields
99+
100+
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# -*- coding: utf-8 -*-
2+
import os
3+
import sys
4+
import pandas as pd
5+
from datetime import timedelta
6+
from movingpandas import TemporalSplitter, ObservationGapSplitter
7+
8+
from qgis.PyQt.QtCore import QCoreApplication, QVariant
9+
from qgis.PyQt.QtGui import QIcon
10+
from qgis.core import (QgsField,QgsFields,
11+
QgsGeometry,
12+
QgsFeature,
13+
QgsFeatureSink,
14+
QgsFeatureRequest,
15+
QgsProcessing,
16+
QgsProcessingAlgorithm,
17+
QgsProcessingParameterFeatureSource,
18+
QgsProcessingParameterString,
19+
QgsProcessingParameterField,
20+
QgsProcessingParameterNumber,
21+
QgsProcessingParameterBoolean,
22+
QgsProcessingParameterFeatureSink,
23+
QgsProcessingParameterEnum,
24+
QgsWkbTypes,
25+
QgsProcessingUtils
26+
)
27+
28+
sys.path.append("..")
29+
30+
from .qgisUtils import tc_from_pt_layer, tc_to_sink, traj_to_sink, get_pt_fields, get_traj_fields
31+
32+
pluginPath = os.path.dirname(__file__)
33+
34+
35+
class SplitTrajectoriesAlgorithm(QgsProcessingAlgorithm):
36+
# script parameters
37+
INPUT = 'INPUT'
38+
TRAJ_ID_FIELD = 'OBJECT_ID_FIELD'
39+
TIMESTAMP_FIELD = 'TIMESTAMP_FIELD'
40+
TIMESTAMP_FORMAT = 'TIMESTAMP_FORMAT'
41+
OUTPUT_PTS = 'OUTPUT_PTS'
42+
OUTPUT_SEGS = 'OUTPUT_SEGS'
43+
OUTPUT_TRAJS = 'OUTPUT_TRAJS'
44+
SPLIT_MODE = 'SPLIT_MODE'
45+
SPLIT_MODE_OPTIONS = ["observation gap", "year", "month", "day", "hour", ]
46+
TIME_GAP = 'TIME_GAP'
47+
48+
def __init__(self):
49+
super().__init__()
50+
51+
def name(self):
52+
return "split"
53+
54+
def icon(self):
55+
return QIcon(os.path.join(pluginPath, "icons", "icon.png"))
56+
57+
def tr(self, text):
58+
return QCoreApplication.translate("split", text)
59+
60+
def displayName(self):
61+
return self.tr("Split trajectories")
62+
63+
#def group(self):
64+
# return self.tr("Basic")
65+
66+
#def groupId(self):
67+
# return "TrajectoryBasic"
68+
69+
def shortHelpString(self):
70+
return self.tr(
71+
"<p>Splits trajectories into subtrajectories using one of the "
72+
"following supported modes: </p>"
73+
"<p><b>Observation gap: </b>"
74+
"splits whenever there is a gap in the observations "
75+
"(for supported time gap formats see: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.to_timedelta.html)</p>"
76+
"<p><b>Temporal (year, month, day, hour): </b>"
77+
"splits using regular time intervals "
78+
"(time gap parameter will be ignored)</p>"
79+
"<p>For more information on trajectory splitters see: "
80+
"https://movingpandas.readthedocs.io/en/main/trajectorysplitter.html</p>"
81+
)
82+
83+
def helpUrl(self):
84+
return "https://github.com/movingpandas/processing-trajectory"
85+
86+
def createInstance(self):
87+
return type(self)()
88+
89+
def initAlgorithm(self, config=None):
90+
# input layer
91+
self.addParameter(QgsProcessingParameterFeatureSource(
92+
name=self.INPUT,
93+
description=self.tr("Input point layer"),
94+
types=[QgsProcessing.TypeVectorPoint]))
95+
# fields
96+
self.addParameter(QgsProcessingParameterField(
97+
name=self.TRAJ_ID_FIELD,
98+
description=self.tr("Trajectory ID field"),
99+
defaultValue="trajectory_id",
100+
parentLayerParameterName=self.INPUT,
101+
type=QgsProcessingParameterField.Any,
102+
allowMultiple=False,
103+
optional=False))
104+
self.addParameter(QgsProcessingParameterField(
105+
name=self.TIMESTAMP_FIELD,
106+
description=self.tr("Timestamp field"),
107+
defaultValue="t",
108+
parentLayerParameterName=self.INPUT,
109+
type=QgsProcessingParameterField.Any,
110+
allowMultiple=False,
111+
optional=False))
112+
self.addParameter(QgsProcessingParameterString(
113+
name=self.TIMESTAMP_FORMAT,
114+
description=self.tr("Timestamp format"),
115+
defaultValue="%Y-%m-%d %H:%M:%S+00",
116+
optional=False))
117+
self.addParameter(QgsProcessingParameterEnum(
118+
name=self.SPLIT_MODE,
119+
description=self.tr("Splitting mode"),
120+
defaultValue="day",
121+
options=self.SPLIT_MODE_OPTIONS,
122+
optional=False))
123+
self.addParameter(QgsProcessingParameterString(
124+
name=self.TIME_GAP,
125+
description=self.tr("Time gap (timedelta, e.g. 1 hours, 15 minutes)"),
126+
defaultValue="1 hours",
127+
optional=True))
128+
# output layer
129+
self.addParameter(QgsProcessingParameterFeatureSink(
130+
name=self.OUTPUT_PTS,
131+
description=self.tr("Trajectory points"),
132+
type=QgsProcessing.TypeVectorPoint))
133+
self.addParameter(QgsProcessingParameterFeatureSink(
134+
name=self.OUTPUT_TRAJS,
135+
description=self.tr("Trajectories"),
136+
type=QgsProcessing.TypeVectorLine))
137+
138+
def processAlgorithm(self, parameters, context, feedback):
139+
self.input_layer = self.parameterAsSource(parameters, self.INPUT, context)
140+
self.traj_id_field = self.parameterAsFields(parameters, self.TRAJ_ID_FIELD, context)[0]
141+
self.timestamp_field = self.parameterAsFields(parameters, self.TIMESTAMP_FIELD, context)[0]
142+
143+
timestamp_format = self.parameterAsString(parameters, self.TIMESTAMP_FORMAT, context)
144+
split_mode = self.parameterAsInt(parameters, self.SPLIT_MODE, context)
145+
split_mode = self.SPLIT_MODE_OPTIONS[split_mode]
146+
time_gap = self.parameterAsString(parameters, self.TIME_GAP, context)
147+
148+
crs = self.input_layer.sourceCrs()
149+
150+
self.fields_pts = get_pt_fields(self.input_layer, self.traj_id_field)
151+
(self.sink_pts, self.dest_pts) = self.parameterAsSink(
152+
parameters, self.OUTPUT_PTS, context, self.fields_pts, QgsWkbTypes.Point, crs)
153+
154+
self.fields_trajs = get_traj_fields(self.input_layer, self.traj_id_field)
155+
(self.sink_trajs, self.dest_trajs) = self.parameterAsSink(
156+
parameters, self.OUTPUT_TRAJS, context, self.fields_trajs, QgsWkbTypes.LineStringM, crs)
157+
158+
tc = tc_from_pt_layer(self.input_layer, self.timestamp_field, self.traj_id_field, timestamp_format)
159+
160+
if split_mode == "observation gap":
161+
self.split_on_gaps(time_gap, tc)
162+
else:
163+
self.split_temporally(split_mode, tc)
164+
165+
return {self.OUTPUT_PTS: self.dest_pts, self.OUTPUT_TRAJS: self.dest_trajs}
166+
167+
def postProcessAlgorithm(self, context, feedback):
168+
processed_layer = QgsProcessingUtils.mapLayerFromString(self.dest_pts, context)
169+
processed_layer.loadNamedStyle(os.path.join(pluginPath, "styles", "pts.qml"))
170+
return {self.OUTPUT_PTS: self.dest_pts, self.OUTPUT_TRAJS: self.dest_trajs}
171+
172+
def split_on_gaps(self, time_gap, tc):
173+
time_gap = pd.Timedelta(time_gap).to_pytimedelta()
174+
for traj in tc.trajectories:
175+
splits = ObservationGapSplitter(traj).split(gap=time_gap)
176+
tc_to_sink(splits, self.sink_pts, self.fields_pts, self.timestamp_field)
177+
for split in splits:
178+
traj_to_sink(split, self.sink_trajs)
179+
180+
def split_temporally(self, split_mode, tc):
181+
for traj in tc.trajectories:
182+
splits = TemporalSplitter(traj).split(mode=split_mode)
183+
tc_to_sink(splits, self.sink_pts, self.fields_pts, self.timestamp_field)
184+
for split in splits:
185+
traj_to_sink(split, self.sink_trajs)
186+
187+
188+

qgis_processing/trajectoryProvider.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@
2727

2828
sys.path.append("..")
2929

30-
from .trajectoriesFromPointLayerAlgorithm import TrajectoriesFromPointLayerAlgorithm
31-
from .clipTrajectoriesByExtentAlgorithm import ClipTrajectoriesByExtentAlgorithm
32-
from .addHeadingAlgorithm import AddHeadingAlgorithm
3330
from .createTrajectoriesAlgorithm import CreateTrajectoriesAlgorithm
34-
from .splitOnDayBreakAlgorithm import SplitOnDayBreakAlgorithm
31+
from .splitTrajectoriesAlgorithm import SplitTrajectoriesAlgorithm
3532

3633
pluginPath = os.path.dirname(__file__)
3734

@@ -66,6 +63,7 @@ def setActive(self, active):
6663

6764
def getAlgs(self):
6865
algs = [CreateTrajectoriesAlgorithm(),
66+
SplitTrajectoriesAlgorithm()
6967
]
7068
return algs
7169

0 commit comments

Comments
 (0)