Skip to content

Commit

Permalink
[processing] Add dedicated "distance" parameter
Browse files Browse the repository at this point in the history
This is a subclass of QgsProcessingParameterNumber, but specifically
for numeric parameters which represent distances. It is linked
to a parent parameter, from which the distance unit will
be determined, and is shown using a dedicated distance widget
within the processing parameters panel. This widget shows
the distance unit.

This avoids the confusion when running algorithms which
use distances where the unit depends on a layer or CRS parameter -
e.g. the distance parameter in the buffer algorithm gives
the distance in layer units... so now we can show those units
directly within the dialog. Hopefully this leads to less
user confusion and accidental "1000 degree buffers"!

Additionally - if the unit is in degrees, a small warning
icon is shown next to the parameter. The tooltip for this
icon advises users to reproject data into a suitable
projected local coordinate system.

Initially implemented for the native buffer and single
sided buffer algorithm only - but more will be added.

Fixes #16290
  • Loading branch information
nyalldawson committed Apr 20, 2018
1 parent 9107da3 commit 6a26256
Show file tree
Hide file tree
Showing 16 changed files with 404 additions and 24 deletions.
60 changes: 60 additions & 0 deletions python/core/processing/qgsprocessingparameters.sip.in
Expand Up @@ -130,6 +130,8 @@ their acceptable ranges, defaults, etc.
sipType = sipType_QgsProcessingParameterMultipleLayers;
else if ( sipCpp->type() == QgsProcessingParameterNumber::typeName() )
sipType = sipType_QgsProcessingParameterNumber;
else if ( sipCpp->type() == QgsProcessingParameterDistance::typeName() )
sipType = sipType_QgsProcessingParameterDistance;
else if ( sipCpp->type() == QgsProcessingParameterRange::typeName() )
sipType = sipType_QgsProcessingParameterRange;
else if ( sipCpp->type() == QgsProcessingParameterRasterLayer::typeName() )
Expand Down Expand Up @@ -1223,6 +1225,64 @@ Sets the acceptable data ``type`` for the parameter.
Creates a new parameter using the definition from a script code.
%End

};

class QgsProcessingParameterDistance : QgsProcessingParameterNumber
{
%Docstring
A double numeric parameter for distance values. Linked to a source layer or CRS parameter
to determine what units the distance values are in.

.. versionadded:: 3.2
%End

%TypeHeaderCode
#include "qgsprocessingparameters.h"
%End
public:

explicit QgsProcessingParameterDistance( const QString &name, const QString &description = QString(),
const QVariant &defaultValue = QVariant(),
const QString &parentParameterName = QString(),
bool optional = false,
double minValue = -DBL_MAX + 1,
double maxValue = DBL_MAX );
%Docstring
Constructor for QgsProcessingParameterDistance.
%End

static QString typeName();
%Docstring
Returns the type name for the parameter class.
%End

virtual QgsProcessingParameterDistance *clone() const /Factory/;


virtual QString type() const;

virtual QStringList dependsOnOtherParameters() const;


QString parentParameterName() const;
%Docstring
Returns the name of the parent parameter, or an empty string if this is not set.

.. seealso:: :py:func:`setParentParameterName`
%End

void setParentParameterName( const QString &parentParameterName );
%Docstring
Sets the name of the parent layer parameter. Use an empty string if this is not required.

.. seealso:: :py:func:`parentParameterName`
%End

virtual QVariantMap toVariantMap() const;

virtual bool fromVariantMap( const QVariantMap &map );


};

class QgsProcessingParameterRange : QgsProcessingParameterDefinition
Expand Down
7 changes: 4 additions & 3 deletions python/plugins/processing/algs/qgis/SingleSidedBuffer.py
Expand Up @@ -28,6 +28,7 @@
from qgis.core import (QgsGeometry,
QgsWkbTypes,
QgsProcessing,
QgsProcessingParameterDistance,
QgsProcessingParameterNumber,
QgsProcessingParameterEnum,
QgsProcessingException)
Expand Down Expand Up @@ -62,9 +63,9 @@ def __init__(self):
'Bevel']

def initParameters(self, config=None):
self.addParameter(QgsProcessingParameterNumber(self.DISTANCE,
self.tr('Distance'), QgsProcessingParameterNumber.Double,
defaultValue=10.0))
self.addParameter(QgsProcessingParameterDistance(self.DISTANCE,
self.tr('Distance'), parentParameterName='INPUT',
defaultValue=10.0))
self.addParameter(QgsProcessingParameterEnum(
self.SIDE,
self.tr('Side'),
Expand Down
1 change: 1 addition & 0 deletions python/plugins/processing/core/parameters.py 100644 → 100755
Expand Up @@ -65,6 +65,7 @@
from PyQt5.QtCore import QCoreApplication

PARAMETER_NUMBER = 'number'
PARAMETER_DISTANCE = 'distance'
PARAMETER_RASTER = 'raster'
PARAMETER_TABLE = 'vector'
PARAMETER_VECTOR = 'source'
Expand Down
Empty file modified python/plugins/processing/gui/MultipleInputDialog.py 100644 → 100755
Empty file.
63 changes: 54 additions & 9 deletions python/plugins/processing/gui/NumberInputPanel.py 100644 → 100755
Expand Up @@ -30,11 +30,15 @@
import sip

from qgis.PyQt import uic
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtWidgets import QDialog
from qgis.PyQt.QtCore import pyqtSignal, QSize
from qgis.PyQt.QtWidgets import QDialog, QLabel

from qgis.core import (QgsExpression,
from qgis.core import (QgsApplication,
QgsExpression,
QgsProperty,
QgsUnitTypes,
QgsMapLayer,
QgsCoordinateReferenceSystem,
QgsProcessingParameterNumber,
QgsProcessingOutputNumber,
QgsProcessingParameterDefinition,
Expand Down Expand Up @@ -203,16 +207,19 @@ def __init__(self, param):
self.spnValue.valueChanged.connect(lambda: self.hasChanged.emit())

def setDynamicLayer(self, layer):
context = createContext()
try:
if isinstance(layer, QgsProcessingFeatureSourceDefinition):
layer, ok = layer.source.valueAsString(context.expressionContext())
if isinstance(layer, str):
layer = QgsProcessingUtils.mapLayerFromString(layer, context)
self.btnDataDefined.setVectorLayer(layer)
self.btnDataDefined.setVectorLayer(self.getLayerFromValue(layer))
except:
pass

def getLayerFromValue(self, value):
context = createContext()
if isinstance(value, QgsProcessingFeatureSourceDefinition):
value, ok = value.source.valueAsString(context.expressionContext())
if isinstance(value, str):
value = QgsProcessingUtils.mapLayerFromString(value, context)
return value

def getValue(self):
if self.btnDataDefined is not None and self.btnDataDefined.isActive():
return self.btnDataDefined.toProperty()
Expand All @@ -235,3 +242,41 @@ def calculateStep(self, minimum, maximum):
return round(step, -int(math.floor(math.log10(step))))
else:
return 1.0


class DistanceInputPanel(NumberInputPanel):

"""
Distance input panel for use outside the modeler - this input panel
contains a label showing the distance unit.
"""

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

self.label = QLabel('')
label_margin = self.fontMetrics().width('X')
self.layout().insertSpacing(1, label_margin / 2)
self.layout().insertWidget(2, self.label)
self.layout().insertSpacing(3, label_margin / 2)
self.warning_label = QLabel()
icon = QgsApplication.getThemeIcon('mIconWarning.svg')
size = max(24, self.spnValue.height() * 0.5)
self.warning_label.setPixmap(icon.pixmap(icon.actualSize(QSize(size, size))))
self.warning_label.setToolTip(self.tr('Distance is in geographic degrees. Consider reprojecting to a projected local coordinate system for accurate results.'))
self.layout().insertWidget(4, self.warning_label)
self.layout().insertSpacing(5, label_margin)
self.setUnits(QgsUnitTypes.DistanceUnknownUnit)

def setUnits(self, units):
self.label.setText(QgsUnitTypes.toString(units))
self.warning_label.setVisible(units == QgsUnitTypes.DistanceDegrees)

def setUnitParameterValue(self, value):
units = QgsUnitTypes.DistanceUnknownUnit
layer = self.getLayerFromValue(value)
if isinstance(layer, QgsMapLayer):
units = layer.crs().mapUnits()
elif isinstance(value, QgsCoordinateReferenceSystem):
units = value.mapUnits()
self.setUnits(units)
3 changes: 2 additions & 1 deletion python/plugins/processing/gui/TestTools.py 100644 → 100755
Expand Up @@ -42,6 +42,7 @@
QgsProcessingParameterDefinition,
QgsProcessingParameterBoolean,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterFile,
QgsProcessingParameterBand,
QgsProcessingParameterString,
Expand Down Expand Up @@ -224,7 +225,7 @@ def createTest(text):
params[param.name()] = token
elif isinstance(param, QgsProcessingParameterBoolean):
params[param.name()] = token
elif isinstance(param, QgsProcessingParameterNumber):
elif isinstance(param, (QgsProcessingParameterNumber,QgsProcessingParameterDistance)):
if param.dataType() == QgsProcessingParameterNumber.Integer:
params[param.name()] = int(token)
else:
Expand Down
49 changes: 45 additions & 4 deletions python/plugins/processing/gui/wrappers.py 100644 → 100755
Expand Up @@ -34,6 +34,7 @@

from qgis.core import (
QgsApplication,
QgsUnitTypes,
QgsCoordinateReferenceSystem,
QgsExpression,
QgsExpressionContextGenerator,
Expand All @@ -53,6 +54,7 @@
QgsProcessingParameterFile,
QgsProcessingParameterMultipleLayers,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterRasterLayer,
QgsProcessingParameterEnum,
QgsProcessingParameterString,
Expand All @@ -62,6 +64,7 @@
QgsProcessingParameterFeatureSource,
QgsProcessingParameterMapLayer,
QgsProcessingParameterBand,
QgsProcessingParameterDistance,
QgsProcessingFeatureSourceDefinition,
QgsProcessingOutputRasterLayer,
QgsProcessingOutputVectorLayer,
Expand Down Expand Up @@ -103,7 +106,7 @@
from processing.core.ProcessingConfig import ProcessingConfig
from processing.modeler.MultilineTextPanel import MultilineTextPanel

from processing.gui.NumberInputPanel import NumberInputPanel, ModelerNumberInputPanel
from processing.gui.NumberInputPanel import NumberInputPanel, ModelerNumberInputPanel, DistanceInputPanel
from processing.gui.RangePanel import RangePanel
from processing.gui.PointSelectionPanel import PointSelectionPanel
from processing.gui.FileSelectionPanel import FileSelectionPanel
Expand Down Expand Up @@ -752,6 +755,42 @@ def parentLayerChanged(self, wrapper):
self.widget.setDynamicLayer(wrapper.value())


class DistanceWidgetWrapper(WidgetWrapper):

def createWidget(self):
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
widget = DistanceInputPanel(self.param)
widget.hasChanged.connect(lambda: self.widgetValueHasChanged.emit(self))
return widget
else:
return ModelerNumberInputPanel(self.param, self.dialog)

def setValue(self, value):
if value is None or value == NULL:
return

self.widget.setValue(value)

def value(self):
return self.widget.getValue()

def postInitialize(self, wrappers):
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
for wrapper in wrappers:
if wrapper.param.name() == self.param.dynamicLayerParameterName():
self.widget.setDynamicLayer(wrapper.value())
wrapper.widgetValueHasChanged.connect(self.dynamicLayerChanged)
if wrapper.param.name() == self.param.parentParameterName():
self.widget.setUnitParameterValue(wrapper.value())
wrapper.widgetValueHasChanged.connect(self.parentParameterChanged)

def dynamicLayerChanged(self, wrapper):
self.widget.setDynamicLayer(wrapper.value())

def parentParameterChanged(self, wrapper):
self.widget.setUnitParameterValue(wrapper.value())


class RangeWidgetWrapper(WidgetWrapper):

def createWidget(self):
Expand Down Expand Up @@ -1133,7 +1172,7 @@ def createWidget(self):
else:
# strings, numbers, files and table fields are all allowed input types
strings = self.dialog.getAvailableValuesOfType(
[QgsProcessingParameterString, QgsProcessingParameterNumber, QgsProcessingParameterFile,
[QgsProcessingParameterString, QgsProcessingParameterNumber, QgsProcessingParameterDistance, QgsProcessingParameterFile,
QgsProcessingParameterField, QgsProcessingParameterExpression],
[QgsProcessingOutputString, QgsProcessingOutputFile])
options = [(self.dialog.resolveValueDescription(s), s) for s in strings]
Expand Down Expand Up @@ -1224,7 +1263,7 @@ def createWidget(self):
widget.setExpression(self.param.defaultValue())
else:
strings = self.dialog.getAvailableValuesOfType(
[QgsProcessingParameterExpression, QgsProcessingParameterString, QgsProcessingParameterNumber],
[QgsProcessingParameterExpression, QgsProcessingParameterString, QgsProcessingParameterNumber, QgsProcessingParameterDistance],
(QgsProcessingOutputString, QgsProcessingOutputNumber))
options = [(self.dialog.resolveValueDescription(s), s) for s in strings]
widget = QComboBox()
Expand Down Expand Up @@ -1540,7 +1579,7 @@ def createWidget(self):
else:
widget = QComboBox()
widget.setEditable(True)
fields = self.dialog.getAvailableValuesOfType([QgsProcessingParameterBand, QgsProcessingParameterNumber],
fields = self.dialog.getAvailableValuesOfType([QgsProcessingParameterBand, QgsProcessingParameterDistance, QgsProcessingParameterNumber],
[QgsProcessingOutputNumber])
if self.param.flags() & QgsProcessingParameterDefinition.FlagOptional:
widget.addItem(self.NOT_SET, self.NOT_SET_OPTION)
Expand Down Expand Up @@ -1644,6 +1683,8 @@ def create_wrapper_from_class(param, dialog, row=0, col=0):
wrapper = MultipleLayerWidgetWrapper
elif param.type() == 'number':
wrapper = NumberWidgetWrapper
elif param.type() == 'distance':
wrapper = DistanceWidgetWrapper
elif param.type() == 'raster':
wrapper = RasterWidgetWrapper
elif param.type() == 'enum':
Expand Down
7 changes: 4 additions & 3 deletions python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py 100644 → 100755
Expand Up @@ -43,6 +43,7 @@
QgsProcessingParameterMatrix,
QgsProcessingParameterMultipleLayers,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterRange,
QgsProcessingParameterRasterLayer,
QgsProcessingParameterEnum,
Expand Down Expand Up @@ -197,8 +198,8 @@ def setupUi(self):
if self.param is not None:
self.datatypeCombo.setCurrentIndex(self.datatypeCombo.findData(self.param.layerType()))
self.verticalLayout.addWidget(self.datatypeCombo)
elif (self.paramType == parameters.PARAMETER_NUMBER or
isinstance(self.param, QgsProcessingParameterNumber)):
elif (self.paramType == parameters.PARAMETER_NUMBER or self.paramType == parameters.PARAMETER_DISTANCE or
isinstance(self.param, (QgsProcessingParameterNumber, QgsProcessingParameterDistance))):
self.verticalLayout.addWidget(QLabel(self.tr('Min value')))
self.minTextBox = QLineEdit()
self.verticalLayout.addWidget(self.minTextBox)
Expand Down Expand Up @@ -360,7 +361,7 @@ def accept(self):
name, description,
self.datatypeCombo.currentData())
elif (self.paramType == parameters.PARAMETER_NUMBER or
isinstance(self.param, QgsProcessingParameterNumber)):
isinstance(self.param, (QgsProcessingParameterNumber, QgsProcessingParameterDistance))):
try:
self.param = QgsProcessingParameterNumber(name, description, QgsProcessingParameterNumber.Double,
self.defaultTextBox.text())
Expand Down
12 changes: 9 additions & 3 deletions python/plugins/processing/tests/GuiTest.py
Expand Up @@ -115,12 +115,18 @@ def testField(self):
def testSource(self):
self.checkConstructWrapper(QgsProcessingParameterFeatureSource('test'), FeatureSourceWidgetWrapper)

def testSource(self):
self.checkConstructWrapper(QgsProcessingParameterBand('test'), BandWidgetWrapper)

def testMapLayer(self):
self.checkConstructWrapper(QgsProcessingParameterMapLayer('test'), MapLayerWidgetWrapper)

def testDistance(self):
self.checkConstructWrapper(QgsProcessingParameterDistance('test'), DistanceWidgetWrapper)

def testNumber(self):
self.checkConstructWrapper(QgsProcessingParameterNumber('test'), NumberWidgetWrapper)

def testBand(self):
self.checkConstructWrapper(QgsProcessingParameterBand('test'), BandWidgetWrapper)


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions python/plugins/processing/tests/ModelerTest.py 100644 → 100755
Expand Up @@ -31,6 +31,7 @@
QgsProcessingModelParameter,
QgsProcessingParameterString,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterField,
QgsProcessingParameterFile)
from processing.modeler.ModelerParametersDialog import (ModelerParametersDialog)
Expand Down
2 changes: 1 addition & 1 deletion src/analysis/processing/qgsalgorithmbuffer.cpp
Expand Up @@ -48,7 +48,7 @@ void QgsBufferAlgorithm::initAlgorithm( const QVariantMap & )
{
addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) );

auto bufferParam = qgis::make_unique < QgsProcessingParameterNumber >( QStringLiteral( "DISTANCE" ), QObject::tr( "Distance" ), QgsProcessingParameterNumber::Double, 10 );
auto bufferParam = qgis::make_unique < QgsProcessingParameterDistance >( QStringLiteral( "DISTANCE" ), QObject::tr( "Distance" ), 10, QStringLiteral( "INPUT" ) );
bufferParam->setIsDynamic( true );
bufferParam->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "Distance" ), QObject::tr( "Buffer distance" ), QgsPropertyDefinition::Double ) );
bufferParam->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) );
Expand Down

0 comments on commit 6a26256

Please sign in to comment.