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 );