Skip to content

Commit bef5b5e

Browse files
committedDec 7, 2016
[processing][heatmap] custom parameter/widget for output resolution
Matches current behaviour of c++ heatmap plugin
1 parent 20f1f76 commit bef5b5e

File tree

6 files changed

+517
-7
lines changed

6 files changed

+517
-7
lines changed
 

‎images/themes/default/heatmap.svg

Lines changed: 143 additions & 0 deletions
Loading

‎python/plugins/processing/algs/qgis/Heatmap.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@
3636
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
3737
from processing.core.parameters import ParameterVector
3838
from processing.core.parameters import ParameterNumber
39-
from processing.core.parameters import ParameterExtent
4039
from processing.core.parameters import ParameterSelection
4140
from processing.core.parameters import ParameterTableField
4241
from processing.core.outputs import OutputRaster
4342
from processing.tools import dataobjects, vector, raster
43+
from processing.algs.qgis.ui.HeatmapWidgets import HeatmapPixelSizeWidgetWrapper
4444

4545
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
4646

@@ -80,11 +80,25 @@ def defineCharacteristics(self):
8080
self.addParameter(ParameterNumber(self.RADIUS,
8181
self.tr('Radius (layer units)'),
8282
0.0, 9999999999, 100.0))
83-
self.addParameter(ParameterNumber(self.PIXEL_SIZE,
84-
self.tr('Output pixel size (layer units)'),
85-
0.0, 9999999999, 0.1))
83+
8684
self.addParameter(ParameterTableField(self.RADIUS_FIELD,
8785
self.tr('Radius from field'), self.INPUT_LAYER, optional=True, datatype=ParameterTableField.DATA_TYPE_NUMBER))
86+
87+
class ParameterHeatmapPixelSize(ParameterNumber):
88+
89+
def __init__(self, name='', description='', parent_layer=None, radius_param=None, radius_field_param=None, minValue=None, maxValue=None,
90+
default=None, optional=False, metadata={}):
91+
ParameterNumber.__init__(self, name, description, minValue, maxValue, default, optional, metadata)
92+
self.parent_layer = parent_layer
93+
self.radius_param = radius_param
94+
self.radius_field_param = radius_field_param
95+
96+
self.addParameter(ParameterHeatmapPixelSize(self.PIXEL_SIZE,
97+
self.tr('Output raster size'), parent_layer=self.INPUT_LAYER, radius_param=self.RADIUS,
98+
radius_field_param=self.RADIUS_FIELD,
99+
minValue=0.0, maxValue=9999999999, default=0.1,
100+
metadata={'widget_wrapper': HeatmapPixelSizeWidgetWrapper}))
101+
88102
self.addParameter(ParameterTableField(self.WEIGHT_FIELD,
89103
self.tr('Weight from field'), self.INPUT_LAYER, optional=True, datatype=ParameterTableField.DATA_TYPE_NUMBER))
90104
self.addParameter(ParameterSelection(self.KERNEL,
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
***************************************************************************
5+
Heatmap.py
6+
---------------------
7+
Date : December 2016
8+
Copyright : (C) 2016 by Nyall Dawson
9+
Email : nyall dot dawson 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__ = 'Nyall Dawson'
21+
__date__ = 'December 2016'
22+
__copyright__ = '(C) 2016, Nyall Dawson'
23+
24+
from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD
25+
from processing.tools import dataobjects
26+
from processing.core.parameters import _resolveLayers
27+
import os
28+
from qgis.PyQt import uic
29+
from qgis.gui import QgsDoubleSpinBox
30+
from qgis.core import QgsRectangle
31+
32+
pluginPath = os.path.dirname(__file__)
33+
WIDGET, BASE = uic.loadUiType(
34+
os.path.join(pluginPath, 'RasterResolutionWidget.ui'))
35+
36+
37+
class HeatmapPixelSizeWidget(BASE, WIDGET):
38+
39+
def __init__(self):
40+
super(HeatmapPixelSizeWidget, self).__init__(None)
41+
self.setupUi(self)
42+
43+
self.layer_bounds = QgsRectangle()
44+
self.layer = None
45+
self.raster_bounds = QgsRectangle()
46+
self.radius = 100
47+
self.radius_field = None
48+
49+
self.mCellXSpinBox.setShowClearButton(False)
50+
self.mCellYSpinBox.setShowClearButton(False)
51+
self.mRowsSpinBox.setShowClearButton(False)
52+
self.mColumnsSpinBox.setShowClearButton(False)
53+
54+
self.mCellYSpinBox.valueChanged.connect(self.mCellXSpinBox.setValue)
55+
self.mCellXSpinBox.valueChanged.connect(self.pixelSizeChanged)
56+
self.mRowsSpinBox.valueChanged.connect(self.rowsChanged)
57+
self.mColumnsSpinBox.valueChanged.connect(self.columnsChanged)
58+
59+
def setRadius(self, radius):
60+
self.radius = radius
61+
self.recalculate_bounds()
62+
63+
def setRadiusField(self, radius_field):
64+
self.radius_field = radius_field
65+
self.recalculate_bounds()
66+
67+
def setLayer(self, layer):
68+
if not layer:
69+
return
70+
bounds = layer.extent()
71+
if bounds.isNull():
72+
return
73+
74+
self.layer = layer
75+
self.layer_bounds = bounds
76+
self.recalculate_bounds()
77+
78+
def recalculate_bounds(self):
79+
self.raster_bounds = QgsRectangle(self.layer_bounds)
80+
81+
if not self.layer:
82+
return
83+
84+
max_radius = self.radius
85+
if self.radius_field:
86+
idx = self.layer.fields().lookupField(self.radius_field)
87+
try:
88+
max_radius = float(self.layer.maximumValue(idx))
89+
except:
90+
pass
91+
92+
self.raster_bounds.setXMinimum(self.raster_bounds.xMinimum() - max_radius)
93+
self.raster_bounds.setYMinimum(self.raster_bounds.yMinimum() - max_radius)
94+
self.raster_bounds.setXMaximum(self.raster_bounds.xMaximum() + max_radius)
95+
self.raster_bounds.setYMaximum(self.raster_bounds.yMaximum() + max_radius)
96+
97+
self.pixelSizeChanged()
98+
99+
def pixelSizeChanged(self):
100+
cell_size = self.mCellXSpinBox.value()
101+
if cell_size <= 0:
102+
return
103+
self.mCellYSpinBox.blockSignals(True)
104+
self.mCellYSpinBox.setValue(cell_size)
105+
self.mCellYSpinBox.blockSignals(False)
106+
rows = max(round(self.raster_bounds.height() / cell_size) + 1, 1)
107+
cols = max(round(self.raster_bounds.width() / cell_size) + 1, 1)
108+
self.mRowsSpinBox.blockSignals(True)
109+
self.mRowsSpinBox.setValue(rows)
110+
self.mRowsSpinBox.blockSignals(False)
111+
self.mColumnsSpinBox.blockSignals(True)
112+
self.mColumnsSpinBox.setValue(cols)
113+
self.mColumnsSpinBox.blockSignals(False)
114+
115+
def rowsChanged(self):
116+
rows = self.mRowsSpinBox.value()
117+
if rows <= 0:
118+
return
119+
cell_size = self.raster_bounds.height() / rows
120+
cols = max(round(self.raster_bounds.width() / cell_size) + 1, 1)
121+
self.mColumnsSpinBox.blockSignals(True)
122+
self.mColumnsSpinBox.setValue(cols)
123+
self.mColumnsSpinBox.blockSignals(False)
124+
for w in [self.mCellXSpinBox, self.mCellYSpinBox]:
125+
w.blockSignals(True)
126+
w.setValue(cell_size)
127+
w.blockSignals(False)
128+
129+
def columnsChanged(self):
130+
cols = self.mColumnsSpinBox.value()
131+
if cols < 2:
132+
return
133+
cell_size = self.raster_bounds.width() / (cols - 1)
134+
rows = max(round(self.raster_bounds.height() / cell_size), 1)
135+
self.mRowsSpinBox.blockSignals(True)
136+
self.mRowsSpinBox.setValue(rows)
137+
self.mRowsSpinBox.blockSignals(False)
138+
for w in [self.mCellXSpinBox, self.mCellYSpinBox]:
139+
w.blockSignals(True)
140+
w.setValue(cell_size)
141+
w.blockSignals(False)
142+
143+
def setValue(self, value):
144+
try:
145+
numeric_value = float(value)
146+
except:
147+
return False
148+
149+
self.mCellXSpinBox.setValue(numeric_value)
150+
self.mCellYSpinBox.setValue(numeric_value)
151+
return True
152+
153+
def value(self):
154+
return self.mCellXSpinBox.value()
155+
156+
157+
class HeatmapPixelSizeWidgetWrapper(WidgetWrapper):
158+
159+
def _panel(self):
160+
return HeatmapPixelSizeWidget()
161+
162+
def createWidget(self):
163+
if self.dialogType == DIALOG_STANDARD:
164+
return self._panel()
165+
else:
166+
w = QgsDoubleSpinBox()
167+
w.setShowClearButton(False)
168+
w.setMinimum(0)
169+
w.setMaximum(99999999999)
170+
w.setDecimals(6)
171+
w.setTooltip(self.tr('Resolution of each pixel in output raster, in layer units'))
172+
173+
def postInitialize(self, wrappers):
174+
if self.dialogType != DIALOG_STANDARD:
175+
return
176+
177+
for wrapper in wrappers:
178+
if wrapper.param.name == self.param.parent_layer:
179+
self.setLayer(wrapper.value())
180+
wrapper.widgetValueHasChanged.connect(self.parentLayerChanged)
181+
elif wrapper.param.name == self.param.radius_param:
182+
self.setRadius(wrapper.value())
183+
wrapper.widgetValueHasChanged.connect(self.radiusChanged)
184+
elif wrapper.param.name == self.param.radius_field_param:
185+
self.setLayer(wrapper.value())
186+
wrapper.widgetValueHasChanged.connect(self.radiusFieldChanged)
187+
188+
def parentLayerChanged(self, wrapper):
189+
self.setLayer(wrapper.value())
190+
191+
def setLayer(self, layer):
192+
if isinstance(layer, str):
193+
layer = dataobjects.getObjectFromUri(_resolveLayers(layer))
194+
self.widget.setLayer(layer)
195+
196+
def radiusChanged(self, wrapper):
197+
self.setRadius(wrapper.value())
198+
199+
def setRadius(self, radius):
200+
self.widget.setRadius(radius)
201+
202+
def radiusFieldChanged(self, wrapper):
203+
self.setRadiusField(wrapper.value())
204+
205+
def setRadiusField(self, radius_field):
206+
self.widget.setRadiusField(radius_field)
207+
208+
def setValue(self, value):
209+
return self.widget.setValue(value)
210+
211+
def value(self):
212+
return self.widget.value()
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>Form</class>
4+
<widget class="QWidget" name="Form">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>583</width>
10+
<height>99</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Form</string>
15+
</property>
16+
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0,0,1">
17+
<item row="0" column="1">
18+
<widget class="QgsSpinBox" name="mRowsSpinBox">
19+
<property name="toolTip">
20+
<string>Number of rows (pixels) in output raster</string>
21+
</property>
22+
<property name="keyboardTracking">
23+
<bool>false</bool>
24+
</property>
25+
<property name="minimum">
26+
<number>1</number>
27+
</property>
28+
<property name="maximum">
29+
<number>999999</number>
30+
</property>
31+
</widget>
32+
</item>
33+
<item row="0" column="2">
34+
<widget class="QLabel" name="columnLabel">
35+
<property name="text">
36+
<string>Columns</string>
37+
</property>
38+
<property name="buddy">
39+
<cstring>mColumnsSpinBox</cstring>
40+
</property>
41+
</widget>
42+
</item>
43+
<item row="1" column="1">
44+
<widget class="QgsDoubleSpinBox" name="mCellXSpinBox">
45+
<property name="toolTip">
46+
<string>Resolution of each pixel in output raster, in layer units</string>
47+
</property>
48+
<property name="decimals">
49+
<number>6</number>
50+
</property>
51+
<property name="maximum">
52+
<double>999999999.000000000000000</double>
53+
</property>
54+
</widget>
55+
</item>
56+
<item row="1" column="0">
57+
<widget class="QLabel" name="cellsizeXLabel">
58+
<property name="text">
59+
<string>Pixel size X</string>
60+
</property>
61+
</widget>
62+
</item>
63+
<item row="0" column="3">
64+
<widget class="QgsSpinBox" name="mColumnsSpinBox">
65+
<property name="toolTip">
66+
<string>Number of columns (pixels) in output raster</string>
67+
</property>
68+
<property name="keyboardTracking">
69+
<bool>false</bool>
70+
</property>
71+
<property name="minimum">
72+
<number>1</number>
73+
</property>
74+
<property name="maximum">
75+
<number>999999</number>
76+
</property>
77+
</widget>
78+
</item>
79+
<item row="0" column="0">
80+
<widget class="QLabel" name="rowLabel">
81+
<property name="text">
82+
<string>Rows</string>
83+
</property>
84+
<property name="buddy">
85+
<cstring>mRowsSpinBox</cstring>
86+
</property>
87+
</widget>
88+
</item>
89+
<item row="1" column="2">
90+
<widget class="QLabel" name="cellsizeYLabel">
91+
<property name="text">
92+
<string>Pixel size Y</string>
93+
</property>
94+
</widget>
95+
</item>
96+
<item row="1" column="3">
97+
<widget class="QgsDoubleSpinBox" name="mCellYSpinBox">
98+
<property name="toolTip">
99+
<string>Resolution of each pixel in output raster, in layer units</string>
100+
</property>
101+
<property name="decimals">
102+
<number>6</number>
103+
</property>
104+
<property name="maximum">
105+
<double>999999999.000000000000000</double>
106+
</property>
107+
</widget>
108+
</item>
109+
<item row="0" column="4">
110+
<spacer name="horizontalSpacer_2">
111+
<property name="orientation">
112+
<enum>Qt::Horizontal</enum>
113+
</property>
114+
<property name="sizeHint" stdset="0">
115+
<size>
116+
<width>40</width>
117+
<height>20</height>
118+
</size>
119+
</property>
120+
</spacer>
121+
</item>
122+
</layout>
123+
</widget>
124+
<customwidgets>
125+
<customwidget>
126+
<class>QgsSpinBox</class>
127+
<extends>QSpinBox</extends>
128+
<header>qgis.gui</header>
129+
</customwidget>
130+
<customwidget>
131+
<class>QgsDoubleSpinBox</class>
132+
<extends>QDoubleSpinBox</extends>
133+
<header>qgis.gui</header>
134+
</customwidget>
135+
</customwidgets>
136+
<resources/>
137+
<connections/>
138+
</ui>

‎python/plugins/processing/core/parameters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -781,8 +781,8 @@ class ParameterNumber(Parameter):
781781
}
782782

783783
def __init__(self, name='', description='', minValue=None, maxValue=None,
784-
default=None, optional=False):
785-
Parameter.__init__(self, name, description, default, optional)
784+
default=None, optional=False, metadata={}):
785+
Parameter.__init__(self, name, description, default, optional, metadata)
786786

787787
if default is not None:
788788
try:

‎python/plugins/processing/gui/wrappers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,9 @@ class NumberWidgetWrapper(WidgetWrapper):
545545

546546
def createWidget(self):
547547
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
548-
return NumberInputPanel(self.param)
548+
widget = NumberInputPanel(self.param)
549+
widget.hasChanged.connect(lambda: self.widgetValueHasChanged.emit(self))
550+
return widget
549551
else:
550552
return ModellerNumberInputPanel(self.param, self.dialog)
551553

@@ -1026,6 +1028,7 @@ def createWidget(self):
10261028
else:
10271029
widget = QgsFieldComboBox()
10281030
widget.setAllowEmptyFieldName(self.param.optional)
1031+
widget.fieldChanged.connect(lambda: self.widgetValueHasChanged.emit(self))
10291032
if self.param.datatype == ParameterTableField.DATA_TYPE_NUMBER:
10301033
widget.setFilters(QgsFieldProxyModel.Numeric)
10311034
elif self.param.datatype == ParameterTableField.DATA_TYPE_STRING:

0 commit comments

Comments
 (0)
Please sign in to comment.