Skip to content

Commit

Permalink
Add the possibility to rotate a single feature (line or polygon) (#5737)
Browse files Browse the repository at this point in the history
  • Loading branch information
qsavoye authored Nov 27, 2024
1 parent 6a0f143 commit 98f5d8a
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 11 deletions.
1 change: 1 addition & 0 deletions images/images.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<file>themes/qfield/nodpi/ic_remove_vertex_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_menu_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_move_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_rotate_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_location_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_location_valid_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_location_disabled_white_24dp.svg</file>
Expand Down
1 change: 1 addition & 0 deletions images/themes/qfield/nodpi/ic_rotate_white_24dp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/core/featurelistextentcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ void FeatureListExtentController::zoomToSelected( bool skipIfIntersects ) const
}
}

QgsPoint FeatureListExtentController::getCentroidFromSelected() const
{
if ( mModel && mSelection && mSelection->focusedItem() > -1 && mMapSettings )
{
const QgsFeature feat = mSelection->focusedFeature();
const QgsVectorLayer *layer = mSelection->focusedLayer();

if ( layer && layer->geometryType() != Qgis::GeometryType::Unknown && layer->geometryType() != Qgis::GeometryType::Null )
{
QgsGeometry geom = feat.geometry();

const QgsCoordinateTransform transf( layer->crs(), mMapSettings->destinationCrs(), mMapSettings->mapSettings().transformContext() );
geom.transform( transf );

return QgsPoint( geom.centroid().asPoint() );
}
}
return QgsPoint();
}

void FeatureListExtentController::onModelChanged()
{
if ( mModel && mSelection )
Expand Down
1 change: 1 addition & 0 deletions src/core/featurelistextentcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class FeatureListExtentController : public QObject
//! zoom to the selected features.
//! If \a skipIfIntersects is true, no change will be applied if bounding box intersects with canvas extent
void zoomToSelected( bool skipIfIntersects = false ) const;
QgsPoint getCentroidFromSelected() const;

signals:
void autoZoomChanged();
Expand Down
10 changes: 10 additions & 0 deletions src/core/multifeaturelistmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ bool MultiFeatureListModel::canMoveSelection() const
return mSourceModel->canMoveSelection();
}

bool MultiFeatureListModel::canRotateSelection() const
{
return mSourceModel->canRotateSelection();
}

bool MultiFeatureListModel::canProcessSelection() const
{
return mSourceModel->canProcessSelection();
Expand Down Expand Up @@ -165,6 +170,11 @@ bool MultiFeatureListModel::moveSelection( const double x, const double y )
return mSourceModel->moveSelection( x, y );
}

bool MultiFeatureListModel::rotateSelection( const double angle )
{
return mSourceModel->rotateSelection( angle );
}

void MultiFeatureListModel::toggleSelectedItem( int item )
{
QModelIndex sourceItem = mapToSource( index( item, 0 ) );
Expand Down
7 changes: 7 additions & 0 deletions src/core/multifeaturelistmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class MultiFeatureListModel : public QSortFilterProxyModel
Q_PROPERTY( bool canDeleteSelection READ canDeleteSelection NOTIFY selectedCountChanged )
Q_PROPERTY( bool canDuplicateSelection READ canDuplicateSelection NOTIFY selectedCountChanged )
Q_PROPERTY( bool canMoveSelection READ canMoveSelection NOTIFY selectedCountChanged )
Q_PROPERTY( bool canRotateSelection READ canRotateSelection NOTIFY selectedCountChanged )
Q_PROPERTY( bool canProcessSelection READ canProcessSelection NOTIFY selectedCountChanged )

public:
Expand Down Expand Up @@ -112,6 +113,9 @@ class MultiFeatureListModel : public QSortFilterProxyModel
//! Returns TRUE if the selected features' geometry can be moved
bool canMoveSelection() const;

//! Returns TRUE if the selected features' geometry can be rotated
bool canRotateSelection() const;

//! Returns TRUE if the selected features can run processing algorithms
bool canProcessSelection() const;

Expand Down Expand Up @@ -149,6 +153,9 @@ class MultiFeatureListModel : public QSortFilterProxyModel
//! Moves selected features along a given \a vector.
Q_INVOKABLE bool moveSelection( const double x, const double y );

//! Rotate selected features along a given \a vector.
Q_INVOKABLE bool rotateSelection( const double angle );

/**
* Toggles the selection state of a given item.
* \param item the item's row number
Expand Down
72 changes: 72 additions & 0 deletions src/core/multifeaturelistmodelbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,38 @@ bool MultiFeatureListModelBase::canMoveSelection() const
return true;
}

bool MultiFeatureListModelBase::canRotateSelection() const
{
if ( mSelectedFeatures.isEmpty() )
return false;

QgsVectorLayer *vlayer = mSelectedFeatures[0].first;
if ( !vlayer || vlayer->readOnly() || !( vlayer->dataProvider()->capabilities() & Qgis::VectorProviderCapability::ChangeGeometries ) || vlayer->customProperty( QStringLiteral( "QFieldSync/is_geometry_locked" ), false ).toBool() )
return false;

const bool geometryLockedExpressionActive = vlayer->customProperty( QStringLiteral( "QFieldSync/is_geometry_locked_expression_active" ), false ).toBool();
if ( geometryLockedExpressionActive )
{
const QString geometryLockedExpression = vlayer->customProperty( QStringLiteral( "QFieldSync/geometry_locked_expression" ), QString() ).toString().trimmed();
if ( !geometryLockedExpression.isEmpty() )
{
QgsExpressionContext expressionContext = vlayer->createExpressionContext();
for ( const QPair<QgsVectorLayer *, QgsFeature> &selectedFeature : std::as_const( mSelectedFeatures ) )
{
expressionContext.setFeature( selectedFeature.second );
QgsExpression expression( geometryLockedExpression );
expression.prepare( &expressionContext );
if ( !expression.evaluate( &expressionContext ).toBool() )
{
return false;
}
}
}
}

return true;
}

bool MultiFeatureListModelBase::canProcessSelection() const
{
if ( mSelectedFeatures.isEmpty() )
Expand Down Expand Up @@ -624,6 +656,46 @@ bool MultiFeatureListModelBase::moveSelection( const double x, const double y )
return isSuccess;
}

bool MultiFeatureListModelBase::rotateSelection( const double angle )
{
if ( !canRotateSelection() )
return false;

QgsVectorLayer *vlayer = mSelectedFeatures[0].first;
if ( !vlayer->startEditing() )
{
QgsMessageLog::logMessage( tr( "Cannot start editing" ), "QField", Qgis::Warning );
return false;
}

bool isSuccess = false;
for ( auto &pair : mSelectedFeatures )
{
QgsGeometry geom = pair.second.geometry();
geom.rotate( angle, geom.centroid().asPoint() );
pair.second.setGeometry( geom );
isSuccess = vlayer->changeGeometry( pair.second.id(), geom );
if ( !isSuccess )
{
QgsMessageLog::logMessage( tr( "Cannot change geometry of feature %1 in %2" ).arg( pair.second.id() ).arg( vlayer->name() ), "QField", Qgis::Critical );
break;
}
}

if ( isSuccess )
{
// commit changes
isSuccess = vlayer->commitChanges();
}
else
{
if ( !vlayer->rollBack() )
QgsMessageLog::logMessage( tr( "Cannot rollback layer changes in layer %1" ).arg( vlayer->name() ), "QField", Qgis::Critical );
}

return isSuccess;
}

void MultiFeatureListModelBase::layerDeleted( QObject *object )
{
int firstRowToRemove = -1;
Expand Down
7 changes: 7 additions & 0 deletions src/core/multifeaturelistmodelbase.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "identifytool.h"

#include <QAbstractItemModel>
#include <qgis.h>
#include <qgsfeaturerequest.h>

/**
Expand Down Expand Up @@ -83,6 +84,9 @@ class MultiFeatureListModelBase : public QAbstractItemModel
//! \copydoc MultiFeatureListModel::canMoveSelection
bool canMoveSelection() const;

//! \copydoc MultiFeatureListModel::canRotateSelection
bool canRotateSelection() const;

//! \copydoc MultiFeatureListModel::canProcessSelection
bool canProcessSelection() const;

Expand Down Expand Up @@ -110,6 +114,9 @@ class MultiFeatureListModelBase : public QAbstractItemModel
//! \copydoc MultiFeatureListModel::moveSelection
bool moveSelection( const double x, const double y );

//! \copydoc MultiFeatureListModel::rotateSelection
bool rotateSelection( const double angle );

//! \copydoc MultiFeatureListModel::toggleSelectedItem
void toggleSelectedItem( int item );

Expand Down
20 changes: 19 additions & 1 deletion src/qml/FeatureListForm.qml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Rectangle {
property MapSettings mapSettings
property DigitizingToolbar digitizingToolbar
property ConfirmationToolbar moveFeaturesToolbar
property ConfirmationToolbar rotateFeaturesToolbar
property CodeReader codeReader

property color selectionColor
Expand All @@ -48,7 +49,7 @@ Rectangle {
property bool fullScreenView: qfieldSettings.fullScreenIdentifyView
property bool isVertical: parent.width < parent.height || parent.width < 300

property bool canvasOperationRequested: digitizingToolbar.geometryRequested || moveFeaturesToolbar.moveFeaturesRequested
property bool canvasOperationRequested: digitizingToolbar.geometryRequested || moveFeaturesToolbar.moveFeaturesRequested || rotateFeaturesToolbar.rotateFeaturesRequested

signal showMessage(string message)
signal editGeometry
Expand Down Expand Up @@ -568,6 +569,15 @@ Rectangle {
}
}

onRotateClicked: {
if (featureFormList.selection.focusedItem !== -1) {
featureFormList.state = "FeatureList";
featureFormList.multiSelection = true;
featureFormList.selection.model.toggleSelectedItem(featureFormList.selection.focusedItem);
rotateFeaturesToolbar.initializeRotateFeatures();
}
}

onTransferClicked: {
transferDialog.show();
}
Expand Down Expand Up @@ -651,6 +661,14 @@ Rectangle {
}
}

Connections {
target: rotateFeaturesToolbar

function onRotateConfirmed() {
featureFormList.model.rotateSelection(rotateFeaturesToolbar.angle);
}
}

onMultiDuplicateClicked: {
if (featureFormList.multiSelection) {
if (featureFormList.model.duplicateSelection()) {
Expand Down
18 changes: 14 additions & 4 deletions src/qml/FeatureListSelectionHighlight.qml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ Repeater {
property MapSettings mapSettings
property double translateX: 0.0
property double translateY: 0.0
property double rotationDegrees: 0.0
property color color: "yellow"
property color focusedColor: "red"
property color selectedColor: Theme.mainColor
property bool showSelectedOnly: false
property double originX: 0.0
property double originY: 0.0

model: selectionModel.model

Expand All @@ -27,9 +30,16 @@ Repeater {
color: model.featureSelected ? featureListSelectionHighlight.selectedColor : selectionModel.model.selectedCount === 0 && selectionModel && model.index === selectionModel.focusedItem ? featureListSelectionHighlight.focusedColor : featureListSelectionHighlight.color
z: model.index === selectionModel.focusedItem ? 1 : 0

transform: Translate {
x: featureListSelectionHighlight.translateX
y: -featureListSelectionHighlight.translateY
}
transform: [
Translate {
x: featureListSelectionHighlight.translateX
y: -featureListSelectionHighlight.translateY
},
Rotation {
origin.x: featureListSelectionHighlight.originX
origin.y: featureListSelectionHighlight.originY
angle: featureListSelectionHighlight.rotationDegrees
}
]
}
}
16 changes: 16 additions & 0 deletions src/qml/NavigationBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Rectangle {
signal destinationClicked
signal moveClicked
signal duplicateClicked
signal rotateClicked
signal transferClicked
signal deleteClicked

Expand Down Expand Up @@ -758,6 +759,21 @@ Rectangle {
onTriggered: duplicateClicked()
}

MenuItem {
id: rotateFeatureBtn
text: qsTr('Rotate Feature')
icon.source: Theme.getThemeVectorIcon("ic_rotate_white_24dp")
// allow only rotation for line or polygon or multipoint
enabled: ((projectInfo.editRights || editButton.isCreatedCloudFeature) && (!selection.focusedLayer || !featureForm.model.featureModel.geometryLocked)) && (selection.focusedLayer.geometryType() == 0 || selection.focusedLayer.geometryType() == 1 || selection.focusedLayer.geometryType() == 2)
visible: enabled

font: Theme.defaultFont
height: visible ? 48 : 0
leftPadding: Theme.menuItemLeftPadding

onTriggered: rotateClicked()
}

MenuItem {
id: transferFeatureAttributesBtn
text: qsTr('Update Attributes From Feature')
Expand Down
Loading

1 comment on commit 98f5d8a

@qfield-fairy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.