Skip to content

Commit ccb72eb

Browse files
committed
[processing] Fixes for Service Area algorithms
- Output interpolated points when travel cost falls mid-way along an edge - Output all intermediate reachable points also - Make outputting upper/lower bound points optional, and non-default. Now by default we just output all definitely reachable points and the interpolated points along edges which correspond to the travel cost. This allows the output to be used to correctly generate service areas e.g. by concave/convex polygons and all reachable nodes will be included in the area. - Allow algorithm to optionally output a line layer (and make the point layer optional too, and default to just the line layer output) containing all reachable line segments (including interpolated segments of lines when the travel cost sits midway along that edge). This output is more easily understandably for users.
1 parent 2e7455c commit ccb72eb

23 files changed

+480
-76
lines changed

python/analysis/network/qgsgraphanalyzer.sip.in

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ Solve shortest path problem using Dijkstra algorithm
3030
:param source: source graph
3131
:param startVertexIdx: index of the start vertex
3232
:param criterionNum: index of the optimization strategy
33-
:param resultTree: array that represents shortest path tree. resultTree[ vertexIndex ] == inboundingArcIndex if vertex reachable, otherwise resultTree[ vertexIndex ] == -1
33+
:param resultTree: array that represents shortest path tree. resultTree[ vertexIndex ] == inboundingArcIndex if vertex reachable, otherwise resultTree[ vertexIndex ] == -1.
34+
Note that the startVertexIdx will also have a value of -1 and may need special handling by callers.
3435
:param resultCost: array of the paths costs
3536
%End
3637

python/plugins/processing/algs/qgis/ServiceAreaFromLayer.py

+111-37
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@
3737
QgsFeatureSink,
3838
QgsFeatureRequest,
3939
QgsGeometry,
40+
QgsGeometryUtils,
4041
QgsFields,
4142
QgsPointXY,
4243
QgsField,
4344
QgsProcessing,
45+
QgsProcessingParameterBoolean,
4446
QgsProcessingParameterEnum,
4547
QgsProcessingParameterPoint,
4648
QgsProcessingParameterField,
@@ -75,7 +77,9 @@ class ServiceAreaFromLayer(QgisAlgorithm):
7577
SPEED_FIELD = 'SPEED_FIELD'
7678
DEFAULT_SPEED = 'DEFAULT_SPEED'
7779
TOLERANCE = 'TOLERANCE'
80+
INCLUDE_BOUNDS = 'INCLUDE_BOUNDS'
7881
OUTPUT = 'OUTPUT'
82+
OUTPUT_LINES = 'OUTPUT_LINES'
7983

8084
def icon(self):
8185
return QIcon(os.path.join(pluginPath, 'images', 'networkanalysis.svg'))
@@ -146,14 +150,24 @@ def initAlgorithm(self, config=None):
146150
self.tr('Topology tolerance'),
147151
QgsProcessingParameterNumber.Double,
148152
0.0, False, 0, 99999999.99))
149-
153+
params.append(QgsProcessingParameterBoolean(self.INCLUDE_BOUNDS,
154+
self.tr('Include upper/lower bound points'),
155+
defaultValue=False))
150156
for p in params:
151157
p.setFlags(p.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
152158
self.addParameter(p)
153159

154-
self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT,
155-
self.tr('Service area (boundary nodes)'),
156-
QgsProcessing.TypeVectorPoint))
160+
lines_output = QgsProcessingParameterFeatureSink(self.OUTPUT_LINES,
161+
self.tr('Service area (lines)'),
162+
QgsProcessing.TypeVectorLine, optional=True)
163+
lines_output.setCreateByDefault(True)
164+
self.addParameter(lines_output)
165+
166+
nodes_output = QgsProcessingParameterFeatureSink(self.OUTPUT,
167+
self.tr('Service area (boundary nodes)'),
168+
QgsProcessing.TypeVectorPoint, optional=True)
169+
nodes_output.setCreateByDefault(False)
170+
self.addParameter(nodes_output)
157171

158172
def name(self):
159173
return 'serviceareafromlayer'
@@ -176,6 +190,10 @@ def processAlgorithm(self, parameters, context, feedback):
176190
defaultSpeed = self.parameterAsDouble(parameters, self.DEFAULT_SPEED, context)
177191
tolerance = self.parameterAsDouble(parameters, self.TOLERANCE, context)
178192

193+
include_bounds = True # default to true to maintain 3.0 API
194+
if self.INCLUDE_BOUNDS in parameters:
195+
include_bounds = self.parameterAsBool(parameters, self.INCLUDE_BOUNDS, context)
196+
179197
fields = startPoints.fields()
180198
fields.append(QgsField('type', QVariant.String, '', 254, 0))
181199
fields.append(QgsField('start', QVariant.String, '', 254, 0))
@@ -240,12 +258,11 @@ def processAlgorithm(self, parameters, context, feedback):
240258
feedback.pushInfo(QCoreApplication.translate('ServiceAreaFromLayer', 'Calculating service areas…'))
241259
graph = builder.graph()
242260

243-
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
244-
fields, QgsWkbTypes.MultiPoint, network.sourceCrs())
261+
(point_sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
262+
fields, QgsWkbTypes.MultiPoint, network.sourceCrs())
263+
(line_sink, line_dest_id) = self.parameterAsSink(parameters, self.OUTPUT_LINES, context,
264+
fields, QgsWkbTypes.MultiLineString, network.sourceCrs())
245265

246-
vertices = []
247-
upperBoundary = []
248-
lowerBoundary = []
249266
total = 100.0 / len(snappedPoints) if snappedPoints else 1
250267
for i, p in enumerate(snappedPoints):
251268
if feedback.isCanceled():
@@ -255,35 +272,92 @@ def processAlgorithm(self, parameters, context, feedback):
255272
origPoint = points[i].toString()
256273

257274
tree, cost = QgsGraphAnalyzer.dijkstra(graph, idxStart, 0)
258-
for j, v in enumerate(cost):
259-
if v > travelCost and tree[j] != -1:
260-
vertexId = graph.edge(tree[j]).fromVertex()
261-
if cost[vertexId] <= travelCost:
262-
vertices.append(j)
263-
264-
for j in vertices:
265-
upperBoundary.append(graph.vertex(graph.edge(tree[j]).toVertex()).point())
266-
lowerBoundary.append(graph.vertex(graph.edge(tree[j]).fromVertex()).point())
267-
268-
geomUpper = QgsGeometry.fromMultiPointXY(upperBoundary)
269-
geomLower = QgsGeometry.fromMultiPointXY(lowerBoundary)
270-
271-
feat.setGeometry(geomUpper)
272-
273-
attrs = source_attributes[i]
274-
attrs.extend(['upper', origPoint])
275-
feat.setAttributes(attrs)
276-
sink.addFeature(feat, QgsFeatureSink.FastInsert)
277-
278-
feat.setGeometry(geomLower)
279-
attrs[-2] = 'lower'
280-
feat.setAttributes(attrs)
281-
sink.addFeature(feat, QgsFeatureSink.FastInsert)
282275

283-
vertices[:] = []
284-
upperBoundary[:] = []
285-
lowerBoundary[:] = []
276+
vertices = set()
277+
area_points = []
278+
lines = []
279+
for vertex, start_vertex_cost in enumerate(cost):
280+
inbound_edge_index = tree[vertex]
281+
if inbound_edge_index == -1 and vertex != idxStart:
282+
# unreachable vertex
283+
continue
284+
285+
if start_vertex_cost > travelCost:
286+
# vertex is too expensive, discard
287+
continue
288+
289+
vertices.add(vertex)
290+
start_point = graph.vertex(vertex).point()
291+
292+
# find all edges coming from this vertex
293+
for edge_id in graph.vertex(vertex).outgoingEdges():
294+
edge = graph.edge(edge_id)
295+
end_vertex_cost = start_vertex_cost + edge.cost(0)
296+
end_point = graph.vertex(edge.toVertex()).point()
297+
if end_vertex_cost <= travelCost:
298+
# end vertex is cheap enough to include
299+
vertices.add(edge.toVertex())
300+
lines.append([start_point, end_point])
301+
else:
302+
# travelCost sits somewhere on this edge, interpolate position
303+
interpolated_end_point = QgsGeometryUtils.interpolatePointOnLineByValue(start_point.x(), start_point.y(), start_vertex_cost,
304+
end_point.x(), end_point.y(), end_vertex_cost, travelCost)
305+
area_points.append(interpolated_end_point)
306+
lines.append([start_point, interpolated_end_point])
307+
308+
for v in vertices:
309+
area_points.append(graph.vertex(v).point())
310+
311+
feat = QgsFeature()
312+
if point_sink is not None:
313+
geomPoints = QgsGeometry.fromMultiPointXY(area_points)
314+
feat.setGeometry(geomPoints)
315+
attrs = source_attributes[i]
316+
attrs.extend(['within', origPoint])
317+
feat.setAttributes(attrs)
318+
point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
319+
320+
if include_bounds:
321+
upperBoundary = []
322+
lowerBoundary = []
323+
324+
vertices = []
325+
for vertex, c in enumerate(cost):
326+
if c > travelCost and tree[vertex] != -1:
327+
vertexId = graph.edge(tree[vertex]).fromVertex()
328+
if cost[vertexId] <= travelCost:
329+
vertices.append(vertex)
330+
331+
for v in vertices:
332+
upperBoundary.append(graph.vertex(graph.edge(tree[v]).toVertex()).point())
333+
lowerBoundary.append(graph.vertex(graph.edge(tree[v]).fromVertex()).point())
334+
335+
geomUpper = QgsGeometry.fromMultiPointXY(upperBoundary)
336+
geomLower = QgsGeometry.fromMultiPointXY(lowerBoundary)
337+
338+
feat.setGeometry(geomUpper)
339+
attrs[-2] = 'upper'
340+
feat.setAttributes(attrs)
341+
point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
342+
343+
feat.setGeometry(geomLower)
344+
attrs[-2] = 'lower'
345+
feat.setAttributes(attrs)
346+
point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
347+
348+
if line_sink is not None:
349+
geom_lines = QgsGeometry.fromMultiPolylineXY(lines)
350+
feat.setGeometry(geom_lines)
351+
attrs = source_attributes[i]
352+
attrs.extend(['lines', origPoint])
353+
feat.setAttributes(attrs)
354+
line_sink.addFeature(feat, QgsFeatureSink.FastInsert)
286355

287356
feedback.setProgress(int(i * total))
288357

289-
return {self.OUTPUT: dest_id}
358+
results = {}
359+
if point_sink is not None:
360+
results[self.OUTPUT] = dest_id
361+
if line_sink is not None:
362+
results[self.OUTPUT_LINES] = line_dest_id
363+
return results

0 commit comments

Comments
 (0)