Navigation Menu

Skip to content

Commit

Permalink
Port hub lines algorithm to new API
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nyalldawson committed Aug 5, 2017
1 parent e035445 commit b4b3999
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 48 deletions.
2 changes: 2 additions & 0 deletions python/plugins/processing/algs/help/qgis.yaml
Expand Up @@ -242,7 +242,9 @@ qgis:generatepointspixelcentroidsinsidepolygons:


qgis:hublines:
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.

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.

qgis:hypsometriccurves: >
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.
Expand Down
113 changes: 67 additions & 46 deletions python/plugins/processing/algs/qgis/HubLines.py
Expand Up @@ -31,15 +31,15 @@
QgsGeometry,
QgsPointXY,
QgsWkbTypes,
QgsApplication,
QgsProcessingUtils)
QgsFeatureRequest,
QgsProcessing,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterField,
QgsProcessingParameterFeatureSink,
QgsProcessingException,
QgsExpression)
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
from processing.core.parameters import ParameterVector
from processing.core.parameters import ParameterTableField
from processing.core.outputs import OutputVector

from processing.tools import dataobjects
from processing.tools import vector


class HubLines(QgisAlgorithm):
Expand All @@ -55,17 +55,21 @@ def group(self):
def __init__(self):
super().__init__()

def tags(self):
return self.tr('join,points,lines,connect,hub,spoke').split(',')

def initAlgorithm(self, config=None):
self.addParameter(ParameterVector(self.HUBS,
self.tr('Hub layer')))
self.addParameter(ParameterTableField(self.HUB_FIELD,
self.tr('Hub ID field'), self.HUBS))
self.addParameter(ParameterVector(self.SPOKES,
self.tr('Spoke layer')))
self.addParameter(ParameterTableField(self.SPOKE_FIELD,
self.tr('Spoke ID field'), self.SPOKES))

self.addOutput(OutputVector(self.OUTPUT, self.tr('Hub lines'), datatype=[dataobjects.TYPE_VECTOR_LINE]))
self.addParameter(QgsProcessingParameterFeatureSource(self.HUBS,
self.tr('Hub layer')))
self.addParameter(QgsProcessingParameterField(self.HUB_FIELD,
self.tr('Hub ID field'), parentLayerParameterName=self.HUBS))
self.addParameter(QgsProcessingParameterFeatureSource(self.SPOKES,
self.tr('Spoke layer')))
self.addParameter(QgsProcessingParameterField(self.SPOKE_FIELD,
self.tr('Spoke ID field'), parentLayerParameterName=self.SPOKES))

self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Hub lines'), QgsProcessing.TypeVectorLine))

def name(self):
return 'hublines'
Expand All @@ -74,44 +78,61 @@ def displayName(self):
return self.tr('Hub lines')

def processAlgorithm(self, parameters, context, feedback):
layerHub = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.HUBS), context)
layerSpoke = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.SPOKES), context)
if parameters[self.SPOKES] == parameters[self.HUBS]:
raise QgsProcessingException(
self.tr('Same layer given for both hubs and spokes'))

fieldHub = self.getParameterValue(self.HUB_FIELD)
fieldSpoke = self.getParameterValue(self.SPOKE_FIELD)
hub_source = self.parameterAsSource(parameters, self.HUBS, context)
spoke_source = self.parameterAsSource(parameters, self.SPOKES, context)
field_hub = self.parameterAsString(parameters, self.HUB_FIELD, context)
field_hub_index = hub_source.fields().lookupField(field_hub)
field_spoke = self.parameterAsString(parameters, self.SPOKE_FIELD, context)
field_spoke_index = hub_source.fields().lookupField(field_spoke)

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

writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(layerSpoke.fields(), QgsWkbTypes.LineString,
layerSpoke.crs(), context)
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
fields, QgsWkbTypes.LineString, hub_source.sourceCrs())

spokes = QgsProcessingUtils.getFeatures(layerSpoke, context)
hubs = QgsProcessingUtils.getFeatures(layerHub, context)
total = 100.0 / layerSpoke.featureCount() if layerSpoke.featureCount() else 0
hubs = hub_source.getFeatures()
total = 100.0 / hub_source.featureCount() if hub_source.featureCount() else 0

for current, spokepoint in enumerate(spokes):
p = spokepoint.geometry().boundingBox().center()
spokeX = p.x()
spokeY = p.y()
spokeId = str(spokepoint[fieldSpoke])
matching_field_types = hub_source.fields().at(field_hub_index).type() == spoke_source.fields().at(field_spoke_index).type()

for hubpoint in hubs:
hubId = str(hubpoint[fieldHub])
if hubId == spokeId:
p = hubpoint.geometry().boundingBox().center()
hubX = p.x()
hubY = p.y()
for current, hub_point in enumerate(hubs):
if feedback.isCanceled():
break

f = QgsFeature()
f.setAttributes(spokepoint.attributes())
f.setGeometry(QgsGeometry.fromPolyline(
[QgsPointXY(spokeX, spokeY), QgsPointXY(hubX, hubY)]))
writer.addFeature(f, QgsFeatureSink.FastInsert)
if not hub_point.hasGeometry():
continue

p = hub_point.geometry().boundingBox().center()
hub_x = p.x()
hub_y = p.y()
hub_id = str(hub_point[field_hub])
hub_attributes = hub_point.attributes()

request = QgsFeatureRequest().setDestinationCrs(hub_source.sourceCrs())
if matching_field_types:
request.setFilterExpression(QgsExpression.createFieldEqualityExpression(field_spoke, hub_attributes[field_hub_index]))

spokes = spoke_source.getFeatures()
for spoke_point in spokes:
if feedback.isCanceled():
break

spoke_id = str(spoke_point[field_spoke])
if hub_id == spoke_id:
p = spoke_point.geometry().boundingBox().center()
spoke_x = p.x()
spoke_y = p.y()

f = QgsFeature()
f.setAttributes(hub_attributes + spoke_point.attributes())
f.setGeometry(QgsGeometry.fromPolyline(
[QgsPointXY(hub_x, hub_y), QgsPointXY(spoke_x, spoke_y)]))
sink.addFeature(f, QgsFeatureSink.FastInsert)

feedback.setProgress(int(current * total))

del writer
return {self.OUTPUT: dest_id}
4 changes: 2 additions & 2 deletions python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Expand Up @@ -77,6 +77,7 @@
from .Hillshade import Hillshade
from .HubDistanceLines import HubDistanceLines
from .HubDistancePoints import HubDistancePoints
from .HubLines import HubLines
from .ImportIntoPostGIS import ImportIntoPostGIS
from .ImportIntoSpatialite import ImportIntoSpatialite
from .Intersection import Intersection
Expand Down Expand Up @@ -143,7 +144,6 @@
# from .ExtractByLocation import ExtractByLocation
# from .SelectByLocation import SelectByLocation
# from .SpatialJoin import SpatialJoin
# from .HubLines import HubLines
# from .GeometryConvert import GeometryConvert
# from .StatisticsByCategories import StatisticsByCategories
# from .FieldsCalculator import FieldsCalculator
Expand Down Expand Up @@ -188,7 +188,6 @@ def getAlgs(self):
# SelectByLocation(),
# ExtractByLocation(),
# SpatialJoin(),
# HubLines(),
# GeometryConvert(), FieldsCalculator(),
# JoinAttributes(),
# FieldsPyculator(),
Expand Down Expand Up @@ -246,6 +245,7 @@ def getAlgs(self):
Hillshade(),
HubDistanceLines(),
HubDistancePoints(),
HubLines(),
ImportIntoPostGIS(),
ImportIntoSpatialite(),
Intersection(),
Expand Down
27 changes: 27 additions & 0 deletions python/plugins/processing/tests/testdata/custom/spoke_points.gfs
@@ -0,0 +1,27 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>spoke_points</Name>
<ElementPath>spoke_points</ElementPath>
<!--POINT-->
<GeometryType>1</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>7</FeatureCount>
<ExtentXMin>1.27875</ExtentXMin>
<ExtentXMax>6.82625</ExtentXMax>
<ExtentYMin>-4.16750</ExtentYMin>
<ExtentYMax>3.88250</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>id</Name>
<ElementPath>id</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>name</Name>
<ElementPath>name</ElementPath>
<Type>String</Type>
<Width>8</Width>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>
63 changes: 63 additions & 0 deletions python/plugins/processing/tests/testdata/custom/spoke_points.gml
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ogr.maptools.org/ spoke_points.xsd"
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:boundedBy>
<gml:Box>
<gml:coord><gml:X>1.27875</gml:X><gml:Y>-4.1675</gml:Y></gml:coord>
<gml:coord><gml:X>6.826249999999999</gml:X><gml:Y>3.882499999999999</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:spoke_points fid="spoke_points.0">
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>5.07625,-2.1725</gml:coordinates></gml:Point></ogr:geometryProperty>
<ogr:id>1</ogr:id>
<ogr:name>point 1</ogr:name>
</ogr:spoke_points>
</gml:featureMember>
<gml:featureMember>
<ogr:spoke_points fid="spoke_points.1">
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>5.82,3.8825</gml:coordinates></gml:Point></ogr:geometryProperty>
<ogr:id>2</ogr:id>
<ogr:name>point 2</ogr:name>
</ogr:spoke_points>
</gml:featureMember>
<gml:featureMember>
<ogr:spoke_points fid="spoke_points.2">
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>1.62,1.4675</gml:coordinates></gml:Point></ogr:geometryProperty>
<ogr:id>3</ogr:id>
<ogr:name>point 3</ogr:name>
</ogr:spoke_points>
</gml:featureMember>
<gml:featureMember>
<ogr:spoke_points fid="spoke_points.3">
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>6.68625,1.23125</gml:coordinates></gml:Point></ogr:geometryProperty>
<ogr:id>4</ogr:id>
<ogr:name>point 4</ogr:name>
</ogr:spoke_points>
</gml:featureMember>
<gml:featureMember>
<ogr:spoke_points fid="spoke_points.4">
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>1.27875,-3.66875</gml:coordinates></gml:Point></ogr:geometryProperty>
<ogr:id>4</ogr:id>
<ogr:name>point 4a</ogr:name>
</ogr:spoke_points>
</gml:featureMember>
<gml:featureMember>
<ogr:spoke_points fid="spoke_points.5">
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>3.81625,-4.1675</gml:coordinates></gml:Point></ogr:geometryProperty>
<ogr:id>4</ogr:id>
<ogr:name>point 4b</ogr:name>
</ogr:spoke_points>
</gml:featureMember>
<gml:featureMember>
<ogr:spoke_points fid="spoke_points.6">
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>6.82625,-2.79375</gml:coordinates></gml:Point></ogr:geometryProperty>
<ogr:id>8</ogr:id>
<ogr:name>point 8</ogr:name>
</ogr:spoke_points>
</gml:featureMember>
</ogr:FeatureCollection>
43 changes: 43 additions & 0 deletions python/plugins/processing/tests/testdata/expected/hub_lines.gfs
@@ -0,0 +1,43 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>hub_lines</Name>
<ElementPath>hub_lines</ElementPath>
<!--LINESTRING-->
<GeometryType>2</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>7</FeatureCount>
<ExtentXMin>1.00000</ExtentXMin>
<ExtentXMax>7.00000</ExtentXMax>
<ExtentYMin>-4.16750</ExtentYMin>
<ExtentYMax>3.88250</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>id</Name>
<ElementPath>id</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>id2</Name>
<ElementPath>id2</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>fid_2</Name>
<ElementPath>fid_2</ElementPath>
<Type>String</Type>
<Width>14</Width>
</PropertyDefn>
<PropertyDefn>
<Name>id_2</Name>
<ElementPath>id_2</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>name</Name>
<ElementPath>name</ElementPath>
<Type>String</Type>
<Width>8</Width>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>

0 comments on commit b4b3999

Please sign in to comment.