Skip to content

Commit b4b3999

Browse files
committed
Port hub lines algorithm to new API
Improvements: - transparent reprojection to match hub/spoke CRS - keep all attributes from matched hub/spoke features - don't break after matching one hub point to spoke - instead join ALL hub/spoke points with matching id values
1 parent e035445 commit b4b3999

File tree

8 files changed

+304
-48
lines changed

8 files changed

+304
-48
lines changed

python/plugins/processing/algs/help/qgis.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ qgis:generatepointspixelcentroidsinsidepolygons:
242242

243243

244244
qgis:hublines:
245+
This algorithm creates hub and spoke diagrams with lines drawn from points on the Spoke Point layer to matching points in the Hub Point layer.
245246

247+
Determination of which hub goes with each point is based on a match between the Hub ID field on the hub points and the Spoke ID field on the spoke points.
246248

247249
qgis:hypsometriccurves: >
248250
This algorithm computes hypsometric curves for an input Digital Elevation Model. Curves are produced as table files in an output folder specified by the user.

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

+67-46
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@
3131
QgsGeometry,
3232
QgsPointXY,
3333
QgsWkbTypes,
34-
QgsApplication,
35-
QgsProcessingUtils)
34+
QgsFeatureRequest,
35+
QgsProcessing,
36+
QgsProcessingParameterFeatureSource,
37+
QgsProcessingParameterField,
38+
QgsProcessingParameterFeatureSink,
39+
QgsProcessingException,
40+
QgsExpression)
3641
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
37-
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
38-
from processing.core.parameters import ParameterVector
39-
from processing.core.parameters import ParameterTableField
40-
from processing.core.outputs import OutputVector
41-
42-
from processing.tools import dataobjects
42+
from processing.tools import vector
4343

4444

4545
class HubLines(QgisAlgorithm):
@@ -55,17 +55,21 @@ def group(self):
5555
def __init__(self):
5656
super().__init__()
5757

58+
def tags(self):
59+
return self.tr('join,points,lines,connect,hub,spoke').split(',')
60+
5861
def initAlgorithm(self, config=None):
59-
self.addParameter(ParameterVector(self.HUBS,
60-
self.tr('Hub layer')))
61-
self.addParameter(ParameterTableField(self.HUB_FIELD,
62-
self.tr('Hub ID field'), self.HUBS))
63-
self.addParameter(ParameterVector(self.SPOKES,
64-
self.tr('Spoke layer')))
65-
self.addParameter(ParameterTableField(self.SPOKE_FIELD,
66-
self.tr('Spoke ID field'), self.SPOKES))
6762

68-
self.addOutput(OutputVector(self.OUTPUT, self.tr('Hub lines'), datatype=[dataobjects.TYPE_VECTOR_LINE]))
63+
self.addParameter(QgsProcessingParameterFeatureSource(self.HUBS,
64+
self.tr('Hub layer')))
65+
self.addParameter(QgsProcessingParameterField(self.HUB_FIELD,
66+
self.tr('Hub ID field'), parentLayerParameterName=self.HUBS))
67+
self.addParameter(QgsProcessingParameterFeatureSource(self.SPOKES,
68+
self.tr('Spoke layer')))
69+
self.addParameter(QgsProcessingParameterField(self.SPOKE_FIELD,
70+
self.tr('Spoke ID field'), parentLayerParameterName=self.SPOKES))
71+
72+
self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Hub lines'), QgsProcessing.TypeVectorLine))
6973

7074
def name(self):
7175
return 'hublines'
@@ -74,44 +78,61 @@ def displayName(self):
7478
return self.tr('Hub lines')
7579

7680
def processAlgorithm(self, parameters, context, feedback):
77-
layerHub = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.HUBS), context)
78-
layerSpoke = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.SPOKES), context)
81+
if parameters[self.SPOKES] == parameters[self.HUBS]:
82+
raise QgsProcessingException(
83+
self.tr('Same layer given for both hubs and spokes'))
7984

80-
fieldHub = self.getParameterValue(self.HUB_FIELD)
81-
fieldSpoke = self.getParameterValue(self.SPOKE_FIELD)
85+
hub_source = self.parameterAsSource(parameters, self.HUBS, context)
86+
spoke_source = self.parameterAsSource(parameters, self.SPOKES, context)
87+
field_hub = self.parameterAsString(parameters, self.HUB_FIELD, context)
88+
field_hub_index = hub_source.fields().lookupField(field_hub)
89+
field_spoke = self.parameterAsString(parameters, self.SPOKE_FIELD, context)
90+
field_spoke_index = hub_source.fields().lookupField(field_spoke)
8291

83-
if layerHub.source() == layerSpoke.source():
84-
raise GeoAlgorithmExecutionException(
85-
self.tr('Same layer given for both hubs and spokes'))
92+
fields = vector.combineFields(hub_source.fields(), spoke_source.fields())
8693

87-
writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(layerSpoke.fields(), QgsWkbTypes.LineString,
88-
layerSpoke.crs(), context)
94+
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
95+
fields, QgsWkbTypes.LineString, hub_source.sourceCrs())
8996

90-
spokes = QgsProcessingUtils.getFeatures(layerSpoke, context)
91-
hubs = QgsProcessingUtils.getFeatures(layerHub, context)
92-
total = 100.0 / layerSpoke.featureCount() if layerSpoke.featureCount() else 0
97+
hubs = hub_source.getFeatures()
98+
total = 100.0 / hub_source.featureCount() if hub_source.featureCount() else 0
9399

94-
for current, spokepoint in enumerate(spokes):
95-
p = spokepoint.geometry().boundingBox().center()
96-
spokeX = p.x()
97-
spokeY = p.y()
98-
spokeId = str(spokepoint[fieldSpoke])
100+
matching_field_types = hub_source.fields().at(field_hub_index).type() == spoke_source.fields().at(field_spoke_index).type()
99101

100-
for hubpoint in hubs:
101-
hubId = str(hubpoint[fieldHub])
102-
if hubId == spokeId:
103-
p = hubpoint.geometry().boundingBox().center()
104-
hubX = p.x()
105-
hubY = p.y()
102+
for current, hub_point in enumerate(hubs):
103+
if feedback.isCanceled():
104+
break
106105

107-
f = QgsFeature()
108-
f.setAttributes(spokepoint.attributes())
109-
f.setGeometry(QgsGeometry.fromPolyline(
110-
[QgsPointXY(spokeX, spokeY), QgsPointXY(hubX, hubY)]))
111-
writer.addFeature(f, QgsFeatureSink.FastInsert)
106+
if not hub_point.hasGeometry():
107+
continue
112108

109+
p = hub_point.geometry().boundingBox().center()
110+
hub_x = p.x()
111+
hub_y = p.y()
112+
hub_id = str(hub_point[field_hub])
113+
hub_attributes = hub_point.attributes()
114+
115+
request = QgsFeatureRequest().setDestinationCrs(hub_source.sourceCrs())
116+
if matching_field_types:
117+
request.setFilterExpression(QgsExpression.createFieldEqualityExpression(field_spoke, hub_attributes[field_hub_index]))
118+
119+
spokes = spoke_source.getFeatures()
120+
for spoke_point in spokes:
121+
if feedback.isCanceled():
113122
break
114123

124+
spoke_id = str(spoke_point[field_spoke])
125+
if hub_id == spoke_id:
126+
p = spoke_point.geometry().boundingBox().center()
127+
spoke_x = p.x()
128+
spoke_y = p.y()
129+
130+
f = QgsFeature()
131+
f.setAttributes(hub_attributes + spoke_point.attributes())
132+
f.setGeometry(QgsGeometry.fromPolyline(
133+
[QgsPointXY(hub_x, hub_y), QgsPointXY(spoke_x, spoke_y)]))
134+
sink.addFeature(f, QgsFeatureSink.FastInsert)
135+
115136
feedback.setProgress(int(current * total))
116137

117-
del writer
138+
return {self.OUTPUT: dest_id}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
from .Hillshade import Hillshade
7878
from .HubDistanceLines import HubDistanceLines
7979
from .HubDistancePoints import HubDistancePoints
80+
from .HubLines import HubLines
8081
from .ImportIntoPostGIS import ImportIntoPostGIS
8182
from .ImportIntoSpatialite import ImportIntoSpatialite
8283
from .Intersection import Intersection
@@ -143,7 +144,6 @@
143144
# from .ExtractByLocation import ExtractByLocation
144145
# from .SelectByLocation import SelectByLocation
145146
# from .SpatialJoin import SpatialJoin
146-
# from .HubLines import HubLines
147147
# from .GeometryConvert import GeometryConvert
148148
# from .StatisticsByCategories import StatisticsByCategories
149149
# from .FieldsCalculator import FieldsCalculator
@@ -188,7 +188,6 @@ def getAlgs(self):
188188
# SelectByLocation(),
189189
# ExtractByLocation(),
190190
# SpatialJoin(),
191-
# HubLines(),
192191
# GeometryConvert(), FieldsCalculator(),
193192
# JoinAttributes(),
194193
# FieldsPyculator(),
@@ -246,6 +245,7 @@ def getAlgs(self):
246245
Hillshade(),
247246
HubDistanceLines(),
248247
HubDistancePoints(),
248+
HubLines(),
249249
ImportIntoPostGIS(),
250250
ImportIntoSpatialite(),
251251
Intersection(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<GMLFeatureClassList>
2+
<GMLFeatureClass>
3+
<Name>spoke_points</Name>
4+
<ElementPath>spoke_points</ElementPath>
5+
<!--POINT-->
6+
<GeometryType>1</GeometryType>
7+
<SRSName>EPSG:4326</SRSName>
8+
<DatasetSpecificInfo>
9+
<FeatureCount>7</FeatureCount>
10+
<ExtentXMin>1.27875</ExtentXMin>
11+
<ExtentXMax>6.82625</ExtentXMax>
12+
<ExtentYMin>-4.16750</ExtentYMin>
13+
<ExtentYMax>3.88250</ExtentYMax>
14+
</DatasetSpecificInfo>
15+
<PropertyDefn>
16+
<Name>id</Name>
17+
<ElementPath>id</ElementPath>
18+
<Type>Integer</Type>
19+
</PropertyDefn>
20+
<PropertyDefn>
21+
<Name>name</Name>
22+
<ElementPath>name</ElementPath>
23+
<Type>String</Type>
24+
<Width>8</Width>
25+
</PropertyDefn>
26+
</GMLFeatureClass>
27+
</GMLFeatureClassList>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://ogr.maptools.org/ spoke_points.xsd"
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy>
8+
<gml:Box>
9+
<gml:coord><gml:X>1.27875</gml:X><gml:Y>-4.1675</gml:Y></gml:coord>
10+
<gml:coord><gml:X>6.826249999999999</gml:X><gml:Y>3.882499999999999</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:spoke_points fid="spoke_points.0">
16+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>5.07625,-2.1725</gml:coordinates></gml:Point></ogr:geometryProperty>
17+
<ogr:id>1</ogr:id>
18+
<ogr:name>point 1</ogr:name>
19+
</ogr:spoke_points>
20+
</gml:featureMember>
21+
<gml:featureMember>
22+
<ogr:spoke_points fid="spoke_points.1">
23+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>5.82,3.8825</gml:coordinates></gml:Point></ogr:geometryProperty>
24+
<ogr:id>2</ogr:id>
25+
<ogr:name>point 2</ogr:name>
26+
</ogr:spoke_points>
27+
</gml:featureMember>
28+
<gml:featureMember>
29+
<ogr:spoke_points fid="spoke_points.2">
30+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>1.62,1.4675</gml:coordinates></gml:Point></ogr:geometryProperty>
31+
<ogr:id>3</ogr:id>
32+
<ogr:name>point 3</ogr:name>
33+
</ogr:spoke_points>
34+
</gml:featureMember>
35+
<gml:featureMember>
36+
<ogr:spoke_points fid="spoke_points.3">
37+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>6.68625,1.23125</gml:coordinates></gml:Point></ogr:geometryProperty>
38+
<ogr:id>4</ogr:id>
39+
<ogr:name>point 4</ogr:name>
40+
</ogr:spoke_points>
41+
</gml:featureMember>
42+
<gml:featureMember>
43+
<ogr:spoke_points fid="spoke_points.4">
44+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>1.27875,-3.66875</gml:coordinates></gml:Point></ogr:geometryProperty>
45+
<ogr:id>4</ogr:id>
46+
<ogr:name>point 4a</ogr:name>
47+
</ogr:spoke_points>
48+
</gml:featureMember>
49+
<gml:featureMember>
50+
<ogr:spoke_points fid="spoke_points.5">
51+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>3.81625,-4.1675</gml:coordinates></gml:Point></ogr:geometryProperty>
52+
<ogr:id>4</ogr:id>
53+
<ogr:name>point 4b</ogr:name>
54+
</ogr:spoke_points>
55+
</gml:featureMember>
56+
<gml:featureMember>
57+
<ogr:spoke_points fid="spoke_points.6">
58+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>6.82625,-2.79375</gml:coordinates></gml:Point></ogr:geometryProperty>
59+
<ogr:id>8</ogr:id>
60+
<ogr:name>point 8</ogr:name>
61+
</ogr:spoke_points>
62+
</gml:featureMember>
63+
</ogr:FeatureCollection>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<GMLFeatureClassList>
2+
<GMLFeatureClass>
3+
<Name>hub_lines</Name>
4+
<ElementPath>hub_lines</ElementPath>
5+
<!--LINESTRING-->
6+
<GeometryType>2</GeometryType>
7+
<SRSName>EPSG:4326</SRSName>
8+
<DatasetSpecificInfo>
9+
<FeatureCount>7</FeatureCount>
10+
<ExtentXMin>1.00000</ExtentXMin>
11+
<ExtentXMax>7.00000</ExtentXMax>
12+
<ExtentYMin>-4.16750</ExtentYMin>
13+
<ExtentYMax>3.88250</ExtentYMax>
14+
</DatasetSpecificInfo>
15+
<PropertyDefn>
16+
<Name>id</Name>
17+
<ElementPath>id</ElementPath>
18+
<Type>Integer</Type>
19+
</PropertyDefn>
20+
<PropertyDefn>
21+
<Name>id2</Name>
22+
<ElementPath>id2</ElementPath>
23+
<Type>Integer</Type>
24+
</PropertyDefn>
25+
<PropertyDefn>
26+
<Name>fid_2</Name>
27+
<ElementPath>fid_2</ElementPath>
28+
<Type>String</Type>
29+
<Width>14</Width>
30+
</PropertyDefn>
31+
<PropertyDefn>
32+
<Name>id_2</Name>
33+
<ElementPath>id_2</ElementPath>
34+
<Type>Integer</Type>
35+
</PropertyDefn>
36+
<PropertyDefn>
37+
<Name>name</Name>
38+
<ElementPath>name</ElementPath>
39+
<Type>String</Type>
40+
<Width>8</Width>
41+
</PropertyDefn>
42+
</GMLFeatureClass>
43+
</GMLFeatureClassList>

0 commit comments

Comments
 (0)