From db21851bbb16899d73697d12ded531a535134283 Mon Sep 17 00:00:00 2001 From: Vincent Cloarec Date: Thu, 1 Jul 2021 02:18:18 -0400 Subject: [PATCH] [mesh] mesh frame editing part-2 - start, commit, roll back and cancel (#43982) [mesh] allows the user of mesh layers to start editing, save, roll back or cancel editing, in the same way as vector layers (editing part-2) --- .../mesh/qgsmeshdataprovider.sip.in | 30 ++ .../auto_generated/mesh/qgsmesheditor.sip.in | 10 + .../auto_generated/mesh/qgsmeshlayer.sip.in | 45 ++- python/core/auto_generated/qgsmaplayer.sip.in | 45 +++ .../vector/qgsvectorlayer.sip.in | 19 +- src/app/qgisapp.cpp | 282 +++++++++++++++--- src/app/qgisapp.h | 37 +++ src/core/layertree/qgslayertreemodel.cpp | 22 +- src/core/layertree/qgslayertreeutils.cpp | 15 +- src/core/mesh/qgsmeshdataprovider.cpp | 2 + src/core/mesh/qgsmeshdataprovider.h | 30 ++ src/core/mesh/qgsmeshdatasetgroupstore.cpp | 4 +- src/core/mesh/qgsmesheditor.cpp | 15 +- src/core/mesh/qgsmesheditor.h | 6 + src/core/mesh/qgsmeshlayer.cpp | 131 +++++++- src/core/mesh/qgsmeshlayer.h | 39 ++- src/core/mesh/qgstriangularmesh.cpp | 24 +- src/core/mesh/qgstriangularmesh.h | 8 + .../meshmemory/qgsmeshmemorydataprovider.cpp | 8 + .../meshmemory/qgsmeshmemorydataprovider.h | 4 + src/core/qgsmaplayer.cpp | 10 + src/core/qgsmaplayer.h | 33 ++ src/core/qgsmaplayerlegend.cpp | 10 +- src/core/vector/qgsvectorlayer.cpp | 2 +- src/core/vector/qgsvectorlayer.h | 13 +- .../qgsmeshrendereractivedatasetwidget.cpp | 6 +- .../qgsmeshrendererscalarsettingswidget.cpp | 9 +- .../mesh/qgsrenderermeshpropertieswidget.cpp | 6 +- src/providers/mdal/qgsmdalprovider.cpp | 46 ++- src/providers/mdal/qgsmdalprovider.h | 7 + tests/src/core/testqgsmesheditor.cpp | 178 ++++++++--- tests/src/core/testqgsmeshlayer.cpp | 3 + tests/testdata/mesh/quad_flower_to_edit.2dm | 14 + .../mesh/quad_flower_to_edit_expected.2dm | 11 + 34 files changed, 955 insertions(+), 169 deletions(-) create mode 100644 tests/testdata/mesh/quad_flower_to_edit.2dm create mode 100644 tests/testdata/mesh/quad_flower_to_edit_expected.2dm diff --git a/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in b/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in index af8f27de6b46..9e4b9a3f7b29 100644 --- a/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in @@ -147,6 +147,18 @@ Returns number of edges in the native mesh Populates the mesh vertices, edges and faces .. versionadded:: 3.6 +%End + + virtual bool saveMeshFrame( const QgsMesh &mesh ) = 0; +%Docstring +Saves the ``mesh`` frame to the source. + +:param mesh: the mesh to save + +:return: ``True`` on success + + +.. versionadded:: 3.22 %End }; @@ -412,6 +424,24 @@ Sets the temporal unit of the provider and reload data if it changes. .. versionadded:: 3.14 %End + + virtual QgsMeshDriverMetadata driverMetadata() const; +%Docstring +Returns the mesh driver metadata of the provider + +:return: the mesh driver metadata of the provider + +.. versionadded:: 3.22 +%End + + + virtual void close() = 0; +%Docstring +Closes the data provider and free every resources used + +.. versionadded:: 3.22 +%End + signals: void datasetGroupsAdded( int count ); %Docstring diff --git a/python/core/auto_generated/mesh/qgsmesheditor.sip.in b/python/core/auto_generated/mesh/qgsmesheditor.sip.in index 3b24c99ffc88..e9f09c94e605 100644 --- a/python/core/auto_generated/mesh/qgsmesheditor.sip.in +++ b/python/core/auto_generated/mesh/qgsmesheditor.sip.in @@ -98,6 +98,16 @@ If removing these vertices leads to a topological errors, the method will return void stopEditing(); %Docstring Stops editing +%End + + QgsRectangle extent() const; +%Docstring +Returns the extent of the edited mesh +%End + + bool isModified() const; +%Docstring +Returns whether the mesh has been modified %End signals: diff --git a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in index 35ba1230f651..a29f9d0a6677 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in @@ -157,6 +157,10 @@ QgsMeshLayer cannot be copied. virtual QString htmlMetadata() const; + virtual bool isEditable() const; + + virtual bool supportsEditing() const; + QString providerType() const; %Docstring @@ -713,6 +717,28 @@ Returns the relative time (in milliseconds) of the dataset from the reference ti Starts edition of the mesh frame. Coordinate ``transform`` used to initialize the triangular mesh if needed. This operation will disconnect the mesh layer from the data provider anf removes all existing dataset group +.. versionadded:: 3.22 +%End + + bool commitFrameEditing( const QgsCoordinateTransform &transform, bool continueEditing = true ); +%Docstring +Commits edition of the mesh frame, +Rebuilds the triangular mesh and its spatial index with ``transform``, +Continue editing with the same mesh editor if ``continueEditing`` is True + +:return: ``True`` if the commit succeeds + +.. versionadded:: 3.22 +%End + + bool rollBackFrameEditing( const QgsCoordinateTransform &transform, bool continueEditing = true ); +%Docstring +Rolls Back edition of the mesh frame. +Reload mesh from file, rebuilds the triangular mesh and its spatial index with ``transform``, +Continue editing with the same mesh editor if ``continueEditing`` is ``True`` + +:return: ``True`` if the rollback succeeds + .. versionadded:: 3.22 %End @@ -732,14 +758,29 @@ Returns a pointer to the mesh editor own by the mesh layer .. versionadded:: 3.22 %End - int meshVerticesCount() const; + virtual bool isModified() const; + +%Docstring +Returns whether the mesh frame has been modified since the last save + +.. versionadded:: 3.22 +%End + + bool contains( const QgsMesh::ElementType &type ) const; +%Docstring +Returns whether the mesh contains at mesh elements of given type + +.. versionadded:: 3.22 +%End + + int meshVertexCount() const; %Docstring Returns the vertices count of the mesh frame .. versionadded:: 3.22 %End - int meshFacesCount() const; + int meshFaceCount() const; %Docstring Returns the faces count of the mesh frame diff --git a/python/core/auto_generated/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index 3defad792e9d..42c7685244b7 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -550,11 +550,35 @@ Set the visibility of the given sublayer name. :param name: sublayer name :param visible: sublayer visibility +%End + + virtual bool supportsEditing() const; +%Docstring +Returns whether the layer supports editing or not. + +:return: ``False`` if the layer is read only or the data provider has no editing capabilities. + +.. note:: + + default implementation returns ``False``. + +.. versionadded:: 3.22 %End virtual bool isEditable() const; %Docstring Returns ``True`` if the layer can be edited. +%End + + virtual bool isModified() const; +%Docstring +Returns ``True`` if the layer has been modified since last commit/save. + +.. note:: + + default implementation returns ``False``. + +.. versionadded:: 3.22 %End virtual bool isSpatial() const; @@ -1724,6 +1748,27 @@ Emitted when the validity of this layer changed. Emitted when a custom property of the layer has been changed or removed. .. versionadded:: 3.18 +%End + + void editingStarted(); +%Docstring +Emitted when editing on this layer has started. + +.. versionadded:: 3.22 +%End + + void editingStopped(); +%Docstring +Emitted when edited changes have been successfully written to the data provider. + +.. versionadded:: 3.22 +%End + + void layerModified(); +%Docstring +Emitted when modifications has been done on layer + +.. versionadded:: 3.22 %End protected: diff --git a/python/core/auto_generated/vector/qgsvectorlayer.sip.in b/python/core/auto_generated/vector/qgsvectorlayer.sip.in index b9fe7a1eb677..21d7d664421c 100644 --- a/python/core/auto_generated/vector/qgsvectorlayer.sip.in +++ b/python/core/auto_generated/vector/qgsvectorlayer.sip.in @@ -1742,6 +1742,7 @@ Returns ``True`` if this is a geometry layer and ``False`` in case of NoGeometry %End virtual bool isModified() const; + %Docstring Returns ``True`` if the provider has been modified since the last commit %End @@ -1808,7 +1809,8 @@ Makes layer read-only (editing disabled) or not :return: ``False`` if the layer is in editing yet %End - bool supportsEditing(); + virtual bool supportsEditing() const; + %Docstring Returns whether the layer supports editing or not @@ -2727,11 +2729,6 @@ Emitted when selection was changed :param selected: Newly selected feature ids :param deselected: Ids of all features which have previously been selected but are not any more :param clearAndSelect: In case this is set to ``True``, the old selection was dismissed and the new selection corresponds to selected -%End - - void layerModified(); -%Docstring -Emitted when modifications has been done on layer %End void allowCommitChanged(); @@ -2749,16 +2746,6 @@ Emitted when the layer is checked for modifications. Use for last-minute additio void beforeEditingStarted(); %Docstring Emitted before editing on this layer is started. -%End - - void editingStarted(); -%Docstring -Emitted when editing on this layer has started. -%End - - void editingStopped(); -%Docstring -Emitted when edited changes have been successfully written to the data provider. %End void beforeCommitChanges( bool stopEditing ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 12eb7c37d878..e866e1f3fefb 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -11140,8 +11140,8 @@ void QgisApp::toggleMapTips( bool enabled ) void QgisApp::toggleEditing() { - QgsVectorLayer *currentLayer = qobject_cast( activeLayer() ); - if ( currentLayer ) + QgsMapLayer *currentLayer = activeLayer(); + if ( currentLayer && currentLayer->supportsEditing() ) { toggleEditing( currentLayer, true ); } @@ -11149,12 +11149,33 @@ void QgisApp::toggleEditing() { // active although there's no layer active!? mActionToggleEditing->setChecked( false ); + mActionToggleEditing->setEnabled( false ); + visibleMessageBar()->pushMessage( tr( "Start editing failed" ), + tr( "Layer cannot be edited" ), + Qgis::MessageLevel::Warning ); } } bool QgisApp::toggleEditing( QgsMapLayer *layer, bool allowCancel ) { - QgsVectorLayer *vlayer = qobject_cast( layer ); + switch ( layer->type() ) + { + case QgsMapLayerType::VectorLayer: + return toggleEditingVectorLayer( qobject_cast( layer ), allowCancel ); + case QgsMapLayerType::MeshLayer: + return toggleEditingMeshLayer( qobject_cast( layer ), allowCancel ); + case QgsMapLayerType::RasterLayer: + case QgsMapLayerType::PluginLayer: + case QgsMapLayerType::VectorTileLayer: + case QgsMapLayerType::AnnotationLayer: + case QgsMapLayerType::PointCloudLayer: + break; + } + return false; +} + +bool QgisApp::toggleEditingVectorLayer( QgsVectorLayer *vlayer, bool allowCancel ) +{ if ( !vlayer ) { return false; @@ -11298,12 +11319,97 @@ bool QgisApp::toggleEditing( QgsMapLayer *layer, bool allowCancel ) vlayer->triggerRepaint(); } - if ( !res && layer == activeLayer() ) + if ( !res && vlayer == activeLayer() ) { // while also called when layer sends editingStarted/editingStopped signals, // this ensures correct restoring of gui state if toggling was canceled // or layer commit/rollback functions failed - activateDeactivateLayerRelatedActions( layer ); + activateDeactivateLayerRelatedActions( vlayer ); + } + + return res; +} + +bool QgisApp::toggleEditingMeshLayer( QgsMeshLayer *mlayer, bool allowCancel ) +{ + if ( !mlayer ) + return false; + + if ( !mlayer->supportsEditing() ) + return false; //TODO: when adapted widget will be ready, ask the user if he want to create a new one based on this one + + bool res = false; + + QgsCoordinateTransform transform( mlayer->crs(), mMapCanvas->mapSettings().destinationCrs(), QgsProject::instance() ); + + if ( !mlayer->isEditable() ) + { + res = mlayer->startFrameEditing( transform ); + mActionToggleEditing->setChecked( res ); + + if ( !res ) + QgsMessageLog::logMessage( tr( "Unable to start mesh editing for layer \"%1\"" ).arg( mlayer->name() ) ); + } + else if ( mlayer->isModified() ) + { + QMessageBox::StandardButtons buttons = QMessageBox::Save | QMessageBox::Discard; + if ( allowCancel ) + buttons = buttons | QMessageBox::Cancel; + switch ( QMessageBox::question( nullptr, + tr( "Stop Editing" ), + tr( "Do you want to save the changes to layer %1?" ).arg( mlayer->name() ), + buttons ) ) + { + case QMessageBox::Cancel: + res = false; + break; + + case QMessageBox::Save: + { + QgsTemporaryCursorOverride waitCursor( Qt::WaitCursor ); + QgsCanvasRefreshBlocker refreshBlocker; + if ( !mlayer->commitFrameEditing( transform, false ) ) + { + res = false; + } + + mlayer->triggerRepaint(); + } + break; + case QMessageBox::Discard: + { + QgsTemporaryCursorOverride waitCursor( Qt::WaitCursor ); + QgsCanvasRefreshBlocker refreshBlocker; + if ( !mlayer->rollBackFrameEditing( transform, false ) ) + { + visibleMessageBar()->pushMessage( tr( "Error" ), + tr( "Problems during roll back" ), + Qgis::MessageLevel::Critical ); + res = false; + } + + mlayer->triggerRepaint(); + break; + } + + default: + break; + } + } + else //mesh layer not modified + { + QgsTemporaryCursorOverride waitCursor( Qt::WaitCursor ); + QgsCanvasRefreshBlocker refreshBlocker; + mlayer->rollBackFrameEditing( transform, false ); + mlayer->triggerRepaint(); + } + + if ( !res && mlayer == activeLayer() ) + { + // while also called when layer sends editingStarted/editingStopped signals, + // this ensures correct restoring of gui state if toggling was canceled + // or layer commit/rollback functions failed + activateDeactivateLayerRelatedActions( mlayer ); } return res; @@ -11315,6 +11421,26 @@ void QgisApp::saveActiveLayerEdits() } void QgisApp::saveEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerRepaint ) +{ + if ( !layer ) + return; + + switch ( layer->type() ) + { + case QgsMapLayerType::VectorLayer: + return saveVectorLayerEdits( layer, leaveEditable, triggerRepaint ); + case QgsMapLayerType::MeshLayer: + return saveMeshLayerEdits( layer, leaveEditable, triggerRepaint ); + case QgsMapLayerType::RasterLayer: + case QgsMapLayerType::PluginLayer: + case QgsMapLayerType::VectorTileLayer: + case QgsMapLayerType::AnnotationLayer: + case QgsMapLayerType::PointCloudLayer: + break; + } +} + +void QgisApp::saveVectorLayerEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerRepaint ) { QgsVectorLayer *vlayer = qobject_cast( layer ); if ( !vlayer || !vlayer->isEditable() || !vlayer->isModified() ) @@ -11335,7 +11461,46 @@ void QgisApp::saveEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerRep } } +void QgisApp::saveMeshLayerEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerRepaint ) +{ + QgsMeshLayer *mlayer = qobject_cast( layer ); + if ( !mlayer || !mlayer->isEditable() || !mlayer->isModified() ) + return; + + if ( mlayer == activeLayer() ) + mSaveRollbackInProgress = true; + + QgsCanvasRefreshBlocker refreshBlocker; + QgsCoordinateTransform transform( mlayer->crs(), mMapCanvas->mapSettings().destinationCrs(), QgsProject::instance() ); + mlayer->commitFrameEditing( transform, leaveEditable ); + + if ( triggerRepaint ) + { + mlayer->triggerRepaint(); + } +} + void QgisApp::cancelEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerRepaint ) +{ + if ( !layer ) + return; + + switch ( layer->type() ) + { + case QgsMapLayerType::VectorLayer: + return cancelVectorLayerEdits( layer, leaveEditable, triggerRepaint ); + case QgsMapLayerType::MeshLayer: + return cancelMeshLayerEdits( layer, leaveEditable, triggerRepaint ); + case QgsMapLayerType::RasterLayer: + case QgsMapLayerType::PluginLayer: + case QgsMapLayerType::VectorTileLayer: + case QgsMapLayerType::AnnotationLayer: + case QgsMapLayerType::PointCloudLayer: + break; + } +} + +void QgisApp::cancelVectorLayerEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerRepaint ) { QgsVectorLayer *vlayer = qobject_cast( layer ); if ( !vlayer || !vlayer->isEditable() ) @@ -11366,6 +11531,33 @@ void QgisApp::cancelEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerR } } +void QgisApp::cancelMeshLayerEdits( QgsMapLayer *layer, bool leaveEditable, bool triggerRepaint ) +{ + QgsMeshLayer *mlayer = qobject_cast( layer ); + if ( !mlayer || !mlayer->isEditable() ) + return; + + if ( mlayer == activeLayer() && leaveEditable ) + mSaveRollbackInProgress = true; + + QgsCanvasRefreshBlocker refreshBlocker; + QgsCoordinateTransform transform( mlayer->crs(), mMapCanvas->mapSettings().destinationCrs(), QgsProject::instance() ); + if ( !mlayer->rollBackFrameEditing( transform, leaveEditable ) ) + { + mSaveRollbackInProgress = false; + QMessageBox::warning( nullptr, + tr( "Error" ), + tr( "Could not %1 changes to layer %2" ) + .arg( leaveEditable ? tr( "rollback" ) : tr( "cancel" ), + mlayer->name() ) ); + } + + if ( triggerRepaint ) + { + mlayer->triggerRepaint(); + } +} + void QgisApp::saveEdits() { const auto constSelectedLayers = mLayerTreeView->selectedLayers(); @@ -11472,17 +11664,38 @@ bool QgisApp::verifyEditsActionDialog( const QString &act, const QString &upon ) void QgisApp::updateLayerModifiedActions() { bool enableSaveLayerEdits = false; - QgsVectorLayer *vlayer = qobject_cast( activeLayer() ); - if ( vlayer ) + + QgsMapLayer *currentLayer = activeLayer(); + if ( currentLayer ) { - QgsVectorDataProvider *dprovider = vlayer->dataProvider(); - if ( dprovider ) + switch ( currentLayer->type() ) { - enableSaveLayerEdits = ( dprovider->capabilities() & QgsVectorDataProvider::ChangeAttributeValues - && vlayer->isEditable() - && vlayer->isModified() ); + case QgsMapLayerType::VectorLayer: + { + QgsVectorLayer *vlayer = qobject_cast( currentLayer ); + if ( QgsVectorDataProvider *dprovider = vlayer->dataProvider() ) + { + enableSaveLayerEdits = ( dprovider->capabilities() & QgsVectorDataProvider::ChangeAttributeValues + && vlayer->isEditable() + && vlayer->isModified() ); + } + } + break; + case QgsMapLayerType::MeshLayer: + { + QgsMeshLayer *mlayer = qobject_cast( currentLayer ); + enableSaveLayerEdits = ( mlayer->isEditable() && mlayer->isModified() ); + } + break; + case QgsMapLayerType::RasterLayer: + case QgsMapLayerType::PluginLayer: + case QgsMapLayerType::VectorTileLayer: + case QgsMapLayerType::AnnotationLayer: + case QgsMapLayerType::PointCloudLayer: + break; } } + mActionSaveLayerEdits->setEnabled( enableSaveLayerEdits ); QList selectedLayerNodes = mLayerTreeView ? mLayerTreeView->selectedLayerNodes() : QList(); @@ -11507,15 +11720,12 @@ QList QgisApp::editableLayers( bool modified ) const const auto constFindLayers = mLayerTreeView->layerTreeModel()->rootGroup()->findLayers(); for ( QgsLayerTreeLayer *nodeLayer : constFindLayers ) { - if ( !nodeLayer->layer() ) - continue; - - QgsVectorLayer *vl = qobject_cast( nodeLayer->layer() ); - if ( !vl ) + QgsMapLayer *layer = nodeLayer->layer(); + if ( !layer ) continue; - if ( vl->isEditable() && ( !modified || vl->isModified() ) ) - editLayers << vl; + if ( layer->isEditable() && ( !modified || layer->isModified() ) ) + editLayers << layer; } return editLayers; } @@ -14406,38 +14616,30 @@ void QgisApp::layersWereAdded( const QList &layers ) const auto constLayers = layers; for ( QgsMapLayer *layer : constLayers ) { - QgsDataProvider *provider = nullptr; + connect( layer, &QgsMeshLayer::layerModified, this, &QgisApp::updateLayerModifiedActions ); + connect( layer, &QgsMeshLayer::editingStarted, this, &QgisApp::layerEditStateChanged ); + connect( layer, &QgsMeshLayer::editingStopped, this, &QgisApp::layerEditStateChanged ); - QgsVectorLayer *vlayer = qobject_cast( layer ); - if ( vlayer ) + if ( QgsVectorLayer *vlayer = qobject_cast( layer ) ) { // notify user about any font family substitution, but only when rendering labels (i.e. not when opening settings dialog) connect( vlayer, &QgsVectorLayer::labelingFontNotFound, this, &QgisApp::labelingFontNotFound ); - QgsVectorDataProvider *vProvider = vlayer->dataProvider(); // Do not check for layer editing capabilities because they may change // (for example when subsetString is added/removed) and signals need to // be in place in order to update the GUI - connect( vlayer, &QgsVectorLayer::layerModified, this, &QgisApp::updateLayerModifiedActions ); - connect( vlayer, &QgsVectorLayer::editingStarted, this, &QgisApp::layerEditStateChanged ); - connect( vlayer, &QgsVectorLayer::editingStopped, this, &QgisApp::layerEditStateChanged ); connect( vlayer, &QgsVectorLayer::readOnlyChanged, this, &QgisApp::layerEditStateChanged ); connect( vlayer, &QgsVectorLayer::raiseError, this, &QgisApp::onLayerError ); connect( vlayer, &QgsVectorLayer::styleLoaded, [this, vlayer]( QgsMapLayer::StyleCategories categories ) { vectorLayerStyleLoaded( vlayer, categories ); } ); - - provider = vProvider; } - QgsRasterLayer *rlayer = qobject_cast( layer ); - if ( rlayer ) + if ( QgsRasterLayer *rlayer = qobject_cast( layer ) ) { // connect up any request the raster may make to update the statusbar message connect( rlayer, &QgsRasterLayer::statusChanged, this, &QgisApp::showStatusMessage ); - - provider = rlayer->dataProvider(); } - if ( provider ) + if ( QgsDataProvider *provider = layer->dataProvider() ) { connect( provider, &QgsDataProvider::dataChanged, layer, [layer] { layer->triggerRepaint(); } ); connect( provider, &QgsDataProvider::dataChanged, this, [this] { refreshMapCanvas(); } ); @@ -15346,6 +15548,9 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer *layer ) } case QgsMapLayerType::MeshLayer: + { + QgsMeshLayer *mlayer = qobject_cast( layer ); + mActionLocalHistogramStretch->setEnabled( false ); mActionFullHistogramStretch->setEnabled( false ); mActionLocalCumulativeCutStretch->setEnabled( false ); @@ -15375,8 +15580,6 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer *layer ) mActionSelectByExpression->setEnabled( false ); mActionSelectByForm->setEnabled( false ); mActionOpenFieldCalc->setEnabled( false ); - mActionToggleEditing->setEnabled( false ); - mActionToggleEditing->setChecked( false ); mActionSaveLayerEdits->setEnabled( false ); mUndoDock->widget()->setEnabled( false ); mActionUndo->setEnabled( false ); @@ -15412,7 +15615,14 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer *layer ) mActionDiagramProperties->setEnabled( false ); mActionIdentify->setEnabled( true ); enableDigitizeTechniqueActions( false ); - break; + + bool canSupportEditing = mlayer->supportsEditing(); + bool isEditable = mlayer->isEditable(); + mActionToggleEditing->setEnabled( canSupportEditing ); + mActionToggleEditing->setChecked( canSupportEditing && isEditable ); + } + + break; case QgsMapLayerType::VectorTileLayer: mActionLocalHistogramStretch->setEnabled( false ); diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 3e686fa56e5d..6b4925beae67 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -2362,6 +2362,43 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow */ void pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features ); + /** + * starts/stops for a vector layer \a vlayer + */ + bool toggleEditingVectorLayer( QgsVectorLayer *vlayer, bool allowCancel = true ); + + /** + * Starts/stops for a mesh layer \a mlayer + */ + bool toggleEditingMeshLayer( QgsMeshLayer *vlayer, bool allowCancel = true ); + + /** + * Saves edits of a vector layer + * \param leaveEditable leave the layer in editing mode when done + * \param triggerRepaint send layer signal to repaint canvas when done + */ + void saveVectorLayerEdits( QgsMapLayer *layer, bool leaveEditable = true, bool triggerRepaint = true ); + + /** + * Saves edits of a mesh layer + * \param leaveEditable leave the layer in editing mode when done + * \param triggerRepaint send layer signal to repaint canvas when done + */ + void saveMeshLayerEdits( QgsMapLayer *layer, bool leaveEditable = true, bool triggerRepaint = true ); + + /** + * Cancels edits of a vector layer + * \param leaveEditable leave the layer in editing mode when done + * \param triggerRepaint send layer signal to repaint canvas when done + */ + void cancelVectorLayerEdits( QgsMapLayer *layer, bool leaveEditable = true, bool triggerRepaint = true ); + + /** + * Cancels edits of a mesh layer + * \param leaveEditable leave the layer in editing mode when done + * \param triggerRepaint send layer signal to repaint canvas when done + */ + void cancelMeshLayerEdits( QgsMapLayer *layer, bool leaveEditable = true, bool triggerRepaint = true ); QgisAppStyleSheet *mStyleSheetBuilder = nullptr; diff --git a/src/core/layertree/qgslayertreemodel.cpp b/src/core/layertree/qgslayertreemodel.cpp index b2392c87a907..b9c8c93d5fde 100644 --- a/src/core/layertree/qgslayertreemodel.cpp +++ b/src/core/layertree/qgslayertreemodel.cpp @@ -27,6 +27,7 @@ #include "qgsmaplayer.h" #include "qgsmaplayerlegend.h" #include "qgsmaplayerstylemanager.h" +#include "qgsmeshlayer.h" #include "qgspluginlayer.h" #include "qgsrasterlayer.h" #include "qgsrenderer.h" @@ -245,14 +246,13 @@ QVariant QgsLayerTreeModel::data( const QModelIndex &index, int role ) const icon = legendIconEmbeddedInParent( nodeLayer ); } - QgsVectorLayer *vlayer = qobject_cast( layer ); - if ( vlayer && vlayer->isEditable() && testFlag( UseTextFormatting ) ) + if ( layer->isEditable() && testFlag( UseTextFormatting ) ) { const int iconSize = scaleIconSize( 16 ); QPixmap pixmap( icon.pixmap( iconSize, iconSize ) ); QPainter painter( &pixmap ); - painter.drawPixmap( 0, 0, iconSize, iconSize, QgsApplication::getThemePixmap( vlayer->isModified() ? QStringLiteral( "/mIconEditableEdits.svg" ) : QStringLiteral( "/mActionToggleEditing.svg" ) ) ); + painter.drawPixmap( 0, 0, iconSize, iconSize, QgsApplication::getThemePixmap( layer->isModified() ? QStringLiteral( "/mIconEditableEdits.svg" ) : QStringLiteral( "/mActionToggleEditing.svg" ) ) ); painter.end(); icon = QIcon( pixmap ); @@ -930,16 +930,12 @@ void QgsLayerTreeModel::connectToLayer( QgsLayerTreeLayer *nodeLayer ) connect( layer, &QgsMapLayer::legendChanged, this, &QgsLayerTreeModel::layerLegendChanged, Qt::UniqueConnection ); connect( layer, &QgsMapLayer::flagsChanged, this, &QgsLayerTreeModel::layerFlagsChanged, Qt::UniqueConnection ); - if ( layer->type() == QgsMapLayerType::VectorLayer ) - { - // using unique connection because there may be temporarily more nodes for a layer than just one - // which would create multiple connections, however disconnect() would disconnect all multiple connections - // even if we wanted to disconnect just one connection in each call. - QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ); - connect( vl, &QgsVectorLayer::editingStarted, this, &QgsLayerTreeModel::layerNeedsUpdate, Qt::UniqueConnection ); - connect( vl, &QgsVectorLayer::editingStopped, this, &QgsLayerTreeModel::layerNeedsUpdate, Qt::UniqueConnection ); - connect( vl, &QgsVectorLayer::layerModified, this, &QgsLayerTreeModel::layerNeedsUpdate, Qt::UniqueConnection ); - } + // using unique connection because there may be temporarily more nodes for a layer than just one + // which would create multiple connections, however disconnect() would disconnect all multiple connections + // even if we wanted to disconnect just one connection in each call. + connect( layer, &QgsMeshLayer::editingStarted, this, &QgsLayerTreeModel::layerNeedsUpdate, Qt::UniqueConnection ); + connect( layer, &QgsMeshLayer::editingStopped, this, &QgsLayerTreeModel::layerNeedsUpdate, Qt::UniqueConnection ); + connect( layer, &QgsMeshLayer::layerModified, this, &QgsLayerTreeModel::layerNeedsUpdate, Qt::UniqueConnection ); emit dataChanged( node2index( nodeLayer ), node2index( nodeLayer ) ); } diff --git a/src/core/layertree/qgslayertreeutils.cpp b/src/core/layertree/qgslayertreeutils.cpp index 8603966fb82f..8917980f3294 100644 --- a/src/core/layertree/qgslayertreeutils.cpp +++ b/src/core/layertree/qgslayertreeutils.cpp @@ -16,6 +16,7 @@ #include "qgslayertreeutils.h" #include "qgslayertree.h" #include "qgsvectorlayer.h" +#include "qgsmeshlayer.h" #include "qgsproject.h" #include "qgslogger.h" @@ -260,18 +261,16 @@ static void _readOldLegendLayer( const QDomElement &layerElem, QgsLayerTreeGroup parent->addChildNode( layerNode ); } - - bool QgsLayerTreeUtils::layersEditable( const QList &layerNodes ) { const auto constLayerNodes = layerNodes; for ( QgsLayerTreeLayer *layerNode : constLayerNodes ) { - QgsVectorLayer *vl = qobject_cast( layerNode->layer() ); - if ( !vl ) + QgsMapLayer *layer = layerNode->layer(); + if ( !layer ) continue; - if ( vl->isEditable() ) + if ( layer->isEditable() ) return true; } return false; @@ -282,11 +281,11 @@ bool QgsLayerTreeUtils::layersModified( const QList &layerN const auto constLayerNodes = layerNodes; for ( QgsLayerTreeLayer *layerNode : constLayerNodes ) { - QgsVectorLayer *vl = qobject_cast( layerNode->layer() ); - if ( !vl ) + QgsMapLayer *layer = layerNode->layer(); + if ( !layer ) continue; - if ( vl->isEditable() && vl->isModified() ) + if ( layer->isEditable() && layer->isModified() ) return true; } return false; diff --git a/src/core/mesh/qgsmeshdataprovider.cpp b/src/core/mesh/qgsmeshdataprovider.cpp index 6cc7f1a529b0..bc5f4db1ef79 100644 --- a/src/core/mesh/qgsmeshdataprovider.cpp +++ b/src/core/mesh/qgsmeshdataprovider.cpp @@ -44,6 +44,8 @@ void QgsMeshDataProvider::setTemporalUnit( QgsUnitTypes::TemporalUnit unit ) reloadData(); } +QgsMeshDriverMetadata QgsMeshDataProvider::driverMetadata() const { return QgsMeshDriverMetadata();} + QgsMeshDatasetIndex QgsMeshDatasetSourceInterface::datasetIndexAtTime( const QDateTime &referenceTime, int groupIndex, quint64 time, diff --git a/src/core/mesh/qgsmeshdataprovider.h b/src/core/mesh/qgsmeshdataprovider.h index 8a657a4165d5..9e8f19e079a1 100644 --- a/src/core/mesh/qgsmeshdataprovider.h +++ b/src/core/mesh/qgsmeshdataprovider.h @@ -28,6 +28,7 @@ #include "qgis_core.h" #include "qgspoint.h" #include "qgsdataprovider.h" +#include "qgsprovidermetadata.h" #include "qgsmeshdataset.h" #include "qgsmeshdataprovidertemporalcapabilities.h" @@ -168,6 +169,17 @@ class CORE_EXPORT QgsMeshDataSourceInterface SIP_ABSTRACT * \since QGIS 3.6 */ virtual void populateMesh( QgsMesh *mesh ) const = 0; + + /** + * Saves the \a mesh frame to the source. + * + * \param mesh the mesh to save + * + * \returns TRUE on success + * + * \since QGIS 3.22 + */ + virtual bool saveMeshFrame( const QgsMesh &mesh ) = 0; }; /** @@ -415,6 +427,24 @@ class CORE_EXPORT QgsMeshDataProvider: public QgsDataProvider, public QgsMeshDat */ void setTemporalUnit( QgsUnitTypes::TemporalUnit unit ); + + /** + * Returns the mesh driver metadata of the provider + * + * \return the mesh driver metadata of the provider + * + * \since QGIS 3.22 + */ + virtual QgsMeshDriverMetadata driverMetadata() const; + + + /** + * Closes the data provider and free every resources used + * + * \since QGIS 3.22 + */ + virtual void close() = 0; + signals: //! Emitted when some new dataset groups have been added void datasetGroupsAdded( int count ); diff --git a/src/core/mesh/qgsmeshdatasetgroupstore.cpp b/src/core/mesh/qgsmeshdatasetgroupstore.cpp index e0823307952a..810ab6286b8b 100644 --- a/src/core/mesh/qgsmeshdatasetgroupstore.cpp +++ b/src/core/mesh/qgsmeshdatasetgroupstore.cpp @@ -99,11 +99,11 @@ bool QgsMeshDatasetGroupStore::addDatasetGroup( QgsMeshDatasetGroup *group ) switch ( group->dataType() ) { case QgsMeshDatasetGroupMetadata::DataOnFaces: - if ( ! group->checkValueCountPerDataset( mLayer->meshFacesCount() ) ) + if ( ! group->checkValueCountPerDataset( mLayer->meshFaceCount() ) ) return false; break; case QgsMeshDatasetGroupMetadata::DataOnVertices: - if ( ! group->checkValueCountPerDataset( mLayer->meshVerticesCount() ) ) + if ( ! group->checkValueCountPerDataset( mLayer->meshVertexCount() ) ) return false; break; case QgsMeshDatasetGroupMetadata::DataOnVolumes: diff --git a/src/core/mesh/qgsmesheditor.cpp b/src/core/mesh/qgsmesheditor.cpp index 461dec33ef5d..558db83bcd55 100644 --- a/src/core/mesh/qgsmesheditor.cpp +++ b/src/core/mesh/qgsmesheditor.cpp @@ -235,7 +235,6 @@ QgsMeshEditingError QgsMeshEditor::removeVertices( const QList &verticesToR } } - if ( !fillHoles ) { QSet concernedNativeFaces; @@ -255,6 +254,7 @@ QgsMeshEditingError QgsMeshEditor::removeVertices( const QList &verticesToR void QgsMeshEditor::stopEditing() { mTopologicalMesh.reindex(); + mUndoStack->clear(); } @@ -381,3 +381,16 @@ void QgsMeshLayerUndoCommandRemoveFaces::redo() QgsMeshEditingError::QgsMeshEditingError(): errorType( Qgis::MeshEditingErrorType::NoError ), elementIndex( -1 ) {} QgsMeshEditingError::QgsMeshEditingError( Qgis::MeshEditingErrorType type, int elementIndex ): errorType( type ), elementIndex( elementIndex ) {} + +QgsRectangle QgsMeshEditor::extent() const +{ + return mTriangularMesh->nativeExtent(); +} + +bool QgsMeshEditor::isModified() const +{ + if ( mUndoStack ) + return !mUndoStack->isClean(); + + return false; +} diff --git a/src/core/mesh/qgsmesheditor.h b/src/core/mesh/qgsmesheditor.h index 4d57401c202a..59b84ab71f9c 100644 --- a/src/core/mesh/qgsmesheditor.h +++ b/src/core/mesh/qgsmesheditor.h @@ -113,6 +113,12 @@ class CORE_EXPORT QgsMeshEditor : public QObject //! Stops editing void stopEditing(); + //! Returns the extent of the edited mesh + QgsRectangle extent() const; + + //! Returns whether the mesh has been modified + bool isModified() const; + signals: //! Emitted when the mesh is edited void meshEdited(); diff --git a/src/core/mesh/qgsmeshlayer.cpp b/src/core/mesh/qgsmeshlayer.cpp index b23d6fb49512..a61a5c51158b 100644 --- a/src/core/mesh/qgsmeshlayer.cpp +++ b/src/core/mesh/qgsmeshlayer.cpp @@ -168,6 +168,9 @@ QgsMeshLayer *QgsMeshLayer::clone() const QgsRectangle QgsMeshLayer::extent() const { + if ( mMeshEditor ) + return mMeshEditor->extent(); + if ( mDataProvider ) return mDataProvider->extent(); else @@ -183,6 +186,19 @@ QString QgsMeshLayer::providerType() const return mProviderKey; } +bool QgsMeshLayer::supportsEditing() const +{ + if ( !mDataProvider ) + return false; + + if ( mMeshEditor ) + return true; + + QgsMeshDriverMetadata driverMetadata = mDataProvider->driverMetadata(); + + return driverMetadata.capabilities() & QgsMeshDriverMetadata::CanWriteMeshData; +} + bool QgsMeshLayer::addDatasets( const QString &path, const QDateTime &defaultReferenceTime ) { bool isTemporalBefore = dataProvider()->temporalCapabilities()->hasTemporalCapabilities(); @@ -401,9 +417,9 @@ QgsMeshDatasetValue QgsMeshLayer::datasetValue( const QgsMeshDatasetIndex &index QgsMeshDatasetValue value; const QgsTriangularMesh *mesh = triangularMesh(); - if ( mesh && dataProvider() && dataProvider()->isValid() && index.isValid() ) + if ( mesh && index.isValid() ) { - if ( dataProvider()->contains( QgsMesh::ElementType::Edge ) ) + if ( contains( QgsMesh::ElementType::Edge ) ) { QgsRectangle searchRectangle( point.x() - searchRadius, point.y() - searchRadius, point.x() + searchRadius, point.y() + searchRadius ); return dataset1dValue( index, point, searchRadius ); @@ -689,6 +705,7 @@ void QgsMeshLayer::onDatasetGroupsAdded( const QList &datasetGroupIndexes ) void QgsMeshLayer::onMeshEdited() { mRendererCache.reset( new QgsMeshLayerRendererCache() ); + emit layerModified(); triggerRepaint(); } @@ -873,6 +890,12 @@ qint64 QgsMeshLayer::datasetRelativeTimeInMilliseconds( const QgsMeshDatasetInde bool QgsMeshLayer::startFrameEditing( const QgsCoordinateTransform &transform ) { + if ( !supportsEditing() ) + { + QgsMessageLog::logMessage( QObject::tr( "Mesh layer \"%1\" not support mesh editing" ).arg( name() ) ); + return false; + } + if ( mMeshEditor ) { QgsMessageLog::logMessage( QObject::tr( "Mesh layer \"%1\" already in editing mode" ).arg( name() ) ); @@ -893,15 +916,78 @@ bool QgsMeshLayer::startFrameEditing( const QgsCoordinateTransform &transform ) return false; } + // During editing, we don't need anymore the provider data. Mesh frame data is stored in the mesh editor. + mDataProvider->close(); + + // All dataset group are removed and replace by a unique virtual dataset group that provide vertices elevation value. + mExtraDatasetUri.clear(); mDatasetGroupStore.reset( new QgsMeshDatasetGroupStore( this ) ); mDatasetGroupStore->addDatasetGroup( new QgsMeshVerticesElevationDatasetGroup( tr( "vertices elevation" ), mNativeMesh.get() ) ); resetDatasetGroupTreeItem(); connect( mMeshEditor, &QgsMeshEditor::meshEdited, this, &QgsMeshLayer::onMeshEdited ); + emit editingStarted(); + return true; } +bool QgsMeshLayer::commitFrameEditing( const QgsCoordinateTransform &transform, bool continueEditing ) +{ + stopFrameEditing( transform ); + + if ( !mDataProvider ) + return false; + + bool res = mDataProvider->saveMeshFrame( *mNativeMesh.get() ); + + if ( continueEditing ) + { + mMeshEditor->initialize(); + emit layerModified(); + return res; + } + + mMeshEditor->deleteLater(); + mMeshEditor = nullptr; + emit editingStopped(); + + mDataProvider->reloadData(); + mDataProvider->populateMesh( mNativeMesh.get() ); + mDatasetGroupStore.reset( new QgsMeshDatasetGroupStore( this ) ); + mDatasetGroupStore->setPersistentProvider( mDataProvider, QStringList() ); + resetDatasetGroupTreeItem(); + return true; +} + +bool QgsMeshLayer::rollBackFrameEditing( const QgsCoordinateTransform &transform, bool continueEditing ) +{ + stopFrameEditing( transform ); + + if ( !mDataProvider ) + return false; + + mDataProvider->reloadData(); + mDataProvider->populateMesh( mNativeMesh.get() ); + updateTriangularMesh( transform ); + + if ( continueEditing ) + { + return mMeshEditor->initialize() == QgsMeshEditingError(); + } + else + { + mMeshEditor->deleteLater(); + mMeshEditor = nullptr; + emit editingStopped(); + + mDatasetGroupStore.reset( new QgsMeshDatasetGroupStore( this ) ); + mDatasetGroupStore->setPersistentProvider( mDataProvider, QStringList() ); + resetDatasetGroupTreeItem(); + return true; + } +} + void QgsMeshLayer::stopFrameEditing( const QgsCoordinateTransform &transform ) { if ( !mMeshEditor ) @@ -909,6 +995,7 @@ void QgsMeshLayer::stopFrameEditing( const QgsCoordinateTransform &transform ) mMeshEditor->stopEditing(); mTriangularMeshes.at( 0 )->update( mNativeMesh.get(), transform ); + mRendererCache.reset( new QgsMeshLayerRendererCache() ); } QgsMeshEditor *QgsMeshLayer::meshEditor() @@ -916,28 +1003,53 @@ QgsMeshEditor *QgsMeshLayer::meshEditor() return mMeshEditor; } -int QgsMeshLayer::meshVerticesCount() const +bool QgsMeshLayer::isModified() const +{ + if ( mMeshEditor ) + return mMeshEditor->isModified(); + + return false; +} + +bool QgsMeshLayer::contains( const QgsMesh::ElementType &type ) const +{ + switch ( type ) + { + case QgsMesh::ElementType::Vertex: + return meshVertexCount() != 0; + case QgsMesh::ElementType::Edge: + return meshEdgeCount() != 0; + case QgsMesh::ElementType::Face: + return meshFaceCount() != 0; + } + return false; +} + +int QgsMeshLayer::meshVertexCount() const { if ( mMeshEditor ) return mNativeMesh->vertexCount(); - else + else if ( mDataProvider ) return mDataProvider->vertexCount(); + else return 0; } -int QgsMeshLayer::meshFacesCount() const +int QgsMeshLayer::meshFaceCount() const { if ( mMeshEditor ) return mNativeMesh->faceCount(); - else + else if ( mDataProvider ) return mDataProvider->faceCount(); + else return 0; } int QgsMeshLayer::meshEdgeCount() const { if ( mMeshEditor ) return mNativeMesh->edgeCount(); - else + else if ( mDataProvider ) return mDataProvider->edgeCount(); + else return 0; } void QgsMeshLayer::updateActiveDatasetGroups() @@ -1475,6 +1587,11 @@ QString QgsMeshLayer::htmlMetadata() const return myMetadata; } +bool QgsMeshLayer::isEditable() const +{ + return mMeshEditor != nullptr; +} + bool QgsMeshLayer::setDataProvider( QString const &provider, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags ) { mDatasetGroupStore->setPersistentProvider( nullptr, QStringList() ); diff --git a/src/core/mesh/qgsmeshlayer.h b/src/core/mesh/qgsmeshlayer.h index 4800984e3dd4..f1cd401cea92 100644 --- a/src/core/mesh/qgsmeshlayer.h +++ b/src/core/mesh/qgsmeshlayer.h @@ -179,6 +179,8 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer void reload() override; QStringList subLayers() const override; QString htmlMetadata() const override; + bool isEditable() const override; + bool supportsEditing() const override; //! Returns the provider type for this layer QString providerType() const; @@ -728,6 +730,26 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer */ bool startFrameEditing( const QgsCoordinateTransform &transform ); + /** + * Commits edition of the mesh frame, + * Rebuilds the triangular mesh and its spatial index with \a transform, + * Continue editing with the same mesh editor if \a continueEditing is True + * + * \return TRUE if the commit succeeds + * \since QGIS 3.22 + */ + bool commitFrameEditing( const QgsCoordinateTransform &transform, bool continueEditing = true ); + + /** + * Rolls Back edition of the mesh frame. + * Reload mesh from file, rebuilds the triangular mesh and its spatial index with \a transform, + * Continue editing with the same mesh editor if \a continueEditing is TRUE + * + * \return TRUE if the rollback succeeds + * \since QGIS 3.22 + */ + bool rollBackFrameEditing( const QgsCoordinateTransform &transform, bool continueEditing = true ); + /** * Stops edition of the mesh, re-indexes the faces and vertices, * rebuilds the triangular mesh and its spatial index with \a transform, @@ -744,19 +766,32 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer */ QgsMeshEditor *meshEditor(); + /** + * Returns whether the mesh frame has been modified since the last save + * + * \since QGIS 3.22 + */ + bool isModified() const override; + + /** + * Returns whether the mesh contains at mesh elements of given type + * \since QGIS 3.22 + */ + bool contains( const QgsMesh::ElementType &type ) const; + /** * Returns the vertices count of the mesh frame * * \since QGIS 3.22 */ - int meshVerticesCount() const; + int meshVertexCount() const; /** * Returns the faces count of the mesh frame * * \since QGIS 3.22 */ - int meshFacesCount() const; + int meshFaceCount() const; /** * Returns the edges count of the mesh frame diff --git a/src/core/mesh/qgstriangularmesh.cpp b/src/core/mesh/qgstriangularmesh.cpp index 8ac013db9ef8..20bf95fb0168 100644 --- a/src/core/mesh/qgstriangularmesh.cpp +++ b/src/core/mesh/qgstriangularmesh.cpp @@ -170,7 +170,8 @@ bool QgsTriangularMesh::update( QgsMesh *nativeMesh, const QgsCoordinateTransfor Q_ASSERT( nativeMesh ); // FIND OUT IF UPDATE IS NEEDED - if ( mTriangularMesh.vertices.size() >= nativeMesh->vertices.size() && + if ( mTriangularMesh.vertices.size() == nativeMesh->vertices.size() && + mNativeMeshFaceCentroids.size() == nativeMesh->faces.size() && mTriangularMesh.faces.size() >= nativeMesh->faces.size() && mTriangularMesh.edges.size() == nativeMesh->edges.size() && ( ( !mCoordinateTransform.isValid() && !transform.isValid() ) || @@ -290,6 +291,27 @@ QgsMeshVertex QgsTriangularMesh::triangularToNativeCoordinates( const QgsMeshVer return transformVertex( vertex, QgsCoordinateTransform::ReverseTransform ); } +QgsRectangle QgsTriangularMesh::nativeExtent() +{ + QgsRectangle nativeExtent; + if ( mCoordinateTransform.isValid() ) + { + try + { + nativeExtent = mCoordinateTransform.transform( mExtent, QgsCoordinateTransform::ReverseTransform ); + } + catch ( QgsCsException &cse ) + { + Q_UNUSED( cse ) + QgsDebugMsg( QStringLiteral( "Caught CRS exception %1" ).arg( cse.what() ) ); + } + } + else + nativeExtent = mExtent; + + return nativeExtent; +} + QgsRectangle QgsTriangularMesh::extent() const { return mExtent; diff --git a/src/core/mesh/qgstriangularmesh.h b/src/core/mesh/qgstriangularmesh.h index 91bf74a9dfe5..1e1e6d73ab40 100644 --- a/src/core/mesh/qgstriangularmesh.h +++ b/src/core/mesh/qgstriangularmesh.h @@ -292,6 +292,14 @@ class CORE_EXPORT QgsTriangularMesh // TODO rename to QgsRendererMesh in QGIS 4 */ QgsMeshVertex triangularToNativeCoordinates( const QgsMeshVertex &vertex ) const; + /** + * + * Returns the extent of the mesh in the native mesh coordinates system, returns empty extent if the transformation fails + * + * \since QGIS 3.22 + */ + QgsRectangle nativeExtent(); + private: /** diff --git a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp index e103a72038a4..368f8f739217 100644 --- a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp +++ b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp @@ -623,6 +623,14 @@ bool QgsMeshMemoryDataProvider::persistDatasetGroup( const QString &outputFilePa return true; // not implemented/supported } +void QgsMeshMemoryDataProvider::close() +{ + mVertices.clear(); + mFaces.clear(); + mEdges.clear(); + mDatasetGroups.clear(); +} + QgsRectangle QgsMeshMemoryDataProvider::calculateExtent() const { QgsRectangle rec; diff --git a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h index a4e38f0fb2ee..31b78451b9aa 100644 --- a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h +++ b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h @@ -153,6 +153,10 @@ class CORE_EXPORT QgsMeshMemoryDataProvider final: public QgsMeshDataProvider int datasetGroupIndex ) override; + bool saveMeshFrame( const QgsMesh & ) override {return false;} + + void close() override; + //! Returns the memory provider key static QString providerKey(); //! Returns the memory provider description diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 5fe8f85c70b6..9e77be0826eb 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -818,6 +818,11 @@ void QgsMapLayer::setSubLayerVisibility( const QString &name, bool vis ) // NOOP } +bool QgsMapLayer::supportsEditing() const +{ + return false; +} + QgsCoordinateReferenceSystem QgsMapLayer::crs() const { return mCRS; @@ -1901,6 +1906,11 @@ bool QgsMapLayer::isEditable() const return false; } +bool QgsMapLayer::isModified() const +{ + return false; +} + bool QgsMapLayer::isSpatial() const { return true; diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index 08ea4e2176c6..7e02738b15bf 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -551,9 +551,24 @@ class CORE_EXPORT QgsMapLayer : public QObject */ virtual void setSubLayerVisibility( const QString &name, bool visible ); + /** + * Returns whether the layer supports editing or not. + * \returns FALSE if the layer is read only or the data provider has no editing capabilities. + * \note default implementation returns FALSE. + * \since QGIS 3.22 in the base class QgsMapLayer. + */ + virtual bool supportsEditing() const; + //! Returns TRUE if the layer can be edited. virtual bool isEditable() const; + /** + * Returns TRUE if the layer has been modified since last commit/save. + * \note default implementation returns FALSE. + * \since QGIS 3.22 in the base class QgsMapLayer. + */ + virtual bool isModified() const; + /** * Returns TRUE if the layer is considered a spatial layer, ie it has some form of geometry associated with it. * \since QGIS 2.16 @@ -1530,6 +1545,24 @@ class CORE_EXPORT QgsMapLayer : public QObject */ void customPropertyChanged( const QString &key ); + /** + * Emitted when editing on this layer has started. + * \since QGIS 3.22 in the QgsMapLayer base class + */ + void editingStarted(); + + /** + * Emitted when edited changes have been successfully written to the data provider. + * \since QGIS 3.22 in the QgsMapLayer base class + */ + void editingStopped(); + + /** + * Emitted when modifications has been done on layer + * \since QGIS 3.22 in the QgsMapLayer base class + */ + void layerModified(); + private slots: void onNotified( const QString &message ); diff --git a/src/core/qgsmaplayerlegend.cpp b/src/core/qgsmaplayerlegend.cpp index 32c674412901..c633484a6574 100644 --- a/src/core/qgsmaplayerlegend.cpp +++ b/src/core/qgsmaplayerlegend.cpp @@ -519,10 +519,6 @@ QList QgsDefaultMeshLayerLegend::createLayerTreeM { QList nodes; - QgsMeshDataProvider *provider = mLayer->dataProvider(); - if ( !provider ) - return nodes; - QgsMeshRendererSettings rendererSettings = mLayer->rendererSettings(); int indexScalar = rendererSettings.activeScalarDatasetGroup(); @@ -530,11 +526,11 @@ QList QgsDefaultMeshLayerLegend::createLayerTreeM QString name; if ( indexScalar > -1 && indexVector > -1 && indexScalar != indexVector ) - name = QString( "%1 / %2" ).arg( provider->datasetGroupMetadata( indexScalar ).name(), provider->datasetGroupMetadata( indexVector ).name() ); + name = QString( "%1 / %2" ).arg( mLayer->datasetGroupMetadata( indexScalar ).name(), mLayer->datasetGroupMetadata( indexVector ).name() ); else if ( indexScalar > -1 ) - name = provider->datasetGroupMetadata( indexScalar ).name(); + name = mLayer->datasetGroupMetadata( indexScalar ).name(); else if ( indexVector > -1 ) - name = provider->datasetGroupMetadata( indexVector ).name(); + name = mLayer->datasetGroupMetadata( indexVector ).name(); else { // neither contours nor vectors get rendered - no legend needed diff --git a/src/core/vector/qgsvectorlayer.cpp b/src/core/vector/qgsvectorlayer.cpp index b9c25fc7850e..163d68998c48 100644 --- a/src/core/vector/qgsvectorlayer.cpp +++ b/src/core/vector/qgsvectorlayer.cpp @@ -3703,7 +3703,7 @@ bool QgsVectorLayer::setReadOnly( bool readonly ) return true; } -bool QgsVectorLayer::supportsEditing() +bool QgsVectorLayer::supportsEditing() const { if ( ! mDataProvider ) return false; diff --git a/src/core/vector/qgsvectorlayer.h b/src/core/vector/qgsvectorlayer.h index 6e6ab7baa124..5ed2b758dde4 100644 --- a/src/core/vector/qgsvectorlayer.h +++ b/src/core/vector/qgsvectorlayer.h @@ -1663,7 +1663,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte bool isSpatial() const FINAL; //! Returns TRUE if the provider has been modified since the last commit - virtual bool isModified() const; + bool isModified() const override; /** * Returns TRUE if the field comes from the auxiliary layer, @@ -1721,7 +1721,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * \return FALSE if the layer is read only or the data provider has no editing capabilities * \since QGIS 3.18 */ - bool supportsEditing(); + bool supportsEditing() const override; /** * Changes a feature's \a geometry within the layer's edit buffer @@ -2551,9 +2551,6 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte */ void selectionChanged( const QgsFeatureIds &selected, const QgsFeatureIds &deselected, bool clearAndSelect ); - //! Emitted when modifications has been done on layer - void layerModified(); - /** * Emitted whenever the allowCommitChanged() property of this layer changes. * @@ -2567,12 +2564,6 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte //! Emitted before editing on this layer is started. void beforeEditingStarted(); - //! Emitted when editing on this layer has started. - void editingStarted(); - - //! Emitted when edited changes have been successfully written to the data provider. - void editingStopped(); - /** * Emitted before changes are committed to the data provider. * diff --git a/src/gui/mesh/qgsmeshrendereractivedatasetwidget.cpp b/src/gui/mesh/qgsmeshrendereractivedatasetwidget.cpp index c0828ed65831..e4e079ca6b9f 100644 --- a/src/gui/mesh/qgsmeshrendereractivedatasetwidget.cpp +++ b/src/gui/mesh/qgsmeshrendereractivedatasetwidget.cpp @@ -147,9 +147,9 @@ QString QgsMeshRendererActiveDatasetWidget::metadata( QgsMeshDatasetIndex datase msg += QLatin1String( "" ); QString definedOnMesh; - if ( mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Face ) ) + if ( mMeshLayer->contains( QgsMesh::ElementType::Face ) ) { - if ( mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) ) + if ( mMeshLayer->contains( QgsMesh::ElementType::Edge ) ) { definedOnMesh = tr( "faces and edges" ); } @@ -158,7 +158,7 @@ QString QgsMeshRendererActiveDatasetWidget::metadata( QgsMeshDatasetIndex datase definedOnMesh = tr( "faces" ); } } - else if ( mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) ) + else if ( mMeshLayer->contains( QgsMesh::ElementType::Edge ) ) { definedOnMesh = tr( "edges" ); } diff --git a/src/gui/mesh/qgsmeshrendererscalarsettingswidget.cpp b/src/gui/mesh/qgsmeshrendererscalarsettingswidget.cpp index 253d69803912..f3d010ebdfa4 100644 --- a/src/gui/mesh/qgsmeshrendererscalarsettingswidget.cpp +++ b/src/gui/mesh/qgsmeshrendererscalarsettingswidget.cpp @@ -84,8 +84,7 @@ QgsMeshRendererScalarSettings QgsMeshRendererScalarSettingsWidget::settings() co settings.setOpacity( mOpacityWidget->opacity() ); settings.setDataResamplingMethod( dataIntepolationMethod() ); - bool hasEdges = ( mMeshLayer->dataProvider() && - mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) ); + bool hasEdges = ( mMeshLayer->contains( QgsMesh::ElementType::Edge ) ); if ( hasEdges ) { @@ -121,10 +120,8 @@ void QgsMeshRendererScalarSettingsWidget::syncToLayer( ) int index = mScalarInterpolationTypeComboBox->findData( settings.dataResamplingMethod() ); whileBlocking( mScalarInterpolationTypeComboBox )->setCurrentIndex( index ); - bool hasEdges = ( mMeshLayer->dataProvider() && - mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) ); - bool hasFaces = ( mMeshLayer->dataProvider() && - mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Face ) ); + bool hasEdges = ( mMeshLayer->contains( QgsMesh::ElementType::Edge ) ); + bool hasFaces = ( mMeshLayer->contains( QgsMesh::ElementType::Face ) ); mScalarResamplingWidget->setVisible( hasFaces ); diff --git a/src/gui/mesh/qgsrenderermeshpropertieswidget.cpp b/src/gui/mesh/qgsrenderermeshpropertieswidget.cpp index 9b592d16b5f8..45de3c6131a0 100644 --- a/src/gui/mesh/qgsrenderermeshpropertieswidget.cpp +++ b/src/gui/mesh/qgsrenderermeshpropertieswidget.cpp @@ -148,12 +148,10 @@ void QgsRendererMeshPropertiesWidget::syncToLayer() onActiveScalarGroupChanged( mMeshLayer->rendererSettings().activeScalarDatasetGroup() ); onActiveVectorGroupChanged( mMeshLayer->rendererSettings().activeVectorDatasetGroup() ); - bool hasFaces = ( mMeshLayer->dataProvider() && - mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Face ) ); + bool hasFaces = ( mMeshLayer->contains( QgsMesh::ElementType::Face ) ); mFaceMeshGroupBox->setVisible( hasFaces ); - bool hasEdges = ( mMeshLayer->dataProvider() && - mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) ); + bool hasEdges = ( mMeshLayer->contains( QgsMesh::ElementType::Edge ) ); mEdgeMeshGroupBox->setVisible( hasEdges ); QgsSettings settings; diff --git a/src/providers/mdal/qgsmdalprovider.cpp b/src/providers/mdal/qgsmdalprovider.cpp index e68fc4dfa79c..a44bcd60ad41 100644 --- a/src/providers/mdal/qgsmdalprovider.cpp +++ b/src/providers/mdal/qgsmdalprovider.cpp @@ -221,6 +221,34 @@ QgsRectangle QgsMdalProvider::extent() const return ret; } +QgsMeshDriverMetadata QgsMdalProvider::driverMetadata() const +{ + if ( !mMeshH ) + return QgsMeshDriverMetadata(); + + QString name = MDAL_M_driverName( mMeshH ); + MDAL_DriverH mdalDriver = MDAL_driverFromName( name.toStdString().c_str() ); + QString longName = MDAL_DR_longName( mdalDriver ); + QString writeDatasetSuffix = MDAL_DR_writeDatasetsSuffix( mdalDriver ); + + QgsMeshDriverMetadata::MeshDriverCapabilities capabilities; + bool hasSaveFaceDatasetsCapability = MDAL_DR_writeDatasetsCapability( mdalDriver, MDAL_DataLocation::DataOnFaces ); + if ( hasSaveFaceDatasetsCapability ) + capabilities |= QgsMeshDriverMetadata::CanWriteFaceDatasets; + bool hasSaveVertexDatasetsCapability = MDAL_DR_writeDatasetsCapability( mdalDriver, MDAL_DataLocation::DataOnVertices ); + if ( hasSaveVertexDatasetsCapability ) + capabilities |= QgsMeshDriverMetadata::CanWriteVertexDatasets; + bool hasSaveEdgeDatasetsCapability = MDAL_DR_writeDatasetsCapability( mdalDriver, MDAL_DataLocation::DataOnEdges ); + if ( hasSaveEdgeDatasetsCapability ) + capabilities |= QgsMeshDriverMetadata::CanWriteEdgeDatasets; + bool hasMeshSaveCapability = MDAL_DR_saveMeshCapability( mdalDriver ); + if ( hasMeshSaveCapability ) + capabilities |= QgsMeshDriverMetadata::CanWriteMeshData; + const QgsMeshDriverMetadata meta( name, longName, capabilities, writeDatasetSuffix ); + + return meta; +} + bool QgsMdalProvider::persistDatasetGroup( const QString &outputFilePath, const QString &outputDriver, @@ -420,6 +448,21 @@ bool QgsMdalProvider::persistDatasetGroup( const QString &outputFilePath, const return true; } +bool QgsMdalProvider::saveMeshFrame( const QgsMesh &mesh ) +{ + QgsMdalProviderMetadata mdalProviderMetaData; + return mdalProviderMetaData.createMeshData( mesh, dataSourceUri(), mDriverName, crs() ); +} + +void QgsMdalProvider::close() +{ + if ( mMeshH ) + MDAL_CloseMesh( mMeshH ); + mMeshH = nullptr; + + mExtraDatasetUris.clear(); +} + void QgsMdalProvider::loadData() { QByteArray curi = dataSourceUri().toUtf8(); @@ -428,6 +471,7 @@ void QgsMdalProvider::loadData() if ( mMeshH ) { + mDriverName = MDAL_M_driverName( mMeshH ); const QString proj = MDAL_M_projection( mMeshH ); if ( !proj.isEmpty() ) mCrs.createFromString( proj ); @@ -472,7 +516,7 @@ void QgsMdalProvider::reloadProviderData() int datasetCountBeforeAdding = datasetGroupCount(); if ( mMeshH ) - for ( auto uri : mExtraDatasetUris ) + for ( const QString &uri : std::as_const( mExtraDatasetUris ) ) { std::string str = uri.toStdString(); MDAL_M_LoadDatasets( mMeshH, str.c_str() ); diff --git a/src/providers/mdal/qgsmdalprovider.h b/src/providers/mdal/qgsmdalprovider.h index e0e4a3e40682..bc6afc50ac90 100644 --- a/src/providers/mdal/qgsmdalprovider.h +++ b/src/providers/mdal/qgsmdalprovider.h @@ -78,6 +78,8 @@ class QgsMdalProvider : public QgsMeshDataProvider QgsMeshDataBlock areFacesActive( QgsMeshDatasetIndex index, int faceIndex, int count ) const override; QgsRectangle extent() const override; + QgsMeshDriverMetadata driverMetadata() const override; + bool persistDatasetGroup( const QString &outputFilePath, const QString &outputDriver, const QgsMeshDatasetGroupMetadata &meta, @@ -92,6 +94,10 @@ class QgsMdalProvider : public QgsMeshDataProvider int datasetGroupIndex ) override; + bool saveMeshFrame( const QgsMesh &mesh ) override; + + void close() override; + /** * Returns file filters for meshes and datasets to be used in Open File Dialogs * \param fileMeshFiltersString file mesh filters @@ -124,6 +130,7 @@ class QgsMdalProvider : public QgsMeshDataProvider QStringList mExtraDatasetUris; QgsCoordinateReferenceSystem mCrs; QStringList mSubLayersUris; + QString mDriverName; /** * Closes and reloads dataset diff --git a/tests/src/core/testqgsmesheditor.cpp b/tests/src/core/testqgsmesheditor.cpp index 41b1d25d8fd1..2bdf58d6abb0 100644 --- a/tests/src/core/testqgsmesheditor.cpp +++ b/tests/src/core/testqgsmesheditor.cpp @@ -42,7 +42,7 @@ class TestQgsMeshEditor : public QObject void init(); // will be called before each testfunction is executed. void cleanup() {} // will be called after every testfunction. - void startEditing(); + void startStopEditing(); void createTopologicMesh(); void editTopologicMesh(); void badTopologicMesh(); @@ -86,13 +86,13 @@ void TestQgsMeshEditor::init() QVERIFY( meshLayerQuadTriangle ); QCOMPARE( meshLayerQuadTriangle->datasetGroupCount(), 1 ); - uri = QString( mDataDir + "/quad_flower.2dm" ); + uri = QString( mDataDir + "/quad_flower_to_edit.2dm" ); meshLayerQuadFlower.reset( new QgsMeshLayer( uri, "Quad Flower", "mdal" ) ); QVERIFY( meshLayerQuadFlower ); QCOMPARE( meshLayerQuadFlower->datasetGroupCount(), 1 ); } -void TestQgsMeshEditor::startEditing() +void TestQgsMeshEditor::startStopEditing() { meshLayerQuadTriangle->addDatasets( mDataDir + "/quad_and_triangle_vertex_scalar.dat" ); QCOMPARE( meshLayerQuadTriangle->datasetGroupCount(), 2 ); @@ -115,9 +115,61 @@ void TestQgsMeshEditor::startEditing() QCOMPARE( meta.isScalar(), true ); QCOMPARE( meta.minimum(), 10.0 ); QCOMPARE( meta.maximum(), 50.0 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); QgsMesh mesh = *meshLayerQuadTriangle->nativeMesh(); for ( int i = 0; i < mesh.vertexCount(); ++i ) QCOMPARE( mesh.vertex( i ).z(), meshLayerQuadTriangle->datasetValue( QgsMeshDatasetIndex( 0, 0 ), i ).scalar() ); + + QgsMeshEditor *editor = meshLayerQuadTriangle->meshEditor(); + QCOMPARE( editor->addVertices( {QgsMeshVertex( 1500, 2500, 0 )}, 10 ), 1 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 6 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 6 ); + + // roll back editing and continue editing + QVERIFY( meshLayerQuadTriangle->isModified() ); + QVERIFY( meshLayerQuadTriangle->rollBackFrameEditing( transform, true ) ); + QVERIFY( editor->mTriangularMesh->faceCentroids().count() == editor->mMesh->faceCount() ); + QVERIFY( editor->mTriangularMesh->vertices().count() == editor->mMesh->vertexCount() ); + + QVERIFY( meshLayerQuadTriangle->meshEditor() ); + + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); + QCOMPARE( meshLayerQuadTriangle->datasetGroupCount(), 1 ); + datasetGroupIndex = meshLayerQuadTriangle->datasetGroupsIndexes().at( 0 ); + meta = meshLayerQuadTriangle->datasetGroupMetadata( datasetGroupIndex ); + QVERIFY( meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnVertices ); + QVERIFY( meta.name() == QStringLiteral( "vertices elevation" ) ); + QCOMPARE( meta.isTemporal(), false ); + QCOMPARE( meta.isScalar(), true ); + QCOMPARE( meta.minimum(), 10.0 ); + QCOMPARE( meta.maximum(), 50.0 ); + + QCOMPARE( editor->addVertices( {QgsMeshVertex( 1500, 2500, 0 )}, 10 ), 1 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 6 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 6 ); + + // roll back editing and stop editing + QVERIFY( meshLayerQuadTriangle->isModified() ); + QVERIFY( meshLayerQuadTriangle->rollBackFrameEditing( transform, false ) ); + + QVERIFY( !meshLayerQuadTriangle->meshEditor() ); + + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); + QCOMPARE( meshLayerQuadTriangle->datasetGroupCount(), 1 ); + datasetGroupIndex = meshLayerQuadTriangle->datasetGroupsIndexes().at( 0 ); + meta = meshLayerQuadTriangle->datasetGroupMetadata( datasetGroupIndex ); + QVERIFY( meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnVertices ); + QVERIFY( meta.name() == QStringLiteral( "Bed Elevation" ) ); + QCOMPARE( meta.isTemporal(), false ); + QCOMPARE( meta.isScalar(), true ); + QCOMPARE( meta.minimum(), 10.0 ); + QCOMPARE( meta.maximum(), 50.0 ); + + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); } static bool checkNeighbors( const QgsTopologicalMesh &mesh, int faceIndex, const QList &expectedNeighbors ) @@ -607,8 +659,8 @@ void TestQgsMeshEditor::meshEditorSimpleEdition() void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() { - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 5 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 2 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); QgsCoordinateTransform transform; QVERIFY( meshLayerQuadTriangle->startFrameEditing( transform ) ); @@ -616,10 +668,10 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() QgsMeshEditor *editor = meshLayerQuadTriangle->meshEditor(); editor->addVertices( {{4000, 2000, 0}}, 10 ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 6 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 6 ); meshLayerQuadTriangle->undoStack()->undo(); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); editor->addVertices( { @@ -627,50 +679,50 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() {4000, 3000, 0} // 6 }, 10 ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); meshLayerQuadTriangle->undoStack()->undo(); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); meshLayerQuadTriangle->undoStack()->redo(); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); // try to add a face that shares only one vertex QgsMeshEditingError error = editor->addFaces( {{2, 5, 6}} ); QVERIFY( error == QgsMeshEditingError( Qgis::MeshEditingErrorType::UniqueSharedVertex, 2 ) ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 2 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); // Add a face that shares two vertices error = editor->addFaces( {{2, 3, 6}} ); QVERIFY( error == QgsMeshEditingError() ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 3 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 3 ); QgsPointXY centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 3100, 2600, 0 ), 10 ); QVERIFY( centroid.compare( QgsPointXY( 3000, 2666.666666666 ), 1e-6 ) ); meshLayerQuadTriangle->undoStack()->undo(); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 2 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 3100, 2600, 0 ), 10 ); QVERIFY( centroid.isEmpty() ); meshLayerQuadTriangle->undoStack()->undo(); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 5 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 2 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 5 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 2 ); meshLayerQuadTriangle->undoStack()->redo(); meshLayerQuadTriangle->undoStack()->redo(); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 3 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 3 ); // Add another face error = editor->addFaces( {{2, 5, 6}} ); QVERIFY( error == QgsMeshEditingError() ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 4 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 4 ); centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 3100, 2600, 0 ), 10 ); QVERIFY( centroid.compare( QgsPointXY( 3000, 2666.666666666 ), 1e-6 ) ); @@ -698,7 +750,7 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() editor->removeFaces( {2, 3} ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 4 ); //removed faces are still present but empty + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 4 ); //removed faces are still present but empty QVERIFY( meshLayerQuadTriangle->nativeMesh()->face( 2 ).isEmpty() ); QVERIFY( meshLayerQuadTriangle->nativeMesh()->face( 3 ).isEmpty() ); @@ -716,7 +768,7 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 3500, 2100, 0 ), 10 ); QVERIFY( centroid.compare( QgsPointXY( 3666.6666666, 2333.33333333 ), 1e-6 ) ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 4 ); //removed faces are still present but empty + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 4 ); //removed faces are still present but empty QVERIFY( QgsMesh::compareFaces( meshLayerQuadTriangle->nativeMesh()->face( 2 ), {2, 6, 3} ) ); QVERIFY( QgsMesh::compareFaces( meshLayerQuadTriangle->nativeMesh()->face( 3 ), {2, 5, 6} ) ); @@ -727,8 +779,8 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() {3000, 3500, 0} }, 10 ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 9 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 8 ); // vertex on a quad face : 4 faces created, 1 removed, removed are still present but void + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 9 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 8 ); // vertex on a quad face : 4 faces created, 1 removed, removed are still present but void QVERIFY( meshLayerQuadTriangle->nativeMesh()->face( 0 ).isEmpty() ); QVERIFY( QgsMesh::compareFaces( meshLayerQuadTriangle->nativeMesh()->face( 4 ), {0, 1, 7} ) ); QVERIFY( QgsMesh::compareFaces( meshLayerQuadTriangle->nativeMesh()->face( 5 ), {1, 3, 7} ) ); @@ -755,8 +807,8 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 3500, 2100, 0 ), 10 ); QVERIFY( centroid.compare( QgsPointXY( 3666.6666666, 2333.33333333 ), 1e-6 ) ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 7 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 4 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 4 ); QVERIFY( QgsMesh::compareFaces( meshLayerQuadTriangle->nativeMesh()->face( 2 ), {2, 6, 3} ) ); QVERIFY( QgsMesh::compareFaces( meshLayerQuadTriangle->nativeMesh()->face( 3 ), {2, 5, 6} ) ); @@ -771,8 +823,8 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 1950, 2700, 0 ), 10 ); QVERIFY( centroid.compare( QgsPointXY( 1833.33333333, 2600 ), 1e-6 ) ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 9 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 8 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 9 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 8 ); editor->removeVertices( {7} ); @@ -785,7 +837,7 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 1950, 2700, 0 ), 10 ); QVERIFY( centroid.isEmpty() ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 9 ); //empty vertex still presents + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 9 ); //empty vertex still presents snappedPoint = meshLayerQuadTriangle->snapOnElement( QgsMesh::Vertex, QgsPoint( 1498, 3505, 0 ), 10 ); QVERIFY( snappedPoint.isEmpty() ); @@ -805,8 +857,8 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() QVERIFY( snappedPoint.compare( QgsPointXY( 1500, 2800 ), 1e-6 ) ); meshLayerQuadTriangle->stopFrameEditing( transform ); - QCOMPARE( meshLayerQuadTriangle->meshVerticesCount(), 9 ); - QCOMPARE( meshLayerQuadTriangle->meshFacesCount(), 7 ); + QCOMPARE( meshLayerQuadTriangle->meshVertexCount(), 9 ); + QCOMPARE( meshLayerQuadTriangle->meshFaceCount(), 7 ); centroid = meshLayerQuadTriangle->snapOnElement( QgsMesh::Face, QgsPoint( 1400, 2050, 0 ), 10 ); QVERIFY( centroid.compare( QgsPointXY( 1500, 2266.66666666 ), 1e-6 ) ); @@ -823,8 +875,8 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadTriangle() void TestQgsMeshEditor::meshEditorFromMeshLayer_quadFlower() { - QCOMPARE( meshLayerQuadFlower->meshVerticesCount(), 8 ); - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 5 ); + QCOMPARE( meshLayerQuadFlower->meshVertexCount(), 8 ); + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 5 ); QgsCoordinateTransform transform; QVERIFY( meshLayerQuadFlower->startFrameEditing( transform ) ); @@ -835,28 +887,28 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadFlower() meshLayerQuadFlower->startFrameEditing( transform ); editor = meshLayerQuadFlower->meshEditor(); QCOMPARE( editor->addPointsAsVertices( {QgsPoint( 1500, 2800, -10 )}, 10 ), 1 ); // 8 - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 9 ); + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 9 ); QCOMPARE( editor->addPointsAsVertices( {QgsPoint( 1800, 2700, -10 )}, 10 ), 1 ); // 9 - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 12 ); + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 12 ); QCOMPARE( editor->addPointsAsVertices( {QgsPoint( 1400, 2300, -10 ), QgsPoint( 1500, 2200, -10 )}, 10 ), 2 ); // 10 & 11 - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 18 ); + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 18 ); // attempt to add a vertex under tolerance next existing one QCOMPARE( editor->addPointsAsVertices( {QgsPoint( 1499, 2801, -10 )}, 10 ), 0 ); - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 18 ); + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 18 ); QCOMPARE( editor->addPointsAsVertices( {QgsPoint( 700, 1750, 0 )}, 10 ), 1 ); // 12 - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 18 ); - QCOMPARE( meshLayerQuadFlower->meshVerticesCount(), 13 ); + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 18 ); + QCOMPARE( meshLayerQuadFlower->meshVertexCount(), 13 ); QVERIFY( editor->addFace( {0, 6, 12} ) == QgsMeshEditingError() ); QCOMPARE( editor->addPointsAsVertices( {QgsPoint( 1400, 2200, -10 )}, 10 ), 1 ); // 13 - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 22 ); + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 22 ); QCOMPARE( meshLayerQuadFlower->datasetValue( QgsMeshDatasetIndex( 0, 0 ), QgsPointXY( 1420, 2220 ), 10 ).x(), -10 ); QVERIFY( editor->removeVertices( {0}, true ) == QgsMeshEditingError() ); //for now filling after removing boundary vertices is not supported, --> behavior as it is false @@ -918,16 +970,46 @@ void TestQgsMeshEditor::meshEditorFromMeshLayer_quadFlower() QVERIFY( editor->removeVertices( {7}, true ) == QgsMeshEditingError() ); - QVERIFY( editor->removeVertices( {4}, false ).errorType != Qgis::MeshEditingErrorType::NoError ); // leads to a topological error + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 57 ); + QCOMPARE( meshLayerQuadFlower->meshVertexCount(), 16 ); + + meshLayerQuadFlower->commitFrameEditing( transform, true ); + + QVERIFY( meshLayerQuadFlower->meshEditor() == editor ); + + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 5 ); + QCOMPARE( meshLayerQuadFlower->meshVertexCount(), 7 ); + + QVERIFY( editor->removeVertices( {3}, false ).errorType != Qgis::MeshEditingErrorType::NoError ); // leads to a topological error + + QVERIFY( editor->removeVertices( {3}, true ).errorType != Qgis::MeshEditingErrorType::NoError ); // filling after removing boundary not supported, so not fill and leads to a topological error + + QVERIFY( editor->removeVertices( {4}, true ) == QgsMeshEditingError() ); + + meshLayerQuadFlower->commitFrameEditing( transform, false ); + + QVERIFY( meshLayerQuadFlower->meshEditor() == nullptr ); + ; + QCOMPARE( meshLayerQuadFlower->meshFaceCount(), 4 ); + QCOMPARE( meshLayerQuadFlower->meshVertexCount(), 6 ); + + QFile alteredFile( QString( mDataDir + "/quad_flower_to_edit.2dm" ) ); + QFile expectedFile( QString( mDataDir + "/quad_flower_to_edit_expected.2dm" ) ); + QFile original( QString( mDataDir + "/quad_flower.2dm" ) ); - QVERIFY( editor->removeVertices( {4}, true ).errorType != Qgis::MeshEditingErrorType::NoError ); // filling after removing boundary not supported, so not fill and leads to a topological error + alteredFile.open( QIODevice::ReadOnly ); + original.open( QIODevice::ReadOnly ); + expectedFile.open( QIODevice::ReadOnly ); - QVERIFY( editor->removeVertices( {5}, true ) == QgsMeshEditingError() ); + QTextStream streamAltered( &alteredFile ); + QTextStream streamOriginal( &original ); + QTextStream streamExpected( &expectedFile ); - meshLayerQuadFlower->stopFrameEditing( transform ); + QCOMPARE( streamAltered.readAll(), streamExpected.readAll() ); - QCOMPARE( meshLayerQuadFlower->meshFacesCount(), 4 ); - QCOMPARE( meshLayerQuadFlower->meshVerticesCount(), 6 ); + alteredFile.close(); + alteredFile.open( QIODevice::WriteOnly ); + streamAltered << streamOriginal.readAll(); } QGSTEST_MAIN( TestQgsMeshEditor ) diff --git a/tests/src/core/testqgsmeshlayer.cpp b/tests/src/core/testqgsmeshlayer.cpp index 5b236a98a716..7d8be6625529 100644 --- a/tests/src/core/testqgsmeshlayer.cpp +++ b/tests/src/core/testqgsmeshlayer.cpp @@ -133,6 +133,7 @@ void TestQgsMeshLayer::initTestCase() QCOMPARE( mMemoryLayer->datasetGroupTreeRootItem()->childCount(), 5 ); QVERIFY( mMemoryLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); QVERIFY( mMemoryLayer->temporalProperties()->isActive() ); + QVERIFY( !mMemoryLayer->supportsEditing() ); QgsProject::instance()->addMapLayers( QList() << mMemoryLayer ); @@ -152,6 +153,7 @@ void TestQgsMeshLayer::initTestCase() mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_els_face_scalar.dat" ); mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_els_face_vector.dat" ); QCOMPARE( mMdalLayer->datasetGroupTreeRootItem()->childCount(), 5 ); + QVERIFY( mMdalLayer->supportsEditing() ); QVERIFY( mMdalLayer->isValid() ); QVERIFY( mMemoryLayer->temporalProperties()->isActive() ); @@ -227,6 +229,7 @@ void TestQgsMeshLayer::test_read_flower_mesh() QVERIFY( layer.dataProvider()->isValid() ); QCOMPARE( 8, layer.dataProvider()->vertexCount() ); QCOMPARE( 5, layer.dataProvider()->faceCount() ); + QVERIFY( layer.supportsEditing() ); } void TestQgsMeshLayer::test_read_1d_mesh() diff --git a/tests/testdata/mesh/quad_flower_to_edit.2dm b/tests/testdata/mesh/quad_flower_to_edit.2dm new file mode 100644 index 000000000000..c7f6f293f479 --- /dev/null +++ b/tests/testdata/mesh/quad_flower_to_edit.2dm @@ -0,0 +1,14 @@ +MESH2D +ND 1 1000.000 2000.000 200.000 +ND 2 2000.000 2000.000 200.000 +ND 3 2500.000 2500.000 800.000 +ND 4 2000.000 3000.000 200.000 +ND 5 1000.000 3000.000 200.000 +ND 6 500.000 2500.000 800.000 +ND 7 1500.000 1500.000 800.000 +ND 8 1500.000 3500.000 800.000 +E4Q 1 1 2 4 5 1 +E3T 2 2 3 4 1 +E3T 3 1 5 6 1 +E3T 4 2 1 7 1 +E3T 5 5 4 8 1 diff --git a/tests/testdata/mesh/quad_flower_to_edit_expected.2dm b/tests/testdata/mesh/quad_flower_to_edit_expected.2dm new file mode 100644 index 000000000000..6cfd0975bfba --- /dev/null +++ b/tests/testdata/mesh/quad_flower_to_edit_expected.2dm @@ -0,0 +1,11 @@ +MESH2D +ND 1 2000 2000 200 +ND 2 2500 2500 800 +ND 3 2000 3000 200 +ND 4 1000 3000 200 +ND 5 1500 1500 800 +ND 6 700 1750 0 +E3T 1 1 2 3 +E3T 2 4 1 3 +E3T 3 1 4 6 +E3T 4 1 6 5