1# -*- coding: utf-8 -*-
2
3"""
4***************************************************************************
5    InterpolationDataWidget.py
6    ---------------------
7    Date                 : December 2016
8    Copyright            : (C) 2016 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__ = 'December 2016'
22__copyright__ = '(C) 2016, Alexander Bruy'
23
24import os
25
26from qgis.PyQt import uic
27from qgis.PyQt.QtCore import pyqtSignal
28from qgis.PyQt.QtWidgets import (QTreeWidgetItem,
29                                 QComboBox)
30from qgis.core import (QgsApplication,
31                       QgsMapLayerProxyModel,
32                       QgsWkbTypes,
33                       QgsRectangle,
34                       QgsReferencedRectangle,
35                       QgsCoordinateReferenceSystem,
36                       QgsProcessingUtils,
37                       QgsProcessingParameterNumber,
38                       QgsProcessingParameterDefinition,
39                       QgsFieldProxyModel)
40from qgis.gui import QgsDoubleSpinBox
41from qgis.analysis import QgsInterpolator
42
43from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD
44from processing.tools import dataobjects
45
46pluginPath = os.path.dirname(__file__)
47
48
49class ParameterInterpolationData(QgsProcessingParameterDefinition):
50
51    def __init__(self, name='', description=''):
52        super().__init__(name, description)
53        self.setMetadata({
54            'widget_wrapper': 'processing.algs.qgis.ui.InterpolationWidgets.InterpolationDataWidgetWrapper'
55        })
56
57    def type(self):
58        return 'idw_interpolation_data'
59
60    def clone(self):
61        return ParameterInterpolationData(self.name(), self.description())
62
63    @staticmethod
64    def parseValue(value):
65        if value is None:
66            return None
67
68        if value == '':
69            return None
70
71        if isinstance(value, str):
72            return value if value != '' else None
73        else:
74            return ParameterInterpolationData.dataToString(value)
75
76    @staticmethod
77    def dataToString(data):
78        s = ''
79        for c in data:
80            s += '{}::~::{}::~::{:d}::~::{:d};'.format(c[0],
81                                                       c[1],
82                                                       c[2],
83                                                       c[3])
84        return s[:-1]
85
86
87WIDGET, BASE = uic.loadUiType(os.path.join(pluginPath, 'interpolationdatawidgetbase.ui'))
88
89
90class InterpolationDataWidget(BASE, WIDGET):
91    hasChanged = pyqtSignal()
92
93    def __init__(self):
94        super(InterpolationDataWidget, self).__init__(None)
95        self.setupUi(self)
96
97        self.btnAdd.setIcon(QgsApplication.getThemeIcon('/symbologyAdd.svg'))
98        self.btnRemove.setIcon(QgsApplication.getThemeIcon('/symbologyRemove.svg'))
99
100        self.btnAdd.clicked.connect(self.addLayer)
101        self.btnRemove.clicked.connect(self.removeLayer)
102
103        self.cmbLayers.layerChanged.connect(self.layerChanged)
104        self.cmbLayers.setFilters(QgsMapLayerProxyModel.VectorLayer)
105        self.cmbFields.setFilters(QgsFieldProxyModel.Numeric)
106        self.cmbFields.setLayer(self.cmbLayers.currentLayer())
107
108    def addLayer(self):
109        layer = self.cmbLayers.currentLayer()
110
111        attribute = ''
112        if self.chkUseZCoordinate.isChecked():
113            attribute = 'Z_COORD'
114        else:
115            attribute = self.cmbFields.currentField()
116
117        self._addLayerData(layer.name(), attribute)
118        self.hasChanged.emit()
119
120    def removeLayer(self):
121        item = self.layersTree.currentItem()
122        if not item:
123            return
124        self.layersTree.invisibleRootItem().removeChild(item)
125        self.hasChanged.emit()
126
127    def layerChanged(self, layer):
128        self.chkUseZCoordinate.setEnabled(False)
129        self.chkUseZCoordinate.setChecked(False)
130
131        if layer is None or not layer.isValid():
132            return
133
134        provider = layer.dataProvider()
135        if not provider:
136            return
137
138        if QgsWkbTypes.hasZ(provider.wkbType()):
139            self.chkUseZCoordinate.setEnabled(True)
140
141        self.cmbFields.setLayer(layer)
142
143    def _addLayerData(self, layerName, attribute):
144        item = QTreeWidgetItem()
145        item.setText(0, layerName)
146        item.setText(1, attribute)
147        self.layersTree.addTopLevelItem(item)
148
149        comboBox = QComboBox()
150        comboBox.addItem(self.tr('Points'))
151        comboBox.addItem(self.tr('Structure lines'))
152        comboBox.addItem(self.tr('Break lines'))
153        comboBox.setCurrentIndex(0)
154        self.layersTree.setItemWidget(item, 2, comboBox)
155
156    def setValue(self, value):
157        self.layersTree.clear()
158        rows = value.split(';')
159        for i, r in enumerate(rows):
160            v = r.split('::~::')
161            self.addLayerData(v[0], v[1])
162
163            comboBox = self.layersTree.itemWidget(self.layersTree.topLevelItem(i), 2)
164            comboBox.setCurrentIndex(comboBox.findText(v[3]))
165        self.hasChanged.emit()
166
167    def value(self):
168        layers = ''
169        context = dataobjects.createContext()
170        for i in range(self.layersTree.topLevelItemCount()):
171            item = self.layersTree.topLevelItem(i)
172            if item:
173                layerName = item.text(0)
174                layer = QgsProcessingUtils.mapLayerFromString(layerName, context)
175                if not layer:
176                    continue
177
178                provider = layer.dataProvider()
179                if not provider:
180                    continue
181
182                interpolationAttribute = item.text(1)
183                interpolationSource = QgsInterpolator.ValueAttribute
184                if interpolationAttribute == 'Z_COORD':
185                    interpolationSource = QgsInterpolator.ValueZ
186                    fieldIndex = -1
187                else:
188                    fieldIndex = layer.fields().indexFromName(interpolationAttribute)
189
190                comboBox = self.layersTree.itemWidget(self.layersTree.topLevelItem(i), 2)
191                inputTypeName = comboBox.currentText()
192                if inputTypeName == self.tr('Points'):
193                    inputType = QgsInterpolator.SourcePoints
194                elif inputTypeName == self.tr('Structure lines'):
195                    inputType = QgsInterpolator.SourceStructureLines
196                else:
197                    inputType = QgsInterpolator.SourceBreakLines
198
199                layers += '{}::~::{:d}::~::{:d}::~::{:d}::|::'.format(layer.source(),
200                                                                      interpolationSource,
201                                                                      fieldIndex,
202                                                                      inputType)
203        return layers[:-len('::|::')]
204
205
206class InterpolationDataWidgetWrapper(WidgetWrapper):
207
208    def createWidget(self):
209        widget = InterpolationDataWidget()
210        widget.hasChanged.connect(lambda: self.widgetValueHasChanged.emit(self))
211        return widget
212
213    def setValue(self, value):
214        self.widget.setValue(value)
215
216    def value(self):
217        return self.widget.value()
218
219
220class ParameterPixelSize(QgsProcessingParameterNumber):
221
222    def __init__(self, name='', description='', layersData=None, extent=None, minValue=None, default=None, optional=False):
223        QgsProcessingParameterNumber.__init__(self, name, description, QgsProcessingParameterNumber.Double, default, optional, minValue)
224        self.setMetadata({
225            'widget_wrapper': 'processing.algs.qgis.ui.InterpolationWidgets.PixelSizeWidgetWrapper'
226        })
227
228        self.layersData = layersData
229        self.extent = extent
230        self.layers = []
231
232    def clone(self):
233        copy = ParameterPixelSize(self.name(), self.description(), self.layersData, self.extent, self.minimum(), self.defaultValue(), self.flags() & QgsProcessingParameterDefinition.FlagOptional)
234        return copy
235
236
237WIDGET, BASE = uic.loadUiType(os.path.join(pluginPath, 'RasterResolutionWidget.ui'))
238
239
240class PixelSizeWidget(BASE, WIDGET):
241
242    def __init__(self):
243        super(PixelSizeWidget, self).__init__(None)
244        self.setupUi(self)
245        self.context = dataobjects.createContext()
246
247        self.extent = QgsRectangle()
248        self.layers = []
249
250        self.mCellXSpinBox.setShowClearButton(False)
251        self.mCellYSpinBox.setShowClearButton(False)
252        self.mRowsSpinBox.setShowClearButton(False)
253        self.mColumnsSpinBox.setShowClearButton(False)
254
255        self.mCellYSpinBox.valueChanged.connect(self.mCellXSpinBox.setValue)
256        self.mCellXSpinBox.valueChanged.connect(self.pixelSizeChanged)
257        self.mRowsSpinBox.valueChanged.connect(self.rowsChanged)
258        self.mColumnsSpinBox.valueChanged.connect(self.columnsChanged)
259
260    def setLayers(self, layersData):
261        self.extent = QgsRectangle()
262        self.layers = []
263        for row in layersData.split(';'):
264            v = row.split('::~::')
265            # need to keep a reference until interpolation is complete
266            layer = QgsProcessingUtils.variantToSource(v[0], self.context)
267            if layer:
268                self.layers.append(layer)
269                bbox = layer.sourceExtent()
270                if self.extent.isEmpty():
271                    self.extent = bbox
272                else:
273                    self.extent.combineExtentWith(bbox)
274
275        self.pixelSizeChanged()
276
277    def setExtent(self, extent):
278        if extent is not None:
279            tokens = extent.split(' ')[0].split(',')
280            ext = QgsRectangle(float(tokens[0]), float(tokens[2]), float(tokens[1]), float(tokens[3]))
281            if len(tokens) > 1:
282                self.extent = QgsReferencedRectangle(ext, QgsCoordinateReferenceSystem(tokens[1][1:-1]))
283            else:
284                self.extent = ext
285        self.pixelSizeChanged()
286
287    def pixelSizeChanged(self):
288        cell_size = self.mCellXSpinBox.value()
289        if cell_size <= 0:
290            return
291
292        self.mCellYSpinBox.blockSignals(True)
293        self.mCellYSpinBox.setValue(cell_size)
294        self.mCellYSpinBox.blockSignals(False)
295        rows = max(round(self.extent.height() / cell_size) + 1, 1)
296        cols = max(round(self.extent.width() / cell_size) + 1, 1)
297        self.mRowsSpinBox.blockSignals(True)
298        self.mRowsSpinBox.setValue(rows)
299        self.mRowsSpinBox.blockSignals(False)
300        self.mColumnsSpinBox.blockSignals(True)
301        self.mColumnsSpinBox.setValue(cols)
302        self.mColumnsSpinBox.blockSignals(False)
303
304    def rowsChanged(self):
305        rows = self.mRowsSpinBox.value()
306        if rows <= 0:
307            return
308        cell_size = self.extent.height() / rows
309        cols = max(round(self.extent.width() / cell_size) + 1, 1)
310        self.mColumnsSpinBox.blockSignals(True)
311        self.mColumnsSpinBox.setValue(cols)
312        self.mColumnsSpinBox.blockSignals(False)
313        for w in [self.mCellXSpinBox, self.mCellYSpinBox]:
314            w.blockSignals(True)
315            w.setValue(cell_size)
316            w.blockSignals(False)
317
318    def columnsChanged(self):
319        cols = self.mColumnsSpinBox.value()
320        if cols < 2:
321            return
322        cell_size = self.extent.width() / (cols - 1)
323        rows = max(round(self.extent.height() / cell_size), 1)
324        self.mRowsSpinBox.blockSignals(True)
325        self.mRowsSpinBox.setValue(rows)
326        self.mRowsSpinBox.blockSignals(False)
327        for w in [self.mCellXSpinBox, self.mCellYSpinBox]:
328            w.blockSignals(True)
329            w.setValue(cell_size)
330            w.blockSignals(False)
331
332    def setValue(self, value):
333        try:
334            numeric_value = float(value)
335        except:
336            return False
337
338        self.mCellXSpinBox.setValue(numeric_value)
339        self.mCellYSpinBox.setValue(numeric_value)
340        return True
341
342    def value(self):
343        return self.mCellXSpinBox.value()
344
345
346class PixelSizeWidgetWrapper(WidgetWrapper):
347
348    def __init__(self, param, dialog, row=0, col=0, **kwargs):
349        super().__init__(param, dialog, row, col, **kwargs)
350        self.context = dataobjects.createContext()
351
352    def _panel(self):
353        return PixelSizeWidget()
354
355    def createWidget(self):
356        if self.dialogType == DIALOG_STANDARD:
357            return self._panel()
358        else:
359            w = QgsDoubleSpinBox()
360            w.setShowClearButton(False)
361            w.setMinimum(0)
362            w.setMaximum(99999999999)
363            w.setDecimals(6)
364            w.setToolTip(self.tr('Resolution of each pixel in output raster, in layer units'))
365            return w
366
367    def postInitialize(self, wrappers):
368        if self.dialogType != DIALOG_STANDARD:
369            return
370
371        for wrapper in wrappers:
372            if wrapper.parameterDefinition().name() == self.param.layersData:
373                self.setLayers(wrapper.parameterValue())
374                wrapper.widgetValueHasChanged.connect(self.layersChanged)
375            elif wrapper.parameterDefinition().name() == self.param.extent:
376                self.setExtent(wrapper.parameterValue())
377                wrapper.widgetValueHasChanged.connect(self.extentChanged)
378
379    def layersChanged(self, wrapper):
380        self.setLayers(wrapper.parameterValue())
381
382    def setLayers(self, layersData):
383        self.widget.setLayers(layersData)
384
385    def extentChanged(self, wrapper):
386        self.setExtent(wrapper.parameterValue())
387
388    def setExtent(self, extent):
389        self.widget.setExtent(extent)
390
391    def setValue(self, value):
392        return self.widget.setValue(value)
393
394    def value(self):
395        return self.widget.value()
396