[needs-docs] Add option to take no action ("Leave as an unknown CRS") when

a layer is loaded which has no CRS

This is the new default behavior. It effectively defers the choice of CRS
from layer loading time to a later time which is convenient for users. E.g.
it no longer asks for CRS choice 100x when loading 100 files into QGIS at once.
(Effectively an application crash/hang... no one will sit through these 100 dialogs!)

The new behavior means there's no prompt (by default!) when loading these layers,
and instead the layers will be shown in the layer tree with an "unknown CRS"
indicator icon. They'll also be un-referenced, with coordinates from the layer
treated as purely numerically, non-earth values. I.e. the same behavior
as all layers get when a project is set to have no CRS.

The user can then resolve these in their own time, and by using convenient
shortcuts like highlighting multiple layers at once and then setting the CRS
for all these in a single action.

Fixes #19762, #27634, #24815  (and probably others)
This commit is contained in:
Nyall Dawson 2019-10-07 11:55:57 +10:00
parent c43381139a
commit ffe66bff82
5 changed files with 205 additions and 148 deletions

View File

@ -70,6 +70,13 @@ projections\defaultProjectCrs=EPSG:4326
# "UsePresetCrs" - always use a preset CRS, see projections\defaultProjectCrs
projections\newProjectCrsBehavior=UseCrsOfFirstLayerAdded
# Specifies the behavior when adding a layer with unknown/invalid CRS to a project
# "NoAction" - take no action and leave as unknown CRS
# "PromptUserForCrs" - user is prompted for a CRS choice
# "UseProjectCrs" - copy the current project's CRS
# "UseDefaultCrs" - use the default layer CRS set via QGIS options
projections\unknownCrsBehavior=NoAction
[core]
# Whether or not to anonymize newly created projects
# If set to 1, then project metadata items like AUTHOR and CREATION DATE

View File

@ -549,6 +549,22 @@ static QgsMessageOutput *messageOutputViewer_()
static void customSrsValidation_( QgsCoordinateReferenceSystem &srs )
{
const QgsOptions::UnknownLayerCrsBehavior mode = QgsSettings().enumValue( QStringLiteral( "/projections/unknownCrsBehavior" ), QgsOptions::UnknownLayerCrsBehavior::NoAction, QgsSettings::App );
switch ( mode )
{
case QgsOptions::UnknownLayerCrsBehavior::NoAction:
return;
case QgsOptions::UnknownLayerCrsBehavior::UseDefaultCrs:
srs.createFromOgcWmsCrs( QgsSettings().value( QStringLiteral( "Projections/layerDefaultCrs" ), GEO_EPSG_CRS_AUTHID ).toString() );
break;
case QgsOptions::UnknownLayerCrsBehavior::PromptUserForCrs:
case QgsOptions::UnknownLayerCrsBehavior::UseProjectCrs:
// can't take any action immediately for these -- we may be in a background thread
break;
}
if ( QThread::currentThread() != QApplication::instance()->thread() )
{
// Running in a background thread -- we can't queue this connection, because
@ -646,62 +662,73 @@ void QgisApp::onActiveLayerChanged( QgsMapLayer *layer )
/*
* This function contains forced validation of CRS used in QGIS.
* There are 3 options depending on the settings:
* There are 4 options depending on the settings:
* - ask for CRS using projection selecter
* - use project's CRS
* - use predefined global CRS
* - take no action (leave as unknown CRS)
*/
void QgisApp::validateCrs( QgsCoordinateReferenceSystem &srs )
{
static QString sAuthId = QString();
QgsSettings mySettings;
QString myDefaultProjectionOption = mySettings.value( QStringLiteral( "Projections/defaultBehavior" ), "prompt" ).toString();
if ( myDefaultProjectionOption == QLatin1String( "prompt" ) )
const QgsOptions::UnknownLayerCrsBehavior mode = QgsSettings().enumValue( QStringLiteral( "/projections/unknownCrsBehavior" ), QgsOptions::UnknownLayerCrsBehavior::NoAction, QgsSettings::App );
switch ( mode )
{
// \note this class is not a descendent of QWidget so we can't pass
// it in the ctor of the layer projection selector
case QgsOptions::UnknownLayerCrsBehavior::NoAction:
break;
QgsProjectionSelectionDialog *mySelector = new QgsProjectionSelectionDialog();
mySelector->setMessage( srs.validationHint() ); //shows a generic message, if not specified
if ( sAuthId.isNull() )
sAuthId = QgsProject::instance()->crs().authid();
QgsCoordinateReferenceSystem defaultCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( sAuthId );
if ( defaultCrs.isValid() )
case QgsOptions::UnknownLayerCrsBehavior::UseDefaultCrs:
{
mySelector->setCrs( defaultCrs );
}
bool waiting = QApplication::overrideCursor() && QApplication::overrideCursor()->shape() == Qt::WaitCursor;
if ( waiting )
QApplication::setOverrideCursor( Qt::ArrowCursor );
if ( mySelector->exec() )
{
QgsDebugMsg( "Layer srs set from dialog: " + QString::number( mySelector->crs().srsid() ) );
srs = mySelector->crs();
srs.createFromOgcWmsCrs( QgsSettings().value( QStringLiteral( "Projections/layerDefaultCrs" ), GEO_EPSG_CRS_AUTHID ).toString() );
sAuthId = srs.authid();
visibleMessageBar()->pushMessage( tr( "CRS was undefined" ), tr( "defaulting to CRS %1 - %2" ).arg( sAuthId, srs.description() ), Qgis::Warning, messageTimeout() );
break;
}
if ( waiting )
QApplication::restoreOverrideCursor();
case QgsOptions::UnknownLayerCrsBehavior::PromptUserForCrs:
{
// \note this class is not a descendent of QWidget so we can't pass
// it in the ctor of the layer projection selector
delete mySelector;
}
else if ( myDefaultProjectionOption == QLatin1String( "useProject" ) )
{
// XXX TODO: Change project to store selected CS as 'projectCRS' not 'selectedWkt'
sAuthId = QgsProject::instance()->crs().authid();
srs.createFromOgcWmsCrs( sAuthId );
QgsDebugMsg( "Layer srs set from project: " + sAuthId );
visibleMessageBar()->pushMessage( tr( "CRS was undefined" ), tr( "defaulting to project CRS %1 - %2" ).arg( sAuthId, srs.description() ), Qgis::Warning, messageTimeout() );
}
else ///Projections/defaultBehavior==useGlobal
{
sAuthId = mySettings.value( QStringLiteral( "Projections/layerDefaultCrs" ), GEO_EPSG_CRS_AUTHID ).toString();
srs.createFromOgcWmsCrs( sAuthId );
QgsDebugMsg( "Layer srs set from default: " + sAuthId );
visibleMessageBar()->pushMessage( tr( "CRS was undefined" ), tr( "defaulting to CRS %1 - %2" ).arg( sAuthId, srs.description() ), Qgis::Warning, messageTimeout() );
QgsProjectionSelectionDialog *mySelector = new QgsProjectionSelectionDialog();
mySelector->setMessage( srs.validationHint() ); //shows a generic message, if not specified
if ( sAuthId.isNull() )
sAuthId = QgsProject::instance()->crs().authid();
QgsCoordinateReferenceSystem defaultCrs( sAuthId );
if ( defaultCrs.isValid() )
{
mySelector->setCrs( defaultCrs );
}
bool waiting = QApplication::overrideCursor() && QApplication::overrideCursor()->shape() == Qt::WaitCursor;
if ( waiting )
QApplication::setOverrideCursor( Qt::ArrowCursor );
if ( mySelector->exec() )
{
QgsDebugMsg( "Layer srs set from dialog: " + QString::number( mySelector->crs().srsid() ) );
srs = mySelector->crs();
sAuthId = srs.authid();
}
if ( waiting )
QApplication::restoreOverrideCursor();
delete mySelector;
break;
}
case QgsOptions::UnknownLayerCrsBehavior::UseProjectCrs:
{
// XXX TODO: Change project to store selected CS as 'projectCRS' not 'selectedWkt'
srs = QgsProject::instance()->crs();
sAuthId = srs.authid();
QgsDebugMsg( "Layer srs set from project: " + sAuthId );
visibleMessageBar()->pushMessage( tr( "CRS was undefined" ), tr( "defaulting to project CRS %1 - %2" ).arg( sAuthId, srs.description() ), Qgis::Warning, messageTimeout() );
break;
}
}
}

View File

@ -453,18 +453,23 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl, const QList<QgsOpti
mLogCanvasRefreshChkBx->setChecked( mSettings->value( QStringLiteral( "/Map/logCanvasRefreshEvent" ), false ).toBool() );
//set the default projection behavior radio buttons
if ( mSettings->value( QStringLiteral( "/Projections/defaultBehavior" ), "prompt" ).toString() == QLatin1String( "prompt" ) )
const QgsOptions::UnknownLayerCrsBehavior mode = QgsSettings().enumValue( QStringLiteral( "/projections/unknownCrsBehavior" ), QgsOptions::UnknownLayerCrsBehavior::NoAction, QgsSettings::App );
switch ( mode )
{
radPromptForProjection->setChecked( true );
}
else if ( mSettings->value( QStringLiteral( "/Projections/defaultBehavior" ), "prompt" ).toString() == QLatin1String( "useProject" ) )
{
radUseProjectProjection->setChecked( true );
}
else //useGlobal
{
radUseGlobalProjection->setChecked( true );
case NoAction:
radCrsNoAction->setChecked( true );
break;
case PromptUserForCrs:
radPromptForProjection->setChecked( true );
break;
case UseProjectCrs:
radUseProjectProjection->setChecked( true );
break;
case UseDefaultCrs:
radUseGlobalProjection->setChecked( true );
break;
}
QString myLayerDefaultCrs = mSettings->value( QStringLiteral( "/Projections/layerDefaultCrs" ), GEO_EPSG_CRS_AUTHID ).toString();
mLayerDefaultCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( myLayerDefaultCrs );
leLayerGlobalCrs->setCrs( mLayerDefaultCrs );
@ -1570,15 +1575,19 @@ void QgsOptions::saveOptions()
//projection defined...
if ( radPromptForProjection->isChecked() )
{
mSettings->setValue( QStringLiteral( "/Projections/defaultBehavior" ), "prompt" );
mSettings->setEnumValue( QStringLiteral( "/projections/unknownCrsBehavior" ), QgsOptions::UnknownLayerCrsBehavior::PromptUserForCrs, QgsSettings::App );
}
else if ( radUseProjectProjection->isChecked() )
{
mSettings->setValue( QStringLiteral( "/Projections/defaultBehavior" ), "useProject" );
mSettings->setEnumValue( QStringLiteral( "/projections/unknownCrsBehavior" ), QgsOptions::UnknownLayerCrsBehavior::UseProjectCrs, QgsSettings::App );
}
else //assumes radUseGlobalProjection is checked
else if ( radCrsNoAction->isChecked() )
{
mSettings->setValue( QStringLiteral( "/Projections/defaultBehavior" ), "useGlobal" );
mSettings->setEnumValue( QStringLiteral( "/projections/unknownCrsBehavior" ), QgsOptions::UnknownLayerCrsBehavior::NoAction, QgsSettings::App );
}
else
{
mSettings->setEnumValue( QStringLiteral( "/projections/unknownCrsBehavior" ), QgsOptions::UnknownLayerCrsBehavior::UseDefaultCrs, QgsSettings::App );
}
mSettings->setValue( QStringLiteral( "/Projections/layerDefaultCrs" ), mLayerDefaultCrs.authid() );

View File

@ -43,6 +43,20 @@ class APP_EXPORT QgsOptions : public QgsOptionsDialogBase, private Ui::QgsOption
Q_OBJECT
public:
/**
* Behavior to use when encountering a layer with an unknown CRS
* \since QGIS 3.10
*/
enum UnknownLayerCrsBehavior
{
NoAction = 0, //!< Take no action and leave as unknown CRS
PromptUserForCrs = 1, //!< User is prompted for a CRS choice
UseProjectCrs = 2, //!< Copy the current project's CRS
UseDefaultCrs = 3, //!< Use the default layer CRS set via QGIS options
};
Q_ENUM( UnknownLayerCrsBehavior )
/**
* Constructor
* \param parent Parent widget (usually a QgisApp)

View File

@ -1614,15 +1614,55 @@
<rect>
<x>0</x>
<y>0</y>
<width>561</width>
<height>421</height>
<width>857</width>
<height>827</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_15">
<item row="4" column="0">
<widget class="QgsCollapsibleGroupBox" name="mDefaultDatumTransformGroupBox">
<property name="title">
<string>Default Datum Transformations</string>
</property>
<layout class="QGridLayout" name="gridLayout_10">
<item row="2" column="0">
<widget class="QLabel" name="label_40">
<property name="text">
<string>Enter default datum transformations which will be used in any newly created project</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="mShowDatumTransformDialogCheckBox">
<property name="text">
<string>Ask for datum transformation if several are available</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QgsDatumTransformTableWidget" name="mDefaultDatumTransformTableWidget" native="true"/>
</item>
</layout>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="mPlanimetricMeasurementsComboBox">
<property name="text">
<string>Planimetric measurements</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QgsCollapsibleGroupBox" name="grpProjectProjection">
<property name="title">
<string>CRS for New Projects</string>
<string>CRS for Projects</string>
</property>
<layout class="QGridLayout" name="gridLayout_27" columnstretch="0,1">
<item row="0" column="0" colspan="2">
@ -1671,26 +1711,23 @@
</layout>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0">
<widget class="QgsCollapsibleGroupBox" name="grpProjectionBehavior">
<property name="title">
<string>CRS for New Layers</string>
<string>CRS for Layers</string>
</property>
<layout class="QGridLayout" name="gridLayout_14" columnstretch="0,1">
<item row="0" column="0" colspan="2">
<item row="0" column="1">
<widget class="QgsProjectionSelectionWidget" name="leLayerGlobalCrs" native="true">
<property name="enabled">
<bool>true</bool>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_8">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
@ -1706,7 +1743,28 @@
</property>
</widget>
</item>
<item row="1" column="0">
<item row="4" column="0" colspan="2">
<widget class="QRadioButton" name="radUseProjectProjection">
<property name="text">
<string>Use pro&amp;ject CRS</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Default CRS for layers</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QRadioButton" name="radUseGlobalProjection">
<property name="text">
<string>&amp;Use default layer CRS</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QRadioButton" name="radPromptForProjection">
<property name="text">
<string>Pro&amp;mpt for CRS</string>
@ -1714,71 +1772,27 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="radUseProjectProjection">
<widget class="QRadioButton" name="radCrsNoAction">
<property name="text">
<string>Use pro&amp;ject CRS</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QRadioButton" name="radUseGlobalProjection">
<property name="text">
<string>&amp;Use a default CRS</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QgsProjectionSelectionWidget" name="leLayerGlobalCrs" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<string>Leave as an unknown CRS (take no action)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0">
<widget class="QgsCollapsibleGroupBox" name="mDefaultDatumTransformGroupBox">
<property name="title">
<string>Default Datum Transformations</string>
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<layout class="QGridLayout" name="gridLayout_10">
<item row="2" column="0">
<widget class="QLabel" name="label_40">
<property name="text">
<string>Enter default datum transformations which will be used in any newly created project</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="mShowDatumTransformDialogCheckBox">
<property name="text">
<string>Ask for datum transformation if several are available</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QgsDatumTransformTableWidget" name="mDefaultDatumTransformTableWidget" native="true"/>
</item>
</layout>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="mPlanimetricMeasurementsComboBox">
<property name="text">
<string>Planimetric measurements</string>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</widget>
</spacer>
</item>
</layout>
</widget>
@ -5631,11 +5645,13 @@ p, li { white-space: pre-wrap; }
<tabstop>radProjectUseCrsOfFirstLayer</tabstop>
<tabstop>radProjectUseDefaultCrs</tabstop>
<tabstop>leProjectGlobalCrs</tabstop>
<tabstop>leLayerGlobalCrs</tabstop>
<tabstop>radCrsNoAction</tabstop>
<tabstop>radPromptForProjection</tabstop>
<tabstop>radUseProjectProjection</tabstop>
<tabstop>radUseGlobalProjection</tabstop>
<tabstop>leLayerGlobalCrs</tabstop>
<tabstop>mShowDatumTransformDialogCheckBox</tabstop>
<tabstop>mPlanimetricMeasurementsComboBox</tabstop>
<tabstop>mOptionsScrollArea_11</tabstop>
<tabstop>cbxAttributeTableDocked</tabstop>
<tabstop>mComboCopyFeatureFormat</tabstop>
@ -5820,22 +5836,6 @@ p, li { white-space: pre-wrap; }
</hint>
</hints>
</connection>
<connection>
<sender>radUseGlobalProjection</sender>
<signal>toggled(bool)</signal>
<receiver>leLayerGlobalCrs</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>290</x>
<y>295</y>
</hint>
<hint type="destinationlabel">
<x>367</x>
<y>315</y>
</hint>
</hints>
</connection>
<connection>
<sender>mAdvancedSettingsEnableButton</sender>
<signal>clicked()</signal>