[FEATURE][processing] Choice of units for non degree/unknown distances

When an algorithm has a distance parameter in meters/feet/etc (i.e.
non-geographic distances), show a combo box allowing choice of
unit type.

(We don't (and should never) expose this for distances in degrees --
it's up to users in this situation to choose a suitable local
projection and reproject their data to match. Refs: a recent
talk by @volaya)
This commit is contained in:
Nyall Dawson 2018-07-26 14:38:37 +10:00
parent d1d6840a5f
commit 2692de6ed0
2 changed files with 73 additions and 2 deletions

View File

@ -32,7 +32,7 @@ import warnings
from qgis.PyQt import uic
from qgis.PyQt.QtCore import pyqtSignal, QSize
from qgis.PyQt.QtWidgets import QDialog, QLabel
from qgis.PyQt.QtWidgets import QDialog, QLabel, QComboBox
from qgis.core import (QgsApplication,
QgsExpression,
@ -268,10 +268,21 @@ class DistanceInputPanel(NumberInputPanel):
super().__init__(param)
self.label = QLabel('')
self.units_combo = QComboBox()
self.base_units = QgsUnitTypes.DistanceUnknownUnit
for u in (QgsUnitTypes.DistanceMeters,
QgsUnitTypes.DistanceKilometers,
QgsUnitTypes.DistanceFeet,
QgsUnitTypes.DistanceMiles,
QgsUnitTypes.DistanceYards):
self.units_combo.addItem(QgsUnitTypes.toString(u), u)
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.layout().insertWidget(3, self.units_combo)
self.layout().insertSpacing(4, label_margin / 2)
self.warning_label = QLabel()
icon = QgsApplication.getThemeIcon('mIconWarning.svg')
size = max(24, self.spnValue.height() * 0.5)
@ -279,11 +290,20 @@ class DistanceInputPanel(NumberInputPanel):
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))
if QgsUnitTypes.unitType(units) != QgsUnitTypes.Standard:
self.units_combo.hide()
self.label.show()
else:
self.units_combo.setCurrentIndex(self.units_combo.findData(units))
self.units_combo.show()
self.label.hide()
self.warning_label.setVisible(units == QgsUnitTypes.DistanceDegrees)
self.base_units = units
def setUnitParameterValue(self, value):
units = QgsUnitTypes.DistanceUnknownUnit
@ -297,3 +317,17 @@ class DistanceInputPanel(NumberInputPanel):
if crs.isValid():
units = crs.mapUnits()
self.setUnits(units)
def getValue(self):
val = super().getValue()
if isinstance(val, float) and self.units_combo.isVisible():
display_unit = self.units_combo.currentData()
return val * QgsUnitTypes.fromUnitToUnitFactor(display_unit, self.base_units)
return val
def setValue(self, value):
try:
self.spnValue.setValue(float(value))
except:
return

View File

@ -176,35 +176,72 @@ class WrappersTest(unittest.TestCase):
widget.setUnitParameterValue('EPSG:3111')
self.assertEqual(widget.label.text(), 'meters')
self.assertFalse(widget.warning_label.isVisible())
self.assertTrue(widget.units_combo.isVisible())
self.assertFalse(widget.label.isVisible())
self.assertEqual(widget.units_combo.currentData(), QgsUnitTypes.DistanceMeters)
widget.setUnitParameterValue('EPSG:4326')
self.assertEqual(widget.label.text(), 'degrees')
self.assertTrue(widget.warning_label.isVisible())
self.assertFalse(widget.units_combo.isVisible())
self.assertTrue(widget.label.isVisible())
widget.setUnitParameterValue(QgsCoordinateReferenceSystem('EPSG:3111'))
self.assertEqual(widget.label.text(), 'meters')
self.assertFalse(widget.warning_label.isVisible())
self.assertTrue(widget.units_combo.isVisible())
self.assertFalse(widget.label.isVisible())
self.assertEqual(widget.units_combo.currentData(), QgsUnitTypes.DistanceMeters)
widget.setUnitParameterValue(QgsCoordinateReferenceSystem('EPSG:4326'))
self.assertEqual(widget.label.text(), 'degrees')
self.assertTrue(widget.warning_label.isVisible())
self.assertFalse(widget.units_combo.isVisible())
self.assertTrue(widget.label.isVisible())
# layer values
vl = QgsVectorLayer("Polygon?crs=epsg:3111&field=pk:int", "vl", "memory")
widget.setUnitParameterValue(vl)
self.assertEqual(widget.label.text(), 'meters')
self.assertFalse(widget.warning_label.isVisible())
self.assertTrue(widget.units_combo.isVisible())
self.assertFalse(widget.label.isVisible())
self.assertEqual(widget.units_combo.currentData(), QgsUnitTypes.DistanceMeters)
vl2 = QgsVectorLayer("Polygon?crs=epsg:4326&field=pk:int", "vl", "memory")
widget.setUnitParameterValue(vl2)
self.assertEqual(widget.label.text(), 'degrees')
self.assertTrue(widget.warning_label.isVisible())
self.assertFalse(widget.units_combo.isVisible())
self.assertTrue(widget.label.isVisible())
# unresolvable values
widget.setUnitParameterValue(vl.id())
self.assertEqual(widget.label.text(), '<unknown>')
self.assertFalse(widget.warning_label.isVisible())
self.assertFalse(widget.units_combo.isVisible())
self.assertTrue(widget.label.isVisible())
# resolvable text value
QgsProject.instance().addMapLayer(vl)
widget.setUnitParameterValue(vl.id())
self.assertEqual(widget.label.text(), 'meters')
self.assertFalse(widget.warning_label.isVisible())
self.assertTrue(widget.units_combo.isVisible())
self.assertFalse(widget.label.isVisible())
self.assertEqual(widget.units_combo.currentData(), QgsUnitTypes.DistanceMeters)
widget.setValue(5)
self.assertEqual(widget.getValue(), 5)
widget.units_combo.setCurrentIndex(widget.units_combo.findData(QgsUnitTypes.DistanceKilometers))
self.assertEqual(widget.getValue(), 5000)
widget.setValue(2)
self.assertEqual(widget.getValue(), 2000)
widget.setUnitParameterValue(vl.id())
self.assertEqual(widget.getValue(), 2)
widget.setValue(5)
self.assertEqual(widget.getValue(), 5)
widget.deleteLater()