Skip to content

Commit

Permalink
Port points to path to new API
Browse files Browse the repository at this point in the history
Improvements:
- Maintain Z/M values
- Keep original data type for group/order fields
- Group field is optional
- Added unit tests
- Don't export text files for features by default
  • Loading branch information
nyalldawson committed Aug 5, 2017
1 parent b4b3999 commit ec4df6c
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 81 deletions.
2 changes: 2 additions & 0 deletions python/plugins/processing/algs/help/qgis.yaml
Expand Up @@ -370,7 +370,9 @@ qgis:pointslayerfromtable: >
The attributes table of the resulting layer will be the input table.

qgis:pointstopath:
Converts a point layer to a line layer, by joining points in a defined order.

Points can be grouped by a field to output individual line features per group.

qgis:polarplot: >
This algorithm generates a polar plot based on the value of an input vector layer.
Expand Down
187 changes: 108 additions & 79 deletions python/plugins/processing/algs/qgis/PointsToPaths.py
Expand Up @@ -29,57 +29,58 @@
import os
from datetime import datetime

from qgis.PyQt.QtCore import QVariant
from qgis.core import (QgsApplication,
QgsFeature,
from qgis.core import (QgsFeature,
QgsFeatureSink,
QgsFields,
QgsField,
QgsGeometry,
QgsDistanceArea,
QgsProject,
QgsPointXY,
QgsLineString,
QgsWkbTypes,
QgsProcessingUtils)
QgsFeatureRequest,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterField,
QgsProcessingParameterString,
QgsProcessing,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterFolderDestination)

from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
from processing.core.parameters import ParameterVector
from processing.core.parameters import ParameterTableField
from processing.core.parameters import ParameterString
from processing.core.outputs import OutputVector
from processing.core.outputs import OutputDirectory
from processing.tools import dataobjects


class PointsToPaths(QgisAlgorithm):

VECTOR = 'VECTOR'
INPUT = 'INPUT'
GROUP_FIELD = 'GROUP_FIELD'
ORDER_FIELD = 'ORDER_FIELD'
DATE_FORMAT = 'DATE_FORMAT'
#GAP_PERIOD = 'GAP_PERIOD'
OUTPUT_LINES = 'OUTPUT_LINES'
OUTPUT_TEXT = 'OUTPUT_TEXT'
OUTPUT = 'OUTPUT'
OUTPUT_TEXT_DIR = 'OUTPUT_TEXT_DIR'

def group(self):
return self.tr('Vector creation tools')

def __init__(self):
super().__init__()

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

def initAlgorithm(self, config=None):
self.addParameter(ParameterVector(self.VECTOR,
self.tr('Input point layer'), [dataobjects.TYPE_VECTOR_POINT]))
self.addParameter(ParameterTableField(self.GROUP_FIELD,
self.tr('Group field'), self.VECTOR))
self.addParameter(ParameterTableField(self.ORDER_FIELD,
self.tr('Order field'), self.VECTOR))
self.addParameter(ParameterString(self.DATE_FORMAT,
self.tr('Date format (if order field is DateTime)'), '', optional=True))
#self.addParameter(ParameterNumber(
# self.GAP_PERIOD,
# 'Gap period (if order field is DateTime)', 0, 60, 0))
self.addOutput(OutputVector(self.OUTPUT_LINES, self.tr('Paths'), datatype=[dataobjects.TYPE_VECTOR_LINE]))
self.addOutput(OutputDirectory(self.OUTPUT_TEXT, self.tr('Directory')))
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
self.tr('Input point layer'), [QgsProcessing.TypeVectorPoint]))
self.addParameter(QgsProcessingParameterField(self.ORDER_FIELD,
self.tr('Order field'), parentLayerParameterName=self.INPUT))
self.addParameter(QgsProcessingParameterField(self.GROUP_FIELD,
self.tr('Group field'), parentLayerParameterName=self.INPUT, optional=True))
self.addParameter(QgsProcessingParameterString(self.DATE_FORMAT,
self.tr('Date format (if order field is DateTime)'), optional=True))

self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Paths'), QgsProcessing.TypeVectorLine))
output_dir_param = QgsProcessingParameterFolderDestination(self.OUTPUT_TEXT_DIR, self.tr('Directory for text output'), optional=True)
output_dir_param.setCreateByDefault(False)
self.addParameter(output_dir_param)

def name(self):
return 'pointstopath'
Expand All @@ -88,29 +89,58 @@ def displayName(self):
return self.tr('Points to path')

def processAlgorithm(self, parameters, context, feedback):
layer = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.VECTOR), context)
groupField = self.getParameterValue(self.GROUP_FIELD)
orderField = self.getParameterValue(self.ORDER_FIELD)
dateFormat = str(self.getParameterValue(self.DATE_FORMAT))
#gap = int(self.getParameterValue(self.GAP_PERIOD))
dirName = self.getOutputValue(self.OUTPUT_TEXT)
source = self.parameterAsSource(parameters, self.INPUT, context)
group_field_name = self.parameterAsString(parameters, self.GROUP_FIELD, context)
order_field_name = self.parameterAsString(parameters, self.ORDER_FIELD, context)
date_format = self.parameterAsString(parameters, self.DATE_FORMAT, context)
text_dir = self.parameterAsString(parameters, self.OUTPUT_TEXT_DIR, context)

group_field_index = source.fields().lookupField(group_field_name)
order_field_index = source.fields().lookupField(order_field_name)

if group_field_index >= 0:
group_field_def = source.fields().at(group_field_index)
else:
group_field_def = None
order_field_def = source.fields().at(order_field_index)

fields = QgsFields()
fields.append(QgsField('group', QVariant.String, '', 254, 0))
fields.append(QgsField('begin', QVariant.String, '', 254, 0))
fields.append(QgsField('end', QVariant.String, '', 254, 0))
writer = self.getOutputFromName(self.OUTPUT_LINES).getVectorWriter(fields, QgsWkbTypes.LineString, layer.crs(),
context)
if group_field_def is not None:
fields.append(group_field_def)
begin_field = QgsField(order_field_def)
begin_field.setName('begin')
fields.append(begin_field)
end_field = QgsField(order_field_def)
end_field.setName('end')
fields.append(end_field)

output_wkb = QgsWkbTypes.LineString
if QgsWkbTypes.hasM(source.wkbType()):
output_wkb = QgsWkbTypes.addM(output_wkb)
if QgsWkbTypes.hasZ(source.wkbType()):
output_wkb = QgsWkbTypes.addZ(output_wkb)

(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
fields, output_wkb, source.sourceCrs())

points = dict()
features = QgsProcessingUtils.getFeatures(layer, context)
total = 100.0 / layer.featureCount() if layer.featureCount() else 0
features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([group_field_index, order_field_index]))
total = 100.0 / source.featureCount() if source.featureCount() else 0
for current, f in enumerate(features):
point = f.geometry().asPoint()
group = f[groupField]
order = f[orderField]
if dateFormat != '':
order = datetime.strptime(str(order), dateFormat)
if feedback.isCanceled():
break

if not f.hasGeometry():
continue

point = f.geometry().geometry().clone()
if group_field_index >= 0:
group = f.attributes()[group_field_index]
else:
group = 1
order = f.attributes()[order_field_index]
if date_format != '':
order = datetime.strptime(str(order), date_format)
if group in points:
points[group].append((order, point))
else:
Expand All @@ -121,46 +151,45 @@ def processAlgorithm(self, parameters, context, feedback):
feedback.setProgress(0)

da = QgsDistanceArea()
da.setSourceCrs(layer.sourceCrs())
da.setSourceCrs(source.sourceCrs())
da.setEllipsoid(context.project().ellipsoid())

current = 0
total = 100.0 / len(points) if points else 1
for group, vertices in list(points.items()):
if feedback.isCanceled():
break

vertices.sort()
f = QgsFeature()
f.initAttributes(len(fields))
f.setFields(fields)
f['group'] = group
f['begin'] = vertices[0][0]
f['end'] = vertices[-1][0]

fileName = os.path.join(dirName, '%s.txt' % group)

with open(fileName, 'w') as fl:
fl.write('angle=Azimuth\n')
fl.write('heading=Coordinate_System\n')
fl.write('dist_units=Default\n')

line = []
i = 0
for node in vertices:
line.append(node[1])

if i == 0:
fl.write('startAt=%f;%f;90\n' % (node[1].x(), node[1].y()))
fl.write('survey=Polygonal\n')
fl.write('[data]\n')
else:
angle = line[i - 1].azimuth(line[i])
distance = da.measureLine(line[i - 1], line[i])
fl.write('%f;%f;90\n' % (angle, distance))

i += 1

f.setGeometry(QgsGeometry.fromPolyline(line))
writer.addFeature(f, QgsFeatureSink.FastInsert)
attributes = []
if group_field_index >= 0:
attributes.append(group)
attributes.extend([vertices[0][0], vertices[-1][0]])
f.setAttributes(attributes)
line = [node[1] for node in vertices]

if text_dir:
fileName = os.path.join(text_dir, '%s.txt' % group)

with open(fileName, 'w') as fl:
fl.write('angle=Azimuth\n')
fl.write('heading=Coordinate_System\n')
fl.write('dist_units=Default\n')

for i in range(len(line)):
if i == 0:
fl.write('startAt=%f;%f;90\n' % (line[i].x(), line[i].y()))
fl.write('survey=Polygonal\n')
fl.write('[data]\n')
else:
angle = line[i - 1].azimuth(line[i])
distance = da.measureLine(QgsPointXY(line[i - 1]), QgsPointXY(line[i]))
fl.write('%f;%f;90\n' % (angle, distance))

f.setGeometry(QgsGeometry(QgsLineString(line)))
sink.addFeature(f, QgsFeatureSink.FastInsert)
current += 1
feedback.setProgress(int(current * total))

del writer
return {self.OUTPUT: dest_id}
5 changes: 3 additions & 2 deletions python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Expand Up @@ -94,6 +94,7 @@
from .PointsAlongGeometry import PointsAlongGeometry
from .PointsInPolygon import PointsInPolygon
from .PointsLayerFromTable import PointsLayerFromTable
from .PointsToPaths import PointsToPaths
from .PoleOfInaccessibility import PoleOfInaccessibility
from .Polygonize import Polygonize
from .PolygonsToLines import PolygonsToLines
Expand Down Expand Up @@ -152,7 +153,6 @@
# from .PointsDisplacement import PointsDisplacement
# from .PointsFromPolygons import PointsFromPolygons
# from .PointsFromLines import PointsFromLines
# from .PointsToPaths import PointsToPaths
# from .SetVectorStyle import SetVectorStyle
# from .SetRasterStyle import SetRasterStyle
# from .SelectByAttributeSum import SelectByAttributeSum
Expand Down Expand Up @@ -194,7 +194,7 @@ def getAlgs(self):
# StatisticsByCategories(),
# RasterLayerStatistics(), PointsDisplacement(),
# PointsFromPolygons(),
# PointsFromLines(), PointsToPaths(),
# PointsFromLines(),
# SetVectorStyle(), SetRasterStyle(),
# HypsometricCurves(),
# FieldsMapper(), SelectByAttributeSum(), Datasources2Vrt(),
Expand Down Expand Up @@ -262,6 +262,7 @@ def getAlgs(self):
PointsAlongGeometry(),
PointsInPolygon(),
PointsLayerFromTable(),
PointsToPaths(),
PoleOfInaccessibility(),
Polygonize(),
PolygonsToLines(),
Expand Down
@@ -0,0 +1,26 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>points_to_path</Name>
<ElementPath>points_to_path</ElementPath>
<!--LINESTRING-->
<GeometryType>2</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>1</FeatureCount>
<ExtentXMin>0.00000</ExtentXMin>
<ExtentXMax>8.00000</ExtentXMax>
<ExtentYMin>-5.00000</ExtentYMin>
<ExtentYMax>3.00000</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>begin</Name>
<ElementPath>begin</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>end</Name>
<ElementPath>end</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=""
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:boundedBy>
<gml:Box>
<gml:coord><gml:X>0</gml:X><gml:Y>-5</gml:Y></gml:coord>
<gml:coord><gml:X>8</gml:X><gml:Y>3</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:points_to_path fid="points_to_path.0">
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>1,1 3,3 2,2 5,2 4,1 0,-5 8,-1 7,-1 0,-1</gml:coordinates></gml:LineString></ogr:geometryProperty>
<ogr:begin>1</ogr:begin>
<ogr:end>9</ogr:end>
</ogr:points_to_path>
</gml:featureMember>
</ogr:FeatureCollection>
@@ -0,0 +1,31 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>points_to_path_grouped</Name>
<ElementPath>points_to_path_grouped</ElementPath>
<!--LINESTRING-->
<GeometryType>2</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>3</FeatureCount>
<ExtentXMin>0.00000</ExtentXMin>
<ExtentXMax>8.00000</ExtentXMax>
<ExtentYMin>-5.00000</ExtentYMin>
<ExtentYMax>3.00000</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>id2</Name>
<ElementPath>id2</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>begin</Name>
<ElementPath>begin</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>end</Name>
<ElementPath>end</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>

0 comments on commit ec4df6c

Please sign in to comment.