diff --git a/python/analysis/network/qgsgraphanalyzer.sip.in b/python/analysis/network/qgsgraphanalyzer.sip.in
index c8edc14367c..b42d419cadd 100644
--- a/python/analysis/network/qgsgraphanalyzer.sip.in
+++ b/python/analysis/network/qgsgraphanalyzer.sip.in
@@ -30,7 +30,8 @@ Solve shortest path problem using Dijkstra algorithm
:param source: source graph
:param startVertexIdx: index of the start vertex
:param criterionNum: index of the optimization strategy
-:param resultTree: array that represents shortest path tree. resultTree[ vertexIndex ] == inboundingArcIndex if vertex reachable, otherwise resultTree[ vertexIndex ] == -1
+:param resultTree: array that represents shortest path tree. resultTree[ vertexIndex ] == inboundingArcIndex if vertex reachable, otherwise resultTree[ vertexIndex ] == -1.
+Note that the startVertexIdx will also have a value of -1 and may need special handling by callers.
:param resultCost: array of the paths costs
%End
diff --git a/python/plugins/processing/algs/qgis/ServiceAreaFromLayer.py b/python/plugins/processing/algs/qgis/ServiceAreaFromLayer.py
index 1b18565d711..5564b3ba6ef 100644
--- a/python/plugins/processing/algs/qgis/ServiceAreaFromLayer.py
+++ b/python/plugins/processing/algs/qgis/ServiceAreaFromLayer.py
@@ -37,10 +37,12 @@ from qgis.core import (QgsWkbTypes,
QgsFeatureSink,
QgsFeatureRequest,
QgsGeometry,
+ QgsGeometryUtils,
QgsFields,
QgsPointXY,
QgsField,
QgsProcessing,
+ QgsProcessingParameterBoolean,
QgsProcessingParameterEnum,
QgsProcessingParameterPoint,
QgsProcessingParameterField,
@@ -75,7 +77,9 @@ class ServiceAreaFromLayer(QgisAlgorithm):
SPEED_FIELD = 'SPEED_FIELD'
DEFAULT_SPEED = 'DEFAULT_SPEED'
TOLERANCE = 'TOLERANCE'
+ INCLUDE_BOUNDS = 'INCLUDE_BOUNDS'
OUTPUT = 'OUTPUT'
+ OUTPUT_LINES = 'OUTPUT_LINES'
def icon(self):
return QIcon(os.path.join(pluginPath, 'images', 'networkanalysis.svg'))
@@ -146,14 +150,24 @@ class ServiceAreaFromLayer(QgisAlgorithm):
self.tr('Topology tolerance'),
QgsProcessingParameterNumber.Double,
0.0, False, 0, 99999999.99))
-
+ params.append(QgsProcessingParameterBoolean(self.INCLUDE_BOUNDS,
+ self.tr('Include upper/lower bound points'),
+ defaultValue=False))
for p in params:
p.setFlags(p.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
self.addParameter(p)
- self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT,
- self.tr('Service area (boundary nodes)'),
- QgsProcessing.TypeVectorPoint))
+ lines_output = QgsProcessingParameterFeatureSink(self.OUTPUT_LINES,
+ self.tr('Service area (lines)'),
+ QgsProcessing.TypeVectorLine, optional=True)
+ lines_output.setCreateByDefault(True)
+ self.addParameter(lines_output)
+
+ nodes_output = QgsProcessingParameterFeatureSink(self.OUTPUT,
+ self.tr('Service area (boundary nodes)'),
+ QgsProcessing.TypeVectorPoint, optional=True)
+ nodes_output.setCreateByDefault(False)
+ self.addParameter(nodes_output)
def name(self):
return 'serviceareafromlayer'
@@ -176,6 +190,10 @@ class ServiceAreaFromLayer(QgisAlgorithm):
defaultSpeed = self.parameterAsDouble(parameters, self.DEFAULT_SPEED, context)
tolerance = self.parameterAsDouble(parameters, self.TOLERANCE, context)
+ include_bounds = True # default to true to maintain 3.0 API
+ if self.INCLUDE_BOUNDS in parameters:
+ include_bounds = self.parameterAsBool(parameters, self.INCLUDE_BOUNDS, context)
+
fields = startPoints.fields()
fields.append(QgsField('type', QVariant.String, '', 254, 0))
fields.append(QgsField('start', QVariant.String, '', 254, 0))
@@ -240,12 +258,11 @@ class ServiceAreaFromLayer(QgisAlgorithm):
feedback.pushInfo(QCoreApplication.translate('ServiceAreaFromLayer', 'Calculating service areas…'))
graph = builder.graph()
- (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
- fields, QgsWkbTypes.MultiPoint, network.sourceCrs())
+ (point_sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
+ fields, QgsWkbTypes.MultiPoint, network.sourceCrs())
+ (line_sink, line_dest_id) = self.parameterAsSink(parameters, self.OUTPUT_LINES, context,
+ fields, QgsWkbTypes.MultiLineString, network.sourceCrs())
- vertices = []
- upperBoundary = []
- lowerBoundary = []
total = 100.0 / len(snappedPoints) if snappedPoints else 1
for i, p in enumerate(snappedPoints):
if feedback.isCanceled():
@@ -255,35 +272,92 @@ class ServiceAreaFromLayer(QgisAlgorithm):
origPoint = points[i].toString()
tree, cost = QgsGraphAnalyzer.dijkstra(graph, idxStart, 0)
- for j, v in enumerate(cost):
- if v > travelCost and tree[j] != -1:
- vertexId = graph.edge(tree[j]).fromVertex()
- if cost[vertexId] <= travelCost:
- vertices.append(j)
- for j in vertices:
- upperBoundary.append(graph.vertex(graph.edge(tree[j]).toVertex()).point())
- lowerBoundary.append(graph.vertex(graph.edge(tree[j]).fromVertex()).point())
+ vertices = set()
+ area_points = []
+ lines = []
+ for vertex, start_vertex_cost in enumerate(cost):
+ inbound_edge_index = tree[vertex]
+ if inbound_edge_index == -1 and vertex != idxStart:
+ # unreachable vertex
+ continue
- geomUpper = QgsGeometry.fromMultiPointXY(upperBoundary)
- geomLower = QgsGeometry.fromMultiPointXY(lowerBoundary)
+ if start_vertex_cost > travelCost:
+ # vertex is too expensive, discard
+ continue
- feat.setGeometry(geomUpper)
+ vertices.add(vertex)
+ start_point = graph.vertex(vertex).point()
- attrs = source_attributes[i]
- attrs.extend(['upper', origPoint])
- feat.setAttributes(attrs)
- sink.addFeature(feat, QgsFeatureSink.FastInsert)
+ # find all edges coming from this vertex
+ for edge_id in graph.vertex(vertex).outgoingEdges():
+ edge = graph.edge(edge_id)
+ end_vertex_cost = start_vertex_cost + edge.cost(0)
+ end_point = graph.vertex(edge.toVertex()).point()
+ if end_vertex_cost <= travelCost:
+ # end vertex is cheap enough to include
+ vertices.add(edge.toVertex())
+ lines.append([start_point, end_point])
+ else:
+ # travelCost sits somewhere on this edge, interpolate position
+ interpolated_end_point = QgsGeometryUtils.interpolatePointOnLineByValue(start_point.x(), start_point.y(), start_vertex_cost,
+ end_point.x(), end_point.y(), end_vertex_cost, travelCost)
+ area_points.append(interpolated_end_point)
+ lines.append([start_point, interpolated_end_point])
- feat.setGeometry(geomLower)
- attrs[-2] = 'lower'
- feat.setAttributes(attrs)
- sink.addFeature(feat, QgsFeatureSink.FastInsert)
+ for v in vertices:
+ area_points.append(graph.vertex(v).point())
- vertices[:] = []
- upperBoundary[:] = []
- lowerBoundary[:] = []
+ feat = QgsFeature()
+ if point_sink is not None:
+ geomPoints = QgsGeometry.fromMultiPointXY(area_points)
+ feat.setGeometry(geomPoints)
+ attrs = source_attributes[i]
+ attrs.extend(['within', origPoint])
+ feat.setAttributes(attrs)
+ point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
+
+ if include_bounds:
+ upperBoundary = []
+ lowerBoundary = []
+
+ vertices = []
+ for vertex, c in enumerate(cost):
+ if c > travelCost and tree[vertex] != -1:
+ vertexId = graph.edge(tree[vertex]).fromVertex()
+ if cost[vertexId] <= travelCost:
+ vertices.append(vertex)
+
+ for v in vertices:
+ upperBoundary.append(graph.vertex(graph.edge(tree[v]).toVertex()).point())
+ lowerBoundary.append(graph.vertex(graph.edge(tree[v]).fromVertex()).point())
+
+ geomUpper = QgsGeometry.fromMultiPointXY(upperBoundary)
+ geomLower = QgsGeometry.fromMultiPointXY(lowerBoundary)
+
+ feat.setGeometry(geomUpper)
+ attrs[-2] = 'upper'
+ feat.setAttributes(attrs)
+ point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
+
+ feat.setGeometry(geomLower)
+ attrs[-2] = 'lower'
+ feat.setAttributes(attrs)
+ point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
+
+ if line_sink is not None:
+ geom_lines = QgsGeometry.fromMultiPolylineXY(lines)
+ feat.setGeometry(geom_lines)
+ attrs = source_attributes[i]
+ attrs.extend(['lines', origPoint])
+ feat.setAttributes(attrs)
+ line_sink.addFeature(feat, QgsFeatureSink.FastInsert)
feedback.setProgress(int(i * total))
- return {self.OUTPUT: dest_id}
+ results = {}
+ if point_sink is not None:
+ results[self.OUTPUT] = dest_id
+ if line_sink is not None:
+ results[self.OUTPUT_LINES] = line_dest_id
+ return results
diff --git a/python/plugins/processing/algs/qgis/ServiceAreaFromPoint.py b/python/plugins/processing/algs/qgis/ServiceAreaFromPoint.py
index 27a23ad49db..aae814e5e05 100644
--- a/python/plugins/processing/algs/qgis/ServiceAreaFromPoint.py
+++ b/python/plugins/processing/algs/qgis/ServiceAreaFromPoint.py
@@ -36,9 +36,11 @@ from qgis.core import (QgsWkbTypes,
QgsFeature,
QgsFeatureSink,
QgsGeometry,
+ QgsGeometryUtils,
QgsFields,
QgsField,
QgsProcessing,
+ QgsProcessingParameterBoolean,
QgsProcessingParameterEnum,
QgsProcessingParameterPoint,
QgsProcessingParameterField,
@@ -73,7 +75,9 @@ class ServiceAreaFromPoint(QgisAlgorithm):
SPEED_FIELD = 'SPEED_FIELD'
DEFAULT_SPEED = 'DEFAULT_SPEED'
TOLERANCE = 'TOLERANCE'
+ INCLUDE_BOUNDS = 'INCLUDE_BOUNDS'
OUTPUT = 'OUTPUT'
+ OUTPUT_LINES = 'OUTPUT_LINES'
def icon(self):
return QIcon(os.path.join(pluginPath, 'images', 'networkanalysis.svg'))
@@ -143,14 +147,25 @@ class ServiceAreaFromPoint(QgisAlgorithm):
self.tr('Topology tolerance'),
QgsProcessingParameterNumber.Double,
0.0, False, 0, 99999999.99))
+ params.append(QgsProcessingParameterBoolean(self.INCLUDE_BOUNDS,
+ self.tr('Include upper/lower bound points'),
+ defaultValue=False))
for p in params:
p.setFlags(p.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
self.addParameter(p)
- self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT,
- self.tr('Service area (boundary nodes)'),
- QgsProcessing.TypeVectorPoint))
+ lines_output = QgsProcessingParameterFeatureSink(self.OUTPUT_LINES,
+ self.tr('Service area (lines)'),
+ QgsProcessing.TypeVectorLine, optional=True)
+ lines_output.setCreateByDefault(True)
+ self.addParameter(lines_output)
+
+ nodes_output = QgsProcessingParameterFeatureSink(self.OUTPUT,
+ self.tr('Service area (boundary nodes)'),
+ QgsProcessing.TypeVectorPoint, optional=True)
+ nodes_output.setCreateByDefault(False)
+ self.addParameter(nodes_output)
def name(self):
return 'serviceareafrompoint'
@@ -173,6 +188,10 @@ class ServiceAreaFromPoint(QgisAlgorithm):
defaultSpeed = self.parameterAsDouble(parameters, self.DEFAULT_SPEED, context)
tolerance = self.parameterAsDouble(parameters, self.TOLERANCE, context)
+ include_bounds = True # default to true to maintain 3.0 API
+ if self.INCLUDE_BOUNDS in parameters:
+ include_bounds = self.parameterAsBool(parameters, self.INCLUDE_BOUNDS, context)
+
directionField = -1
if directionFieldName:
directionField = network.fields().lookupField(directionFieldName)
@@ -208,18 +227,41 @@ class ServiceAreaFromPoint(QgisAlgorithm):
idxStart = graph.findVertex(snappedPoints[0])
tree, cost = QgsGraphAnalyzer.dijkstra(graph, idxStart, 0)
- vertices = []
- for i, v in enumerate(cost):
- if v > travelCost and tree[i] != -1:
- vertexId = graph.edge(tree[i]).fromVertex()
- if cost[vertexId] <= travelCost:
- vertices.append(i)
+ vertices = set()
+ points = []
+ lines = []
+
+ for vertex, start_vertex_cost in enumerate(cost):
+ inbound_edge_index = tree[vertex]
+ if inbound_edge_index == -1 and vertex != idxStart:
+ # unreachable vertex
+ continue
+
+ if start_vertex_cost > travelCost:
+ # vertex is too expensive, discard
+ continue
+
+ vertices.add(vertex)
+ start_point = graph.vertex(vertex).point()
+
+ # find all edges coming from this vertex
+ for edge_id in graph.vertex(vertex).outgoingEdges():
+ edge = graph.edge(edge_id)
+ end_vertex_cost = start_vertex_cost + edge.cost(0)
+ end_point = graph.vertex(edge.toVertex()).point()
+ if end_vertex_cost <= travelCost:
+ # end vertex is cheap enough to include
+ vertices.add(edge.toVertex())
+ lines.append([start_point, end_point])
+ else:
+ # travelCost sits somewhere on this edge, interpolate position
+ interpolated_end_point = QgsGeometryUtils.interpolatePointOnLineByValue(start_point.x(), start_point.y(), start_vertex_cost,
+ end_point.x(), end_point.y(), end_vertex_cost, travelCost)
+ points.append(interpolated_end_point)
+ lines.append([start_point, interpolated_end_point])
- upperBoundary = []
- lowerBoundary = []
for i in vertices:
- upperBoundary.append(graph.vertex(graph.edge(tree[i]).toVertex()).point())
- lowerBoundary.append(graph.vertex(graph.edge(tree[i]).fromVertex()).point())
+ points.append(graph.vertex(i).point())
feedback.pushInfo(QCoreApplication.translate('ServiceAreaFromPoint', 'Writing results…'))
@@ -230,25 +272,55 @@ class ServiceAreaFromPoint(QgisAlgorithm):
feat = QgsFeature()
feat.setFields(fields)
- geomUpper = QgsGeometry.fromMultiPointXY(upperBoundary)
- geomLower = QgsGeometry.fromMultiPointXY(lowerBoundary)
+ (point_sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
+ fields, QgsWkbTypes.MultiPoint, network.sourceCrs())
- (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
- fields, QgsWkbTypes.MultiPoint, network.sourceCrs())
+ results = {}
- feat.setGeometry(geomUpper)
- feat['type'] = 'upper'
- feat['start'] = startPoint.toString()
- sink.addFeature(feat, QgsFeatureSink.FastInsert)
+ if point_sink is not None:
+ results[self.OUTPUT] = dest_id
+ geomPoints = QgsGeometry.fromMultiPointXY(points)
+ feat.setGeometry(geomPoints)
+ feat['type'] = 'within'
+ feat['start'] = startPoint.toString()
+ point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
- feat.setGeometry(geomLower)
- feat['type'] = 'lower'
- feat['start'] = startPoint.toString()
- sink.addFeature(feat, QgsFeatureSink.FastInsert)
+ if include_bounds:
+ upperBoundary = []
+ lowerBoundary = []
- upperBoundary.append(startPoint)
- lowerBoundary.append(startPoint)
- geomUpper = QgsGeometry.fromMultiPointXY(upperBoundary)
- geomLower = QgsGeometry.fromMultiPointXY(lowerBoundary)
+ vertices = []
+ for i, v in enumerate(cost):
+ if v > travelCost and tree[i] != -1:
+ vertexId = graph.edge(tree[i]).fromVertex()
+ if cost[vertexId] <= travelCost:
+ vertices.append(i)
- return {self.OUTPUT: dest_id}
+ for i in vertices:
+ upperBoundary.append(graph.vertex(graph.edge(tree[i]).toVertex()).point())
+ lowerBoundary.append(graph.vertex(graph.edge(tree[i]).fromVertex()).point())
+
+ geomUpper = QgsGeometry.fromMultiPointXY(upperBoundary)
+ geomLower = QgsGeometry.fromMultiPointXY(lowerBoundary)
+
+ feat.setGeometry(geomUpper)
+ feat['type'] = 'upper'
+ feat['start'] = startPoint.toString()
+ point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
+
+ feat.setGeometry(geomLower)
+ feat['type'] = 'lower'
+ feat['start'] = startPoint.toString()
+ point_sink.addFeature(feat, QgsFeatureSink.FastInsert)
+
+ (line_sink, line_dest_id) = self.parameterAsSink(parameters, self.OUTPUT_LINES, context,
+ fields, QgsWkbTypes.MultiLineString, network.sourceCrs())
+ if line_sink is not None:
+ results[self.OUTPUT_LINES] = line_dest_id
+ geom_lines = QgsGeometry.fromMultiPolylineXY(lines)
+ feat.setGeometry(geom_lines)
+ feat['type'] = 'lines'
+ feat['start'] = startPoint.toString()
+ line_sink.addFeature(feat, QgsFeatureSink.FastInsert)
+
+ return results
diff --git a/python/plugins/processing/tests/testdata/expected/service_area.gml b/python/plugins/processing/tests/testdata/expected/service_area.gml
index 30f4d5bf76d..7a654dd6a0a 100644
--- a/python/plugins/processing/tests/testdata/expected/service_area.gml
+++ b/python/plugins/processing/tests/testdata/expected/service_area.gml
@@ -13,13 +13,20 @@
+ 1001909.22669746,6221447.427092561001952.34123543,6221487.302427461001953.43051079,6221448.674502121002032.5709223,6221550.812426151002969.05030799,6221676.742202881002829.94223561,6222321.008570481003004.11226582,6222163.116468851002963.52384506,6222257.458032321003019.60535174,6222304.927858961002967.72944989,6222310.074124971002200.16938687,6221990.048830961002131.79656056,6222131.16160011002094.78459206,6222017.329911031002062.09347554,6221990.715186841001967.62712745,6221918.337508791001980.19825308,6222000.742807521002050.07703423,6221904.368922031001968.4230433,6221742.687093091002428.50365906,6222266.91061481002338.00077012,6222370.250298361002477.4573779,6221349.031034741002210.48174146,6221405.957905231002325.82583316,6221391.575579661002739.97402922,6222338.53625281002709.22816052,6222314.305045491002736.08018519,6222311.405118011002897.23102887,6221326.986762791002788.41696794,6221470.768308831002786.2954101,6221332.834313211002904.54858627,6221469.124573861002648.98946836,6222212.98522061002901.19905976,6222006.390093191002888.74299755,6221990.937612681002679.03257324,6221723.12466281002842.33788343,6222175.197448551002076.31366647,6221866.903615331002045.66496862,6221837.849917271002448.62305961,6222244.851089561002382.21032908,6222317.668035661002464.2146579,6221876.534596191002131.61040283,6221796.629652131002302.22841804,6221576.642583061002310.58466849,6221563.422003221002207.17904492,6221977.82108571002275.1898604,6221455.195880411002226.82368085,6221517.628435221002532.83636772,6221785.381365211002421.46534272,6221674.175717251002526.28363451,6221554.702499941002485.75145034,6221954.550744191002621.6858386,6222058.822411011002529.45107897,6222163.925369651001930.78353526,6221467.365215941001968.37489204,6221502.130836841002137.81278244,6221630.620180511002215.13556068,6221689.254468871002288.4678461,6221744.859644951002334.20638747,6221779.490982841002425.63766482,6221848.744465431002462.62346654,6221875.387804421002518.06990805,6221915.348757411002626.85249869,6221993.75622021002657.33259138,6222018.206205361002781.74783802,6222107.451932551002852.18578258,6222162.740329831002903.48249244,6222204.647257571002916.58033846,6222216.167497841002945.17950103,6222241.318560991002991.11072264,6222281.729143861002643.23398503,6221648.434425311002623.85396603,6221672.144810971002624.53430232,6222242.617699631002724.17727046,6222326.958407111002684.54187821,6222169.90614121002845.50542017,6221400.906928381002893.44547534,6221999.893574851002813.21865203,6221847.099999621002779.14071154,6222255.138831141002161.08326842,6222058.231201131002125.77638469,6222042.55745981002015.92985521,6221953.130307371002509.88988421,6222183.568354631002380.76911747,6222089.858594961002284.1421556,6222019.722741821002233.59814873,6221951.119522031002200.84267254,6221919.961915281002141.64083448,6221887.81776555
+ within
+ 1002465.00896, 6221875.43249
+
+
+
+
1001891.40736133,6221430.945819861003040.80096358,6222322.184127111002622.66996036,6221162.468077261001843.08473263,6221828.628735431002086.66939634,6222010.72408161001940.09434754,6221707.785856711001962.5674552,6222024.235914931002299.19357268,6222416.407063981002103.66905047,6221324.681541261002129.95849754,6222135.738784741002416.57059498,6221277.561594651002897.32222975,6222395.428388881002761.33635685,6222354.193245271002897.87933631,6221326.060277931003037.55521283,6222118.738635351003028.1814962,6221611.968302891002667.16458207,6221195.87188063
upper
1002465.00896, 6221875.43249
-
+
1001930.78353526,6221467.365215941002991.11072264,6222281.729143861002310.58466849,6221563.422003221002015.92985521,6221953.130307371002125.77638469,6222042.55745981002045.66496862,6221837.849917271002015.92985521,6221953.130307371002382.21032908,6222317.668035661002275.1898604,6221455.195880411002161.08326842,6222058.231201131002275.1898604,6221455.195880411002991.11072264,6222281.729143861002724.17727046,6222326.958407111002845.50542017,6221400.906928381002945.17950103,6222241.318560991002657.33259138,6222018.206205361002845.50542017,6221400.90692838
lower
1002465.00896, 6221875.43249
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer.dbf b/python/plugins/processing/tests/testdata/expected/service_area_from_layer.dbf
index 4f73679e8b1..66e7035ab09 100644
Binary files a/python/plugins/processing/tests/testdata/expected/service_area_from_layer.dbf and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer.dbf differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shp b/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shp
index f805525f46d..26cd0dbaada 100644
Binary files a/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shp and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shp differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shx b/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shx
index f5dd58fb129..7e81e453fc3 100644
Binary files a/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shx and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer.shx differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.dbf b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.dbf
new file mode 100644
index 00000000000..0f950b2bc56
Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.dbf differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.prj b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.prj
new file mode 100644
index 00000000000..55aa2f4a4f7
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.prj
@@ -0,0 +1 @@
+PROJCS["WGS_1984_UTM_Zone_33S",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",15],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["Meter",1]]
\ No newline at end of file
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.qpj b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.qpj
new file mode 100644
index 00000000000..6b27046c82c
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.qpj
@@ -0,0 +1 @@
+PROJCS["WGS 84 / UTM zone 33S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",15],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32733"]]
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.shp b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.shp
new file mode 100644
index 00000000000..bab1c262cfd
Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.shp differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.shx b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.shx
new file mode 100644
index 00000000000..142a89f4770
Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines.shx differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.dbf b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.dbf
new file mode 100644
index 00000000000..cd37a4c3528
Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.dbf differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.prj b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.prj
new file mode 100644
index 00000000000..55aa2f4a4f7
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.prj
@@ -0,0 +1 @@
+PROJCS["WGS_1984_UTM_Zone_33S",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",15],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["Meter",1]]
\ No newline at end of file
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.qpj b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.qpj
new file mode 100644
index 00000000000..6b27046c82c
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.qpj
@@ -0,0 +1 @@
+PROJCS["WGS 84 / UTM zone 33S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",15],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32733"]]
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.shp b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.shp
new file mode 100644
index 00000000000..8ed22c957b9
Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.shp differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.shx b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.shx
new file mode 100644
index 00000000000..d474bc53a3d
Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_no_bounds.shx differ
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_lines.gml b/python/plugins/processing/tests/testdata/expected/service_area_lines.gml
new file mode 100644
index 00000000000..b0a2a85f931
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_lines.gml
@@ -0,0 +1,21 @@
+
+
+
+
+ 1003314.0958517156222106.12154613
+ 1003967.8003082366222690.130282185
+
+
+
+
+
+ 1003318.51056419,6222558.84948158 1003323.47521749,6222559.423055791003318.51056419,6222558.84948158 1003322.48706454,6222561.876723961003318.51056419,6222558.84948158 1003314.09585172,6222556.507037821003416.77290022,6222570.20188393 1003318.51056419,6222558.849481581003416.77290022,6222570.20188393 1003430.63468333,6222565.775291461003430.63468333,6222565.77529146 1003416.77290022,6222570.201883931003430.63468333,6222565.77529146 1003461.44609399,6222555.866161331003430.63468333,6222565.77529146 1003403.86741564,6222623.831099291003461.44609399,6222555.86616133 1003430.63468333,6222565.775291461003461.44609399,6222555.86616133 1003536.86501513,6222502.07425481003536.86501513,6222502.0742548 1003461.44609399,6222555.866161331003536.86501513,6222502.0742548 1003555.31281829,6222483.134397121003555.31281829,6222483.13439712 1003536.86501513,6222502.07425481003555.31281829,6222483.13439712 1003602.8338769,6222436.678746131003555.31281829,6222483.13439712 1003736.7650073,6222682.947308631003602.8338769,6222436.67874613 1003555.31281829,6222483.134397121003602.8338769,6222436.67874613 1003647.3751082,6222401.821005671003602.8338769,6222436.67874613 1003483.81137232,6222286.605794041003647.3751082,6222401.82100567 1003602.8338769,6222436.678746131003647.3751082,6222401.82100567 1003653.19070942,6222396.085075091003647.3751082,6222401.82100567 1003696.4437017,6222455.048662211003692.73332615,6222357.0841711 1003653.19070942,6222396.085075091003692.73332615,6222357.0841711 1003745.40906699,6222317.558843681003745.40906699,6222317.55884368 1003692.73332615,6222357.08417111003745.40906699,6222317.55884368 1003883.50731708,6222232.580743361003745.40906699,6222317.55884368 1003596.67006395,6222145.795062481003883.50731708,6222232.58074336 1003783.39573872,6222294.183926191003883.50731708,6222232.58074336 1003964.66165518,6222154.641869611003964.66165518,6222154.64186961 1003961.03504388,6222158.124788791003964.66165518,6222154.64186961 1003967.80030824,6222150.7135191003403.86741564,6222623.83109929 1003360.47539199,6222590.796631071003403.86741564,6222623.83109929 1003440.28158698,6222664.42858051003403.86741564,6222623.83109929 1003426.7015651,6222574.305872871003394.25380049,6222340.63979273 1003373.83959122,6222318.823411221003394.25380049,6222340.63979273 1003413.414808,6222363.564712691003394.25380049,6222340.63979273 1003416.23997955,6222320.40853971003441.51556356,6222297.15044041 1003394.25380049,6222340.639792731003441.51556356,6222297.15044041 1003465.11136422,6222304.28925441003465.11136422,6222304.2892544 1003441.51556356,6222297.150440411003465.11136422,6222304.2892544 1003483.26027016,6222288.617839171003483.26027016,6222288.61783917 1003465.11136422,6222304.28925441003483.26027016,6222288.61783917 1003483.81137232,6222286.605794041003483.81137232,6222286.60579404 1003483.26027016,6222288.617839171003483.81137232,6222286.60579404 1003573.80211768,6222400.073216561003483.81137232,6222286.60579404 1003472.3070191,6222272.116305681003472.3070191,6222272.11630568 1003483.81137232,6222286.605794041003472.3070191,6222272.11630568 1003406.90500878,6222189.641588061003472.3070191,6222272.11630568 1003560.92830067,6222182.09960921003406.90500878,6222189.64158806 1003419.99086281,6222206.143406751003406.90500878,6222189.64158806 1003422.43926795,6222175.420603471003406.90500878,6222189.64158806 1003392.71936137,6222205.20809791003596.67006395,6222145.79506248 1003559.85075068,6222183.19412611003596.67006395,6222145.79506248 1003631.02591393,6222185.469193531003596.67006395,6222145.79506248 1003562.31361325,6222106.121546131003696.4437017,6222455.04866221 1003647.3751082,6222401.821005671003696.4437017,6222455.04866221 1003745.10253898,6222507.540625781003696.4437017,6222455.04866221 1003730.70401858,6222428.970778021003745.10253898,6222507.54062578 1003696.4437017,6222455.048662211003745.10253898,6222507.54062578 1003852.72754463,6222623.657706781003745.10253898,6222507.54062578 1003776.08011855,6222475.567477461003852.72754463,6222623.65770678 1003791.11900533,6222557.187989031003852.72754463,6222623.65770678 1003914.33323318,6222690.130282191003852.72754463,6222623.65770678 1003784.38305434,6222683.179698491003852.72754463,6222623.65770678 1003921.7483249,6222564.921096321003776.08011855,6222475.56747746 1003745.10253898,6222507.540625781003776.08011855,6222475.56747746 1003730.70401858,6222428.970778021003730.70401858,6222428.97077802 1003776.08011855,6222475.567477461003730.70401858,6222428.97077802 1003696.4437017,6222455.048662211003653.19070942,6222396.08507509 1003647.3751082,6222401.821005671003653.19070942,6222396.08507509 1003692.73332615,6222357.0841711
+ lines
+ 1003654.80653, 6222397.72334
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_lines.xsd b/python/plugins/processing/tests/testdata/expected/service_area_lines.xsd
new file mode 100644
index 00000000000..2ad1a84e8dd
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_lines.xsd
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_no_bounds.gml b/python/plugins/processing/tests/testdata/expected/service_area_no_bounds.gml
new file mode 100644
index 00000000000..b088efb9cd6
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_no_bounds.gml
@@ -0,0 +1,21 @@
+
+
+
+
+ 1001909.226697466221326.986762793
+ 1003019.605351746222370.250298364
+
+
+
+
+
+ 1001909.22669746,6221447.427092561001952.34123543,6221487.302427461001953.43051079,6221448.674502121002032.5709223,6221550.812426151002969.05030799,6221676.742202881002829.94223561,6222321.008570481003004.11226582,6222163.116468851002963.52384506,6222257.458032321003019.60535174,6222304.927858961002967.72944989,6222310.074124971002200.16938687,6221990.048830961002131.79656056,6222131.16160011002094.78459206,6222017.329911031002062.09347554,6221990.715186841001967.62712745,6221918.337508791001980.19825308,6222000.742807521002050.07703423,6221904.368922031001968.4230433,6221742.687093091002428.50365906,6222266.91061481002338.00077012,6222370.250298361002477.4573779,6221349.031034741002210.48174146,6221405.957905231002325.82583316,6221391.575579661002739.97402922,6222338.53625281002709.22816052,6222314.305045491002736.08018519,6222311.405118011002897.23102887,6221326.986762791002788.41696794,6221470.768308831002786.2954101,6221332.834313211002904.54858627,6221469.124573861002648.98946836,6222212.98522061002901.19905976,6222006.390093191002888.74299755,6221990.937612681002679.03257324,6221723.12466281002842.33788343,6222175.197448551002076.31366647,6221866.903615331002045.66496862,6221837.849917271002448.62305961,6222244.851089561002382.21032908,6222317.668035661002464.2146579,6221876.534596191002131.61040283,6221796.629652131002302.22841804,6221576.642583061002310.58466849,6221563.422003221002207.17904492,6221977.82108571002275.1898604,6221455.195880411002226.82368085,6221517.628435221002532.83636772,6221785.381365211002421.46534272,6221674.175717251002526.28363451,6221554.702499941002485.75145034,6221954.550744191002621.6858386,6222058.822411011002529.45107897,6222163.925369651001930.78353526,6221467.365215941001968.37489204,6221502.130836841002137.81278244,6221630.620180511002215.13556068,6221689.254468871002288.4678461,6221744.859644951002334.20638747,6221779.490982841002425.63766482,6221848.744465431002462.62346654,6221875.387804421002518.06990805,6221915.348757411002626.85249869,6221993.75622021002657.33259138,6222018.206205361002781.74783802,6222107.451932551002852.18578258,6222162.740329831002903.48249244,6222204.647257571002916.58033846,6222216.167497841002945.17950103,6222241.318560991002991.11072264,6222281.729143861002643.23398503,6221648.434425311002623.85396603,6221672.144810971002624.53430232,6222242.617699631002724.17727046,6222326.958407111002684.54187821,6222169.90614121002845.50542017,6221400.906928381002893.44547534,6221999.893574851002813.21865203,6221847.099999621002779.14071154,6222255.138831141002161.08326842,6222058.231201131002125.77638469,6222042.55745981002015.92985521,6221953.130307371002509.88988421,6222183.568354631002380.76911747,6222089.858594961002284.1421556,6222019.722741821002233.59814873,6221951.119522031002200.84267254,6221919.961915281002141.64083448,6221887.81776555
+ within
+ 1002465.00896, 6221875.43249
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/service_area_no_bounds.xsd b/python/plugins/processing/tests/testdata/expected/service_area_no_bounds.xsd
new file mode 100644
index 00000000000..dd9786c62df
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/service_area_no_bounds.xsd
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
index 40b7d615dff..73673555733 100644
--- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
+++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
@@ -3049,6 +3049,66 @@ tests:
start: skip
end: skip
+ - algorithm: qgis:serviceareafrompoint
+ name: Service area (from point, shortest, no bounds)
+ params:
+ DEFAULT_DIRECTION: 2
+ DEFAULT_SPEED: 5.0
+ INCLUDE_BOUNDS: false
+ INPUT:
+ name: roads.gml
+ type: vector
+ START_POINT: 1002465.0089601517,6221875.432489508
+ STRATEGY: 0
+ TOLERANCE: 0.0
+ TRAVEL_COST: 700.0
+ VALUE_BACKWARD: ''
+ VALUE_BOTH: ''
+ VALUE_FORWARD: ''
+ results:
+ OUTPUT:
+ name: expected/service_area_no_bounds.gml
+ type: vector
+ compare:
+ ignore_crs_check: true
+ geometry:
+ precision: 2
+ fields:
+ cost:
+ precision: 2
+ start: skip
+ end: skip
+
+ - algorithm: qgis:serviceareafrompoint
+ name: Service area from point, output lines
+ params:
+ DEFAULT_DIRECTION: 2
+ DEFAULT_SPEED: 5.0
+ INCLUDE_BOUNDS: false
+ INPUT:
+ name: roads.gml
+ type: vector
+ START_POINT: 1003654.8065293541,6222397.723338357 [EPSG:32733]
+ STRATEGY: 0
+ TOLERANCE: 0.0
+ TRAVEL_COST: 400.0
+ VALUE_BACKWARD: ''
+ VALUE_BOTH: ''
+ VALUE_FORWARD: ''
+ results:
+ OUTPUT_LINES:
+ name: expected/service_area_lines.gml
+ type: vector
+ compare:
+ ignore_crs_check: true
+ geometry:
+ precision: 2
+ fields:
+ cost:
+ precision: 2
+ start: skip
+ end: skip
+
- algorithm: qgis:serviceareafromlayer
name: Service area from layer
params:
@@ -3083,6 +3143,75 @@ tests:
- d
- type
+ - algorithm: qgis:serviceareafromlayer
+ name: Service area from layer no bounds
+ params:
+ DEFAULT_DIRECTION: 2
+ DEFAULT_SPEED: 5.0
+ INCLUDE_BOUNDS: false
+ INPUT:
+ name: roads.gml
+ type: vector
+ START_POINTS:
+ name: custom/route_points.gml
+ type: vector
+ STRATEGY: 0
+ TOLERANCE: 0.0
+ TRAVEL_COST: 700.0
+ VALUE_BACKWARD: ''
+ VALUE_BOTH: ''
+ VALUE_FORWARD: ''
+ results:
+ OUTPUT:
+ name: expected/service_area_from_layer_no_bounds.shp
+ type: vector
+ compare:
+ ignore_crs_check: true
+ geometry:
+ precision: 2
+ fields:
+ cost:
+ precision: 2
+ start: skip
+ end: skip
+ pk:
+ - d
+ - type
+
+ - algorithm: qgis:serviceareafromlayer
+ name: Service area from layer (lines)
+ params:
+ DEFAULT_DIRECTION: 2
+ DEFAULT_SPEED: 5.0
+ INCLUDE_BOUNDS: false
+ INPUT:
+ name: roads.gml
+ type: vector
+ START_POINTS:
+ name: custom/route_points.gml
+ type: vector
+ STRATEGY: 0
+ TOLERANCE: 0.0
+ TRAVEL_COST: 500.0
+ VALUE_BACKWARD: ''
+ VALUE_BOTH: ''
+ VALUE_FORWARD: ''
+ results:
+ OUTPUT_LINES:
+ name: expected/service_area_from_layer_lines.shp
+ type: vector
+ compare:
+ ignore_crs_check: true
+ geometry:
+ precision: 2
+ fields:
+ cost:
+ precision: 2
+ start: skip
+ end: skip
+ pk:
+ - d
+ - type
- algorithm: qgis:createattributeindex
name: Create attribute index
diff --git a/src/analysis/network/qgsgraphanalyzer.h b/src/analysis/network/qgsgraphanalyzer.h
index 273b5c2203d..06a4fb656fb 100644
--- a/src/analysis/network/qgsgraphanalyzer.h
+++ b/src/analysis/network/qgsgraphanalyzer.h
@@ -38,7 +38,8 @@ class ANALYSIS_EXPORT QgsGraphAnalyzer
* \param source source graph
* \param startVertexIdx index of the start vertex
* \param criterionNum index of the optimization strategy
- * \param resultTree array that represents shortest path tree. resultTree[ vertexIndex ] == inboundingArcIndex if vertex reachable, otherwise resultTree[ vertexIndex ] == -1
+ * \param resultTree array that represents shortest path tree. resultTree[ vertexIndex ] == inboundingArcIndex if vertex reachable, otherwise resultTree[ vertexIndex ] == -1.
+ * Note that the startVertexIdx will also have a value of -1 and may need special handling by callers.
* \param resultCost array of the paths costs
*/
static void SIP_PYALTERNATIVETYPE( SIP_PYLIST ) dijkstra( const QgsGraph *source, int startVertexIdx, int criterionNum, QVector *resultTree = nullptr, QVector *resultCost = nullptr );