1# -*- coding: utf-8 -*-
2
3"""
4***************************************************************************
5    PointsToPaths.py
6    ---------------------
7    Date                 : April 2014
8    Copyright            : (C) 2014 by Alexander Bruy
9    Email                : alexander dot bruy at gmail dot com
10***************************************************************************
11*                                                                         *
12*   This program is free software; you can redistribute it and/or modify  *
13*   it under the terms of the GNU General Public License as published by  *
14*   the Free Software Foundation; either version 2 of the License, or     *
15*   (at your option) any later version.                                   *
16*                                                                         *
17***************************************************************************
18"""
19
20__author__ = 'Alexander Bruy'
21__date__ = 'April 2014'
22__copyright__ = '(C) 2014, Alexander Bruy'
23
24import os
25from datetime import datetime
26
27from qgis.core import (QgsFeature,
28                       QgsFeatureSink,
29                       QgsFields,
30                       QgsField,
31                       QgsGeometry,
32                       QgsDistanceArea,
33                       QgsPointXY,
34                       QgsLineString,
35                       QgsWkbTypes,
36                       QgsFeatureRequest,
37                       QgsProcessingException,
38                       QgsProcessingParameterBoolean,
39                       QgsProcessingParameterFeatureSource,
40                       QgsProcessingParameterField,
41                       QgsProcessingParameterString,
42                       QgsProcessingFeatureSource,
43                       QgsProcessing,
44                       QgsProcessingParameterFeatureSink,
45                       QgsProcessingParameterFolderDestination)
46
47from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
48
49
50class PointsToPaths(QgisAlgorithm):
51    INPUT = 'INPUT'
52    CLOSE_PATH = 'CLOSE_PATH'
53    GROUP_FIELD = 'GROUP_FIELD'
54    ORDER_FIELD = 'ORDER_FIELD'
55    DATE_FORMAT = 'DATE_FORMAT'
56    OUTPUT = 'OUTPUT'
57    OUTPUT_TEXT_DIR = 'OUTPUT_TEXT_DIR'
58
59    def group(self):
60        return self.tr('Vector creation')
61
62    def groupId(self):
63        return 'vectorcreation'
64
65    def __init__(self):
66        super().__init__()
67
68    def tags(self):
69        return self.tr('join,points,lines,connect').split(',')
70
71    def initAlgorithm(self, config=None):
72        self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
73                                                              self.tr('Input point layer'), [QgsProcessing.TypeVectorPoint]))
74        self.addParameter(QgsProcessingParameterBoolean(self.CLOSE_PATH,
75                                                        self.tr('Close path'), defaultValue=False))
76        self.addParameter(QgsProcessingParameterField(self.ORDER_FIELD,
77                                                      self.tr('Order field'), parentLayerParameterName=self.INPUT))
78        self.addParameter(QgsProcessingParameterField(self.GROUP_FIELD,
79                                                      self.tr('Group field'), parentLayerParameterName=self.INPUT, optional=True))
80        self.addParameter(QgsProcessingParameterString(self.DATE_FORMAT,
81                                                       self.tr('Date format (if order field is DateTime)'), optional=True))
82
83        self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Paths'), QgsProcessing.TypeVectorLine))
84        output_dir_param = QgsProcessingParameterFolderDestination(self.OUTPUT_TEXT_DIR, self.tr('Directory for text output'), optional=True)
85        output_dir_param.setCreateByDefault(False)
86        self.addParameter(output_dir_param)
87
88    def name(self):
89        return 'pointstopath'
90
91    def displayName(self):
92        return self.tr('Points to path')
93
94    def processAlgorithm(self, parameters, context, feedback):
95        source = self.parameterAsSource(parameters, self.INPUT, context)
96        if source is None:
97            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))
98
99        close_path = self.parameterAsBool(parameters, self.CLOSE_PATH, context)
100        group_field_name = self.parameterAsString(parameters, self.GROUP_FIELD, context)
101        order_field_name = self.parameterAsString(parameters, self.ORDER_FIELD, context)
102        date_format = self.parameterAsString(parameters, self.DATE_FORMAT, context)
103        text_dir = self.parameterAsString(parameters, self.OUTPUT_TEXT_DIR, context)
104
105        group_field_index = source.fields().lookupField(group_field_name)
106        order_field_index = source.fields().lookupField(order_field_name)
107
108        if group_field_index >= 0:
109            group_field_def = source.fields().at(group_field_index)
110        else:
111            group_field_def = None
112        order_field_def = source.fields().at(order_field_index)
113
114        fields = QgsFields()
115        if group_field_def is not None:
116            fields.append(group_field_def)
117        begin_field = QgsField(order_field_def)
118        begin_field.setName('begin')
119        fields.append(begin_field)
120        end_field = QgsField(order_field_def)
121        end_field.setName('end')
122        fields.append(end_field)
123
124        output_wkb = QgsWkbTypes.LineString
125        if QgsWkbTypes.hasM(source.wkbType()):
126            output_wkb = QgsWkbTypes.addM(output_wkb)
127        if QgsWkbTypes.hasZ(source.wkbType()):
128            output_wkb = QgsWkbTypes.addZ(output_wkb)
129
130        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
131                                               fields, output_wkb, source.sourceCrs())
132        if sink is None:
133            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
134
135        if text_dir and not(os.path.exists(text_dir)):
136            raise QgsProcessingException(self.tr("The text output directory does not exist"))
137
138        points = dict()
139        features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([group_field_index, order_field_index]), QgsProcessingFeatureSource.FlagSkipGeometryValidityChecks)
140        total = 100.0 / source.featureCount() if source.featureCount() else 0
141        for current, f in enumerate(features):
142            if feedback.isCanceled():
143                break
144
145            if not f.hasGeometry():
146                continue
147
148            point = f.geometry().constGet().clone()
149            if group_field_index >= 0:
150                group = f[group_field_index]
151            else:
152                group = 1
153            order = f[order_field_index]
154            if date_format != '':
155                order = datetime.strptime(str(order), date_format)
156            if group in points:
157                points[group].append((order, point))
158            else:
159                points[group] = [(order, point)]
160
161            feedback.setProgress(int(current * total))
162
163        feedback.setProgress(0)
164
165        da = QgsDistanceArea()
166        da.setSourceCrs(source.sourceCrs(), context.transformContext())
167        da.setEllipsoid(context.ellipsoid())
168
169        current = 0
170        total = 100.0 / len(points) if points else 1
171        for group, vertices in points.items():
172            if feedback.isCanceled():
173                break
174
175            vertices.sort(key=lambda x: (x[0] is None, x[0]))
176            f = QgsFeature()
177            attributes = []
178            if group_field_index >= 0:
179                attributes.append(group)
180            attributes.extend([vertices[0][0], vertices[-1][0]])
181            f.setAttributes(attributes)
182            line = [node[1] for node in vertices]
183
184            if close_path is True:
185                if line[0] != line[-1]:
186                    line.append(line[0])
187
188            if text_dir:
189                fileName = os.path.join(text_dir, '%s.txt' % group)
190
191                with open(fileName, 'w') as fl:
192                    fl.write('angle=Azimuth\n')
193                    fl.write('heading=Coordinate_System\n')
194                    fl.write('dist_units=Default\n')
195
196                    for i in range(len(line)):
197                        if i == 0:
198                            fl.write('startAt=%f;%f;90\n' % (line[i].x(), line[i].y()))
199                            fl.write('survey=Polygonal\n')
200                            fl.write('[data]\n')
201                        else:
202                            angle = line[i - 1].azimuth(line[i])
203                            distance = da.measureLine(QgsPointXY(line[i - 1]), QgsPointXY(line[i]))
204                            fl.write('%f;%f;90\n' % (angle, distance))
205
206            f.setGeometry(QgsGeometry(QgsLineString(line)))
207            sink.addFeature(f, QgsFeatureSink.FastInsert)
208            current += 1
209            feedback.setProgress(int(current * total))
210
211        return {self.OUTPUT: dest_id}
212