From cb4af9144cfae79140a905db83810e98829fdbf3 Mon Sep 17 00:00:00 2001 From: Peter Petrik Date: Thu, 5 Oct 2023 15:36:09 +0200 Subject: [PATCH] add missing CRS to project load errors (#2813) * add missing CRS to project load errors * fix std::bad_function in tests --- app/activeproject.cpp | 60 ++++++++---- app/activeproject.h | 9 +- app/qml/ProjectIssuesPanel.qml | 88 ++++++++---------- app/qml/main.qml | 4 +- app/test/testactiveproject.cpp | 33 +++++++ app/test/testactiveproject.h | 1 + .../Survey_points_no_crs.gpkg | Bin 0 -> 98304 bytes .../bad_layer.qgz | Bin 0 -> 11521 bytes .../data.csv | 2 + 9 files changed, 128 insertions(+), 69 deletions(-) create mode 100644 test/test_data/project-with-missing-layer-and-invalid-crs/Survey_points_no_crs.gpkg create mode 100644 test/test_data/project-with-missing-layer-and-invalid-crs/bad_layer.qgz create mode 100644 test/test_data/project-with-missing-layer-and-invalid-crs/data.csv diff --git a/app/activeproject.cpp b/app/activeproject.cpp index d1be7b03c..acfb53093 100644 --- a/app/activeproject.cpp +++ b/app/activeproject.cpp @@ -194,30 +194,15 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) emit positionTrackingSupportedChanged(); } - bool foundInvalidLayer = false; - QStringList invalidLayers; - QMap projectLayers = mQgsProject->mapLayers(); - - for ( QgsMapLayer *layer : projectLayers ) - { - if ( !layer->isValid() ) - { - invalidLayers.append( layer->name() ); - foundInvalidLayer = true; - emit reportIssue( layer->name(), layer->publicSource() ); - } - } + bool foundErrorsInLoadedProject = validateProject(); flagFile.remove(); if ( !force ) { emit loadingFinished(); - if ( foundInvalidLayer ) + if ( foundErrorsInLoadedProject ) { - QString message = QStringLiteral( "WARNING: The following layers are invalid: %1" ).arg( invalidLayers.join( ", " ) ); - CoreUtils::log( "project loading", message ); - QFile file( logFilePath ); if ( file.open( QIODevice::ReadOnly ) ) { @@ -263,6 +248,47 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) return res; } +bool ActiveProject::validateProject() +{ + Q_ASSERT( mQgsProject ); + + bool errorsFound = false; + + // A. Per project validations + // A.1. Project CRS + if ( !mQgsProject->crs().isValid() ) + { + errorsFound = true; + CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Invalid canvas CRS" ) ); + emit reportIssue( tr( "General" ), tr( "Project has invalid CRS assigned. Map and tools have undefined behaviour!" ) ); + } + + // B. Per-Layer validations + QMap projectLayers = mQgsProject->mapLayers(); + for ( QgsMapLayer *layer : projectLayers ) + { + // B.1. Layer Validity + if ( !layer->isValid() ) + { + errorsFound = true; + CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Invalid layer %1" ).arg( layer->name() ) ); + emit reportIssue( tr( "Layer" ) + ": " + layer->name(), tr( "Unable to load source " ) + ": " + layer->publicSource() ); + } + else + { + // B.2. Layer CRS + if ( layer->isSpatial() && !layer->crs().isValid() ) + { + errorsFound = true; + CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Invalid layer CRS %1" ).arg( layer->name() ) ); + emit reportIssue( tr( "Layer" ) + ": " + layer->name(), tr( "Layer has invalid CRS assigned. Recording tools have undefined behaviour." ) ); + } + } + } + + return errorsFound; +} + bool ActiveProject::reloadProject( QString projectDir ) { if ( mQgsProject->homePath() == projectDir ) diff --git a/app/activeproject.h b/app/activeproject.h index 58ea4981d..b775303f6 100644 --- a/app/activeproject.h +++ b/app/activeproject.h @@ -128,7 +128,7 @@ class ActiveProject: public QObject void loadingFinished(); void projectReadingFailed( QString error ); - void reportIssue( QString layerName, QString message ); + void reportIssue( QString title, QString message ); void loadingErrorFound(); void qgisLogChanged(); @@ -155,6 +155,13 @@ class ActiveProject: public QObject private: + /** + * Build up warning list from loaded project + * Emits reportIssue for each issue found + * Returns true if there were errors found + */ + bool validateProject(); + //! Tries to match current visible layers with some theme and if it fails, invalidates current map theme void updateMapTheme(); diff --git a/app/qml/ProjectIssuesPanel.qml b/app/qml/ProjectIssuesPanel.qml index f1e13defb..7183bb2a3 100644 --- a/app/qml/ProjectIssuesPanel.qml +++ b/app/qml/ProjectIssuesPanel.qml @@ -1,3 +1,4 @@ + /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -21,17 +22,16 @@ Item { property real rowHeight: InputStyle.rowHeight property var projectIssuesModel: ListModel {} property string projectLoadingLog: "" - property string headerText: qsTr( "The following layers failed loading" ) + ":" - function reportIssue( layerName, message ) { - projectIssuesModel.append( { name: layerName, message: message } ); + function reportIssue(title, message) { + projectIssuesModel.append( { title: title, message: message } ) } function clear() { - projectIssuesModel.clear(); + projectIssuesModel.clear() } - Keys.onReleased: function( event ) { + Keys.onReleased: function (event) { if (event.key === Qt.Key_Back || event.key === Qt.Key_Escape) { event.accepted = true @@ -80,22 +80,11 @@ Item { contentWidth: availableWidth // to only scroll vertically spacing: InputStyle.panelSpacing - background: Rectangle { - anchors.fill: parent - color: InputStyle.panelBackgroundLight - } - Column { id: settingListContent anchors.fill: parent spacing: 1 - PanelItem { - color: InputStyle.panelBackgroundLight - text: headerText - bold: true - } - PanelItem { id: invalidLayersList height: 0 @@ -108,37 +97,32 @@ Item { spacing: 3 delegate: PanelItem { anchors.margins: 5 - width: ListView.view.width + width: ListView.view.width height: row.height color: InputStyle.clrPanelMain - Row { + Column { id: row - width: parent.width - anchors.left: parent.left - anchors.top: parent.top - Text { - id: nameTextItem - padding: 5 - font.pixelSize: InputStyle.fontPixelSizeBig - text: qsTr( name ) - wrapMode: Text.Wrap - } - - Rectangle { - id: seperator - width: 3 - height: parent.height - color: "gray" - } - - Text { - id: messageTextItem - width: parent.width - nameTextItem.width - seperator.width - padding: 5 - font.pixelSize: InputStyle.fontPixelSizeBig - text: qsTr( message ) - wrapMode: Text.Wrap - } + width: parent.width + anchors.left: parent.left + anchors.top: parent.top + Text { + id: nameTextItem + width: parent.width + padding: 5 + font.pixelSize: InputStyle.fontPixelSizeBig + text: title + color: InputStyle.fontColor + wrapMode: Text.Wrap + } + + Text { + id: messageTextItem + width: parent.width + padding: 10 + font.pixelSize: InputStyle.fontPixelSizeNormal + text: message + wrapMode: Text.Wrap + } } onHeightChanged: invalidLayersList.height += height; } @@ -150,12 +134,18 @@ Item { } } - // Debug/Logging PanelItem { - color: InputStyle.panelBackgroundLight - text: qsTr("QGIS log") - bold: true + height: qgisLogTextHeader.height + width: parent.width + Text { + id: qgisLogTextHeader + width: parent.width + padding: 5 + text: qsTr("QGIS log") + font.pixelSize: InputStyle.fontPixelSizeBig + color: InputStyle.fontColor + } } PanelItem { @@ -164,7 +154,7 @@ Item { Text { id: qgisLogTextItem width: parent.width - padding: 5 + padding: 10 text: projectLoadingLog wrapMode: Text.Wrap } diff --git a/app/qml/main.qml b/app/qml/main.qml index 190cff047..d8e112b61 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -806,8 +806,8 @@ ApplicationWindow { failedToLoadProjectBanner.pushNotificationMessage( qsTr( "There were issues loading the project." ) ) } - function onReportIssue( layerName, message ) { - projectIssuesPanel.reportIssue( layerName, message ) + function onReportIssue( title, message ) { + projectIssuesPanel.reportIssue( title, message ) } function onProjectReloaded( project ) { diff --git a/app/test/testactiveproject.cpp b/app/test/testactiveproject.cpp index 74119fcde..1335de0c3 100644 --- a/app/test/testactiveproject.cpp +++ b/app/test/testactiveproject.cpp @@ -30,6 +30,30 @@ void TestActiveProject::cleanup() { } +void TestActiveProject::testProjectValidations() +{ + QString projectDir = TestUtils::testDataDir() + "/project-with-missing-layer-and-invalid-crs"; + QString projectFilename = "bad_layer.qgz"; + + AppSettings as; + ActiveLayer al; + LayersModel lm; + LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection ); + ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() ); + + QSignalSpy spyReportIssues( &activeProject, &ActiveProject::reportIssue ); + QSignalSpy spyErrorsFound( &activeProject, &ActiveProject::loadingErrorFound ); + + mApi->localProjectsManager().addLocalProject( projectDir, projectFilename ); + QVERIFY( activeProject.load( projectDir + "/" + projectFilename ) ); + + QCOMPARE( spyErrorsFound.count(), 1 ); + QCOMPARE( spyReportIssues.count(), 3 ); // invalid project CRS, invalid layer CRS, missing layer Survey + + const QString id = mApi->localProjectsManager().projectId( projectDir + "/" + projectFilename ); + mApi->localProjectsManager().removeLocalProject( id ); +} + void TestActiveProject::testProjectLoadFailure() { QString projectname = QStringLiteral( "testProjectLoadFailure" ); @@ -50,6 +74,9 @@ void TestActiveProject::testProjectLoadFailure() QVERIFY( !activeProject.load( projectdir + "/" + projectfilename ) ); QVERIFY( !activeProject.localProject().isValid() ); QVERIFY( spy.count() ); + + const QString id = mApi->localProjectsManager().projectId( projectdir + "/" + projectdir ); + mApi->localProjectsManager().removeLocalProject( id ); } void TestActiveProject::testPositionTrackingFlag() @@ -76,6 +103,9 @@ void TestActiveProject::testPositionTrackingFlag() QCOMPARE( spy.count(), 1 ); QCOMPARE( activeProject.positionTrackingSupported(), false ); + QString id = mApi->localProjectsManager().projectId( projectDir + "/" + projectName ); + mApi->localProjectsManager().removeLocalProject( id ); + // project "tracking" - tracking enabled projectDir = TestUtils::testDataDir() + "/tracking/"; projectName = "tracking-project.qgz"; @@ -86,4 +116,7 @@ void TestActiveProject::testPositionTrackingFlag() QCOMPARE( spy.count(), 2 ); QCOMPARE( activeProject.positionTrackingSupported(), true ); + + id = mApi->localProjectsManager().projectId( projectDir + "/" + projectName ); + mApi->localProjectsManager().removeLocalProject( id ); } diff --git a/app/test/testactiveproject.h b/app/test/testactiveproject.h index e74790736..37f6f27d1 100644 --- a/app/test/testactiveproject.h +++ b/app/test/testactiveproject.h @@ -24,6 +24,7 @@ class TestActiveProject : public QObject void init(); void cleanup(); + void testProjectValidations(); void testProjectLoadFailure(); void testPositionTrackingFlag(); diff --git a/test/test_data/project-with-missing-layer-and-invalid-crs/Survey_points_no_crs.gpkg b/test/test_data/project-with-missing-layer-and-invalid-crs/Survey_points_no_crs.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..7157ff52089c41ef8b64f50edc2fb55ecac798bf GIT binary patch literal 98304 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 oSZ&9E76_pN1x>Sv4mO1cZS67YJks2nZuX6MY*) z4^t;5M|0;(pG&6=-o%GbXKbDKbRHFSM&EFZl!WeDGQL>1H;zo2!A`5BRzvm>wc(5uMW8U01-t(h%!|^eJ_>W_O0V8` zc)!kisNKKsS&K;pH?Nj>Gy1K+03h~9S^MRIG%t0FcfF_gla=}V`L%0Q-?K&xXV5D> zhgjOwiEk07H>g<~9h`m@)ft!M~ zyCor*jJA^I{zr)yW2=4@{##zEN2f#f`v_$!=8pbn-lxw7_mdRbi;DeEW+`s$qZ`zw zqG4q*K9>`b-eDoFNQF)RbW(PubPt>*Hw)MyhjMJ}h>p8uQmz2BIhF$KZoHQ-f})}j zYHLHcrxcnnfrzMXVBqmAT=KI60jE3Xn7bc=D8UVg8``p&G{$eF;)}x!Z~P162c=xB z3LwG+U4FM76xlO$ii0c*$~FGnF@2!b<}yVhDZOr=)LM%5{*|4@8!Ldyzab5%r5N_G zFL&EddS2qB?&}VMcZ6KG$5!UK{>5{|`qATfl;FA zf`p<*YBX5u0T&xcOS$xmmfI$&%J!Hz2q%eMS zKG+FO^qz}GyMaPv-6~c#vP;yq#lM4LBd+bDPMM`1X`>{SyJ&Z*QS;YQ!|a1iKuSde z&Ul_!>Jn}NwrbaH_b~rnaXXx-Jxo{DAVZ^NeDnx3!GrqL&0lY=%8RPL<9cyX$jdC{ zyHwH655gW_fdkYzQ3U5Td|2mElu?+RL_UIc1Y6iMH%_Gayb}yb#nId~(q_>YG)@P- z%tc27KbW(^O{~LUG@tbS0d#jnVpzM{kd*6;{;l57NuI4R=3er~XJXthJ`(VXAfZRi zfoT4&<_crzS8g&4nMMOsmO*P8+zV#Qz6D_j^Yn&mPbO2^{sI3CAIJqTL_pHZd zR~B5Pnqe&#yDR}wwMoMBj?%ad%td^H%*q(KW-)B>Mbn%!mKRiK$RP&8r{GUDC>6p? zj-W-l*qIj*0>(GK4#HW14dkHu^B6fHw2y0{50$a*RYy%jHhYeA)0p+n4AAx)pEa+w zM}g1SyEm&ARb^$SOLc4KavLMTNL$%Scl{a%dnLG>%5njbAk==`Yy?C|d>=uXp*&0I z=81*3nwng{{U;!bsG0m!S|b5zf0*UQNPY{wG}PRrhn)D@ydTH=>)NJJ2O&|f`fuRm zGIYyV0S2MQuvx&yR$5{0fua7@ZOXXY1Gx`vFt!P*=QT|AJAe&z_m;H`68bEh@~j0vS$nRaw5w00qT9Lcny-ET1)GcIhUHl93_&NTW>p;n^r7wqmjf5*7#s#{7! znuHlx*EfsOaxK+6jBr$LLt1GaX`awcHu$illF3lSXs?o?xPKqS>1Ym0?HwxQNj?wWE*#@*#W;(#vil0j*98?HeTXbqgZftzm2 z_s-eqL%}HX5Yu$U*xppp^0M&!6nwug!v^LWtc&6FXCABw418Yr_V88Q5wT|&{?_#` z;*#GcE+5=h2C6n^n6`6poTfGHDC)0IcUL|MN%LCc< z_U-)e?Tr<<{&v@$;^gw@-HEr0%fk^EPMIH6A8d@UA!m^_VDZg=on}m+8`(-s*u1at z=N%Fou`Y1>?T*8f>GB3OMuEUo=&EvI#N)oE!%bbU;KqkwCdexw&`zkE zO<4MNQLd}2-YHj7c1`vwqiuwr?; zFKVC``Fr>Wo_*hVAMaqkyLH;Kh?WEPc(hC+8!}B*ax>|`PFp1O?lGQ~%0|~gB zwV2ajd%*0qYWbZY7D)MtrBGL9=XSW*WrZSxjj36g*&QdlK3#U6dgqdL+J59v<>YHd zO-XzKz{enbS%QV;rGRTfuKX4${WqR)8=ZKupS6uV-&vJM={ceVl<-WbTdV(_yqry8?U^rj-^2l*!pS#a0$) z@qoZUWN$yrlgmiGzy{rUBHfK7b}enrgw**nI$xelA`Y7Vr1-l9KxCV1nWp*iR!k@ z)RbppYe2e$HM2~`Ym0QIQkKTBFd9(#0p9#ETcq1xhuwEECHyaUMqIhBRZAgt(+)(T zBg_xhtQ7xpbctU|35OwCR7f_uu-{Seno^|GTR7uyDQtq+DFjazIUQB(RhIB}k9!Yv zMT84Qm)zUipa*7qadcFqO~4jLp)9fV65cI7;A^A7Rbm5SACmsWvXrF zxjSma!WYnE5s;-rVTm>n1dw!Q$V*d@iqluu*D~yYI;vGC#2WZHmA)B^k`Wpug_2;fScxVY z8B{5F2<{$7iS2_?$KdqrQWoaQWIJWykVUs4o5Unk(s7_hdrC5Dl9jagXK6iMT9)jr z-&>aL{~i4J-ybL~+u}~!&YL%p?>BWhw#v>r_$)fL&pT7_?fXG`duOs-^57f63ohZ< z^iS7z5Y2B0HU*ZhYIpO_ybQ!;$iwkr0LXufG!zWm^lR>cri8%4U zu;j{X{bQL$2?M)uzZhLOA04tX=H{}gG)ByC(-L1lFAz4Nnrr%m3q2B$4Lx9+v>C`h@$JIU%0AO-FzQVw#&uJbby*T{w1v>I-ho#dK7O@y%! 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