Skip to content
Permalink
Browse files

[processing] Add dedicated "distance" parameter

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 6a2625664e341f676808fb39fa40347d843aa955
@@ -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() )
@@ -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
@@ -28,6 +28,7 @@
from qgis.core import (QgsGeometry,
QgsWkbTypes,
QgsProcessing,
QgsProcessingParameterDistance,
QgsProcessingParameterNumber,
QgsProcessingParameterEnum,
QgsProcessingException)
@@ -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'),
@@ -65,6 +65,7 @@
from PyQt5.QtCore import QCoreApplication

PARAMETER_NUMBER = 'number'
PARAMETER_DISTANCE = 'distance'
PARAMETER_RASTER = 'raster'
PARAMETER_TABLE = 'vector'
PARAMETER_VECTOR = 'source'
No changes.
@@ -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,
@@ -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()
@@ -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)
@@ -42,6 +42,7 @@
QgsProcessingParameterDefinition,
QgsProcessingParameterBoolean,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterFile,
QgsProcessingParameterBand,
QgsProcessingParameterString,
@@ -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:
@@ -34,6 +34,7 @@

from qgis.core import (
QgsApplication,
QgsUnitTypes,
QgsCoordinateReferenceSystem,
QgsExpression,
QgsExpressionContextGenerator,
@@ -53,6 +54,7 @@
QgsProcessingParameterFile,
QgsProcessingParameterMultipleLayers,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterRasterLayer,
QgsProcessingParameterEnum,
QgsProcessingParameterString,
@@ -62,6 +64,7 @@
QgsProcessingParameterFeatureSource,
QgsProcessingParameterMapLayer,
QgsProcessingParameterBand,
QgsProcessingParameterDistance,
QgsProcessingFeatureSourceDefinition,
QgsProcessingOutputRasterLayer,
QgsProcessingOutputVectorLayer,
@@ -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
@@ -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):
@@ -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]
@@ -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()
@@ -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)
@@ -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':
@@ -43,6 +43,7 @@
QgsProcessingParameterMatrix,
QgsProcessingParameterMultipleLayers,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterRange,
QgsProcessingParameterRasterLayer,
QgsProcessingParameterEnum,
@@ -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)
@@ -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())
@@ -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()
@@ -31,6 +31,7 @@
QgsProcessingModelParameter,
QgsProcessingParameterString,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterField,
QgsProcessingParameterFile)
from processing.modeler.ModelerParametersDialog import (ModelerParametersDialog)
@@ -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" ) );

0 comments on commit 6a26256

Please sign in to comment.
You can’t perform that action at this time.