diff --git a/python/3d/auto_generated/qgscameracontroller.sip.in b/python/3d/auto_generated/qgscameracontroller.sip.in index 8398b3ab0bc9..a06948f69d27 100644 --- a/python/3d/auto_generated/qgscameracontroller.sip.in +++ b/python/3d/auto_generated/qgscameracontroller.sip.in @@ -205,6 +205,13 @@ Reacts to the shift of origin of the scene, updating camera pose and any other member variables so that the origin stays at the same position relative to other entities. +.. versionadded:: 3.42 +%End + + void setInputHandlersEnabled( bool enable ); +%Docstring +Sets whether the camera controller responds to mouse and keyboard events + .. versionadded:: 3.42 %End diff --git a/python/PyQt6/3d/auto_generated/qgscameracontroller.sip.in b/python/PyQt6/3d/auto_generated/qgscameracontroller.sip.in index 8398b3ab0bc9..a06948f69d27 100644 --- a/python/PyQt6/3d/auto_generated/qgscameracontroller.sip.in +++ b/python/PyQt6/3d/auto_generated/qgscameracontroller.sip.in @@ -205,6 +205,13 @@ Reacts to the shift of origin of the scene, updating camera pose and any other member variables so that the origin stays at the same position relative to other entities. +.. versionadded:: 3.42 +%End + + void setInputHandlersEnabled( bool enable ); +%Docstring +Sets whether the camera controller responds to mouse and keyboard events + .. versionadded:: 3.42 %End diff --git a/src/3d/qgscameracontroller.cpp b/src/3d/qgscameracontroller.cpp index fadafd3a5735..b889a069a4e3 100644 --- a/src/3d/qgscameracontroller.cpp +++ b/src/3d/qgscameracontroller.cpp @@ -293,6 +293,9 @@ void QgsCameraController::moveCameraPositionBy( const QVector3D &posDiff ) void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse ) { + if ( !mInputHandlersEnabled ) + return; + switch ( mCameraNavigationMode ) { case Qgis::NavigationMode::TerrainBased: @@ -531,6 +534,9 @@ void QgsCameraController::handleTerrainNavigationWheelZoom() void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel ) { + if ( !mInputHandlersEnabled ) + return; + switch ( mCameraNavigationMode ) { case Qgis::NavigationMode::Walk: @@ -563,6 +569,9 @@ void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel ) void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse ) { + if ( !mInputHandlersEnabled ) + return; + mKeyboardHandler->setFocus( true ); if ( mouse->button() == Qt3DInput::QMouseEvent::MiddleButton || ( ( mouse->modifiers() & Qt::ShiftModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) || ( ( mouse->modifiers() & Qt::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) ) @@ -593,12 +602,18 @@ void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse ) void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse ) { Q_UNUSED( mouse ) + if ( !mInputHandlersEnabled ) + return; + setMouseParameters( MouseOperation::None ); } void QgsCameraController::onKeyPressed( Qt3DInput::QKeyEvent *event ) { + if ( !mInputHandlersEnabled ) + return; + if ( event->modifiers() & Qt::ControlModifier && event->key() == Qt::Key_QuoteLeft ) { // switch navigation mode @@ -891,6 +906,9 @@ void QgsCameraController::onPositionChangedFlyNavigation( Qt3DInput::QMouseEvent void QgsCameraController::onKeyReleased( Qt3DInput::QKeyEvent *event ) { + if ( !mInputHandlersEnabled ) + return; + if ( event->isAutoRepeat() ) return; diff --git a/src/3d/qgscameracontroller.h b/src/3d/qgscameracontroller.h index 2208481c5fb4..6bb158e7831f 100644 --- a/src/3d/qgscameracontroller.h +++ b/src/3d/qgscameracontroller.h @@ -219,6 +219,12 @@ class _3D_EXPORT QgsCameraController : public QObject */ void setOrigin( const QgsVector3D &origin ); + /** + * Sets whether the camera controller responds to mouse and keyboard events + * \since QGIS 3.42 + */ + void setInputHandlersEnabled( bool enable ) { mInputHandlersEnabled = enable; } + public slots: /** @@ -362,6 +368,7 @@ class _3D_EXPORT QgsCameraController : public QObject Qt3DInput::QMouseHandler *mMouseHandler = nullptr; Qt3DInput::QKeyboardHandler *mKeyboardHandler = nullptr; + bool mInputHandlersEnabled = true; Qgis::NavigationMode mCameraNavigationMode = Qgis::NavigationMode::TerrainBased; Qgis::VerticalAxisInversion mVerticalAxisInversion = Qgis::VerticalAxisInversion::WhenDragging; double mCameraMovementSpeed = 5.0; diff --git a/src/3d/qgsrubberband3d.h b/src/3d/qgsrubberband3d.h index 51b33e14268e..0331d4fc52ec 100644 --- a/src/3d/qgsrubberband3d.h +++ b/src/3d/qgsrubberband3d.h @@ -129,6 +129,8 @@ class _3D_EXPORT QgsRubberBand3D //! Sets whether the marker on the last vertex is displayed. We typically do not want it displayed while it is still tracked by the mouse. void setHideLastMarker( bool hide ) { mHideLastMarker = hide; } + bool isEmpty() const { return mLineString.isEmpty(); } + private: void updateGeometry(); void updateMarkerMaterial(); diff --git a/src/app/3d/qgs3dmapcanvaswidget.cpp b/src/app/3d/qgs3dmapcanvaswidget.cpp index f21c393fac73..a56d239ae11d 100644 --- a/src/app/3d/qgs3dmapcanvaswidget.cpp +++ b/src/app/3d/qgs3dmapcanvaswidget.cpp @@ -47,6 +47,7 @@ #include "qgs3dmapsettings.h" #include "qgs3dmaptoolidentify.h" #include "qgs3dmaptoolmeasureline.h" +#include "qgs3dmaptoolpointcloudchangeattribute.h" #include "qgs3dnavigationwidget.h" #include "qgs3ddebugwidget.h" #include "qgs3dutils.h" @@ -57,6 +58,7 @@ #include "qgsdockablewidgethelper.h" #include "qgsrubberband.h" +#include "qgspointcloudlayer.h" #include #include @@ -75,6 +77,24 @@ Qgs3DMapCanvasWidget::Qgs3DMapCanvasWidget( const QString &name, bool isDocked ) toolBar->addAction( QgsApplication::getThemeIcon( QStringLiteral( "mActionZoomFullExtent.svg" ) ), tr( "Zoom Full" ), this, &Qgs3DMapCanvasWidget::resetView ); + // Editing toolbar + mEditingToolBar = new QToolBar( this ); + mEditingToolBar->setVisible( false ); + + QAction *actionPointCloudChangeAttributeTool = mEditingToolBar->addAction( QIcon( QgsApplication::iconPath( "mActionSelectPolygon.svg" ) ), tr( "Change Point Cloud Attribute" ), this, &Qgs3DMapCanvasWidget::changePointCloudAttribute ); + actionPointCloudChangeAttributeTool->setCheckable( true ); + + mEditingToolBar->addWidget( new QLabel( tr( "Attribute" ) ) ); + mCboChangeAttribute = new QComboBox(); + mEditingToolBar->addWidget( mCboChangeAttribute ); + mSpinChangeAttributeValue = new QgsDoubleSpinBox(); + mEditingToolBar->addWidget( new QLabel( tr( "Value" ) ) ); + mEditingToolBar->addWidget( mSpinChangeAttributeValue ); + QAction *actionEditingToolbar = toolBar->addAction( QIcon( QgsApplication::iconPath( "mIconPointCloudLayer.svg" ) ), tr( "Show Editing Toolbar" ), this, [this] { mEditingToolBar->setVisible( !mEditingToolBar->isVisible() ); } ); + actionEditingToolbar->setCheckable( true ); + connect( mCboChangeAttribute, qOverload( &QComboBox::currentIndexChanged ), this, [this]( int ) { onPointCloudChangeAttributeSettingsChanged(); } ); + connect( mSpinChangeAttributeValue, qOverload( &QgsDoubleSpinBox::valueChanged ), this, [this]( double ) { onPointCloudChangeAttributeSettingsChanged(); } ); + QAction *toggleOnScreenNavigation = toolBar->addAction( QgsApplication::getThemeIcon( QStringLiteral( "mAction3DNavigation.svg" ) ), tr( "Toggle On-Screen Navigation" ) @@ -99,8 +119,8 @@ Qgs3DMapCanvasWidget::Qgs3DMapCanvasWidget( const QString &name, bool isDocked ) actionGroup->addAction( actionCameraControl ); actionGroup->addAction( actionIdentify ); actionGroup->addAction( actionMeasurementTool ); + actionGroup->addAction( actionPointCloudChangeAttributeTool ); actionGroup->setExclusive( true ); - actionCameraControl->setChecked( true ); mActionAnim = toolBar->addAction( QIcon( QgsApplication::iconPath( "mTaskRunning.svg" ) ), tr( "Animations" ), this, &Qgs3DMapCanvasWidget::toggleAnimations ); mActionAnim->setCheckable( true ); @@ -237,6 +257,9 @@ Qgs3DMapCanvasWidget::Qgs3DMapCanvasWidget( const QString &name, bool isDocked ) mMapToolMeasureLine = new Qgs3DMapToolMeasureLine( mCanvas ); + mMapToolPointCloudChangeAttribute = new Qgs3DMapToolPointCloudChangeAttribute( mCanvas ); + onPointCloudChangeAttributeSettingsChanged(); + mLabelPendingJobs = new QLabel( this ); mProgressPendingJobs = new QProgressBar( this ); mProgressPendingJobs->setRange( 0, 0 ); @@ -271,6 +294,7 @@ Qgs3DMapCanvasWidget::Qgs3DMapCanvasWidget( const QString &name, bool isDocked ) layout->setContentsMargins( 0, 0, 0, 0 ); layout->setSpacing( 0 ); layout->addLayout( topLayout ); + layout->addWidget( mEditingToolBar ); layout->addWidget( mMessageBar ); // mContainer takes ownership of Qgs3DMapCanvas @@ -317,6 +341,8 @@ Qgs3DMapCanvasWidget::Qgs3DMapCanvasWidget( const QString &name, bool isDocked ) connect( dockAction, &QAction::toggled, this, [=]( const bool isSmallSize ) { toolBar->setIconSize( QgisApp::instance()->iconSize( isSmallSize ) ); } ); + + updateLayerRelatedActions( QgisApp::instance()->activeLayer() ); } Qgs3DMapCanvasWidget::~Qgs3DMapCanvasWidget() @@ -377,12 +403,55 @@ void Qgs3DMapCanvasWidget::measureLine() mCanvas->setMapTool( action->isChecked() ? mMapToolMeasureLine : nullptr ); } +void Qgs3DMapCanvasWidget::changePointCloudAttribute() +{ + QAction *action = qobject_cast( sender() ); + if ( !action ) + return; + + mCanvas->setMapTool( action->isChecked() ? mMapToolPointCloudChangeAttribute : nullptr ); +} + void Qgs3DMapCanvasWidget::setCanvasName( const QString &name ) { mCanvasName = name; mDockableWidgetHelper->setWindowTitle( name ); } +void Qgs3DMapCanvasWidget::enableEditingTools( bool enable ) +{ + mEditingToolBar->setEnabled( enable ); +} + +void Qgs3DMapCanvasWidget::updateLayerRelatedActions( QgsMapLayer *layer ) +{ + if ( !layer || layer->type() != Qgis::LayerType::PointCloud ) + { + enableEditingTools( false ); + + if ( mCanvas->mapTool() == mMapToolPointCloudChangeAttribute ) + mCanvas->setMapTool( nullptr ); + + return; + } + + QgsPointCloudLayer *pcLayer = qobject_cast( layer ); + const QVector attributes = pcLayer->attributes().attributes(); + const QString previousAttribute = mCboChangeAttribute->currentText(); + whileBlocking( mCboChangeAttribute )->clear(); + for ( const QgsPointCloudAttribute &attribute : attributes ) + { + if ( attribute.name() == QLatin1String( "X" ) || attribute.name() == QLatin1String( "Y" ) || attribute.name() == QLatin1String( "Z" ) ) + continue; + + whileBlocking( mCboChangeAttribute )->addItem( attribute.name() ); + } + if ( mCboChangeAttribute->findText( previousAttribute ) != -1 ) + mCboChangeAttribute->setCurrentText( previousAttribute ); + + enableEditingTools( pcLayer->isEditable() ); +} + void Qgs3DMapCanvasWidget::toggleNavigationWidget( bool visibility ) { mNavigationWidget->setVisible( visibility ); @@ -734,6 +803,130 @@ void Qgs3DMapCanvasWidget::onGpuMemoryLimitReached() mGpuMemoryLimitReachedReported = true; } +void Qgs3DMapCanvasWidget::onPointCloudChangeAttributeSettingsChanged() +{ + const QString attributeName = mCboChangeAttribute->currentText(); + + if ( attributeName == QLatin1String( "Intensity" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 65535 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "ReturnNumber" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 15 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "NumberOfReturns" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 15 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "Synthetic" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 1 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "KeyPoint" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 1 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "Withheld" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 1 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "Overlap" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 1 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "ScannerChannel" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 3 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "ScanDirectionFlag" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 1 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "EdgeOfFlightLine" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 1 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "Classification" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 255 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "UserData" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 255 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "ScanAngleRank" ) ) + { + mSpinChangeAttributeValue->setMinimum( -30'000 ); + mSpinChangeAttributeValue->setMaximum( 30'000 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "PointSourceId" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 65535 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "GpsTime" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( std::numeric_limits::max() ); + mSpinChangeAttributeValue->setDecimals( 42 ); + } + else if ( attributeName == QLatin1String( "Red" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 65535 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "Green" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 65535 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "Blue" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 65535 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + else if ( attributeName == QLatin1String( "Infrared" ) ) + { + mSpinChangeAttributeValue->setMinimum( 0 ); + mSpinChangeAttributeValue->setMaximum( 65535 ); + mSpinChangeAttributeValue->setDecimals( 0 ); + } + + mMapToolPointCloudChangeAttribute->setAttribute( attributeName ); + // TODO: validate values for attribute + mMapToolPointCloudChangeAttribute->setNewValue( mSpinChangeAttributeValue->value() ); +} + void Qgs3DMapCanvasWidget::setSceneExtentOn2DCanvas() { if ( !qobject_cast( mMainCanvas->mapTool() ) ) diff --git a/src/app/3d/qgs3dmapcanvaswidget.h b/src/app/3d/qgs3dmapcanvaswidget.h index 0a941879998d..85011ebe03d3 100644 --- a/src/app/3d/qgs3dmapcanvaswidget.h +++ b/src/app/3d/qgs3dmapcanvaswidget.h @@ -16,14 +16,16 @@ #ifndef QGS3DMAPCANVASWIDGET_H #define QGS3DMAPCANVASWIDGET_H -#include "qmenu.h" #include "qgsdockwidget.h" #include "qgis_app.h" #include "qobjectuniqueptr.h" -#include "qtoolbutton.h" #include "qgsrectangle.h" +#include +#include #include +#include +#include #define SIP_NO_FILE @@ -35,14 +37,17 @@ class Qgs3DMapCanvas; class Qgs3DMapSettings; class Qgs3DMapToolIdentify; class Qgs3DMapToolMeasureLine; +class Qgs3DMapToolPointCloudChangeAttribute; class Qgs3DNavigationWidget; class Qgs3DDebugWidget; +class QgsMapLayer; class QgsMapTool; class QgsMapToolExtent; class QgsMapCanvas; class QgsDockableWidgetHelper; class QgsMessageBar; class QgsRubberBand; +class QgsDoubleSpinBox; class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget { @@ -69,6 +74,10 @@ class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget void showAnimationWidget() { mActionAnim->trigger(); } + void enableEditingTools( bool enable ); + + void updateLayerRelatedActions( QgsMapLayer *layer ); + private slots: void resetView(); void configure(); @@ -77,6 +86,7 @@ class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget void cameraControl(); void identify(); void measureLine(); + void changePointCloudAttribute(); void exportScene(); void toggleNavigationWidget( bool visibility ); void toggleFpsCounter( bool visibility ); @@ -100,6 +110,8 @@ class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget void onExtentChanged(); void onGpuMemoryLimitReached(); + void onPointCloudChangeAttributeSettingsChanged(); + private: QString mCanvasName; Qgs3DMapCanvas *mCanvas = nullptr; @@ -112,6 +124,7 @@ class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget QTimer *mLabelNavSpeedHideTimeout = nullptr; Qgs3DMapToolIdentify *mMapToolIdentify = nullptr; Qgs3DMapToolMeasureLine *mMapToolMeasureLine = nullptr; + Qgs3DMapToolPointCloudChangeAttribute *mMapToolPointCloudChangeAttribute = nullptr; std::unique_ptr mMapToolExtent; QgsMapTool *mMapToolPrevious = nullptr; QMenu *mExportMenu = nullptr; @@ -144,6 +157,10 @@ class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget Qgs3DNavigationWidget *mNavigationWidget = nullptr; //! On-screen Debug widget Qgs3DDebugWidget *mDebugWidget = nullptr; + + QToolBar *mEditingToolBar = nullptr; + QComboBox *mCboChangeAttribute = nullptr; + QgsDoubleSpinBox *mSpinChangeAttributeValue = nullptr; }; #endif // QGS3DMAPCANVASWIDGET_H diff --git a/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.cpp b/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.cpp new file mode 100644 index 000000000000..660ce16968c0 --- /dev/null +++ b/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.cpp @@ -0,0 +1,255 @@ +/*************************************************************************** + qgs3dmaptoolpointcloudchangeattribute.cpp + --------------------- + begin : January 2025 + copyright : (C) 2025 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgs3dmaptoolpointcloudchangeattribute.h" +#include "moc_qgs3dmaptoolpointcloudchangeattribute.cpp" +#include "qgs3dmapcanvaswidget.h" +#include "qgs3dmapcanvas.h" +#include "qgs3dmapscene.h" +#include "qgsrubberband3d.h" +#include "qgswindow3dengine.h" +#include "qgsframegraph.h" +#include "qgs3dutils.h" +#include "qgscameracontroller.h" +#include "qgspolygon.h" +#include "qgspointcloudlayer.h" +#include "qgsmultipoint.h" +#include "qgsguiutils.h" +#include "qgisapp.h" + +#include + +Qgs3DMapToolPointCloudChangeAttribute::Qgs3DMapToolPointCloudChangeAttribute( Qgs3DMapCanvas *canvas ) + : Qgs3DMapTool( canvas ) +{ +} + +Qgs3DMapToolPointCloudChangeAttribute::~Qgs3DMapToolPointCloudChangeAttribute() = default; + +void Qgs3DMapToolPointCloudChangeAttribute::mousePressEvent( QMouseEvent *event ) +{ + mClickPoint = event->pos(); +} + +void Qgs3DMapToolPointCloudChangeAttribute::mouseMoveEvent( QMouseEvent *event ) +{ + const QgsPoint movedPoint = screenPointToMap( event->pos() ); + mPolygonRubberBand->moveLastPoint( movedPoint ); +} + +void Qgs3DMapToolPointCloudChangeAttribute::keyPressEvent( QKeyEvent *event ) +{ + if ( event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete ) + { + if ( mScreenPoints.isEmpty() ) + { + return; + } + else if ( mScreenPoints.size() == 1 ) + { + //removing first point, so restart everything + restart(); + } + else + { + mScreenPoints.removeLast(); + mPolygonRubberBand->removeLastPoint(); + } + } + else if ( event->key() == Qt::Key_Escape ) + { + restart(); + } +} + +void Qgs3DMapToolPointCloudChangeAttribute::mouseReleaseEvent( QMouseEvent *event ) +{ + if ( ( event->pos() - mClickPoint ).manhattanLength() > QApplication::startDragDistance() ) + return; + + const QgsPoint newPoint = screenPointToMap( event->pos() ); + + if ( event->button() == Qt::LeftButton ) + { + if ( mPolygonRubberBand->isEmpty() ) + { + mPolygonRubberBand->addPoint( newPoint ); + mCanvas->cameraController()->setInputHandlersEnabled( false ); + } + mPolygonRubberBand->addPoint( newPoint ); + mScreenPoints.append( QgsPointXY( event->x(), event->y() ) ); + } + else if ( event->button() == Qt::RightButton ) + { + run(); + restart(); + } +} + +void Qgs3DMapToolPointCloudChangeAttribute::activate() +{ + // cannot move this to the costructor as there are no mapSettings available yet when the tool is created + if ( !mPolygonRubberBand ) + { + mPolygonRubberBand = new QgsRubberBand3D( *mCanvas->mapSettings(), mCanvas->engine(), mCanvas->engine()->frameGraph()->rubberBandsRootEntity(), Qgis::GeometryType::Polygon ); + mPolygonRubberBand->setHideLastMarker( true ); + } +} + +void Qgs3DMapToolPointCloudChangeAttribute::deactivate() +{ + restart(); +} + +void Qgs3DMapToolPointCloudChangeAttribute::setAttribute( const QString &attribute ) +{ + mAttributeName = attribute; +} + +void Qgs3DMapToolPointCloudChangeAttribute::setNewValue( double value ) +{ + mNewValue = value; +} + +void Qgs3DMapToolPointCloudChangeAttribute::run() +{ + if ( mScreenPoints.size() < 3 ) + return; + + QgsTemporaryCursorOverride busyCursor( Qt::WaitCursor ); + + const QgsGeometry searchPolygon = QgsGeometry( new QgsPolygon( new QgsLineString( mScreenPoints ) ) ); + + QgsMapLayer *mapLayer = QgisApp::instance()->activeLayer(); + Q_ASSERT( mapLayer->type() == Qgis::LayerType::PointCloud ); + QgsPointCloudLayer *pcLayer = qobject_cast( mapLayer ); + const SelectedPoints sel = searchPoints( pcLayer, searchPolygon ); + + int offset; + const QgsPointCloudAttribute *attribute = pcLayer->attributes().find( mAttributeName, offset ); + + for ( auto it = sel.begin(); it != sel.end(); ++it ) + { + pcLayer->changeAttributeValue( it.key(), it.value(), *attribute, mNewValue ); + } +} + +void Qgs3DMapToolPointCloudChangeAttribute::restart() +{ + mCanvas->cameraController()->setInputHandlersEnabled( true ); + mScreenPoints.clear(); + mPolygonRubberBand->reset(); +} + + +QgsPoint Qgs3DMapToolPointCloudChangeAttribute::screenPointToMap( const QPoint &pos ) const +{ + const QgsRay3D ray = Qgs3DUtils::rayFromScreenPoint( pos, mCanvas->size(), mCanvas->cameraController()->camera() ); + + // pick an arbitrary point mid-way between near and far plane + const float pointDistance = ( mCanvas->cameraController()->camera()->farPlane() + mCanvas->cameraController()->camera()->nearPlane() ) / 2; + const QVector3D pointWorld = ray.origin() + pointDistance * ray.direction().normalized(); + + const QgsVector3D origin = mCanvas->mapSettings()->origin(); + const QgsPoint pointMap( pointWorld.x() + origin.x(), pointWorld.y() + origin.y(), pointWorld.z() + origin.z() ); + return pointMap; +} + +QgsGeometry Qgs3DMapToolPointCloudChangeAttribute::box3DToPolygonInScreenSpace( QgsBox3D box, const MapToPixel3D &mapToPixel3D ) +{ + QVector pts; + for ( QgsVector3D c : box.corners() ) + { + const QPointF pt = mapToPixel3D.transform( c.x(), c.y(), c.z() ); + pts.append( QgsPointXY( pt.x(), pt.y() ) ); + } + + // TODO: maybe we should only do rectangle check rather than (more precise) convex hull? + + // combine into QgsMultiPoint + apply convex hull + const QgsGeometry g( new QgsMultiPoint( pts ) ); + return g.convexHull(); +} + +SelectedPoints Qgs3DMapToolPointCloudChangeAttribute::searchPoints( QgsPointCloudLayer *layer, const QgsGeometry &searchPolygon ) const +{ + SelectedPoints result; + + MapToPixel3D mapToPixel3D; + mapToPixel3D.VP = mCanvas->camera()->projectionMatrix() * mCanvas->camera()->viewMatrix(); + mapToPixel3D.origin = mCanvas->mapSettings()->origin(); + mapToPixel3D.canvasSize = mCanvas->size(); + + QgsPointCloudIndex pcIndex = layer->index(); + const QVector chunks = mCanvas->scene()->getLayerActiveChunkNodes( layer ); + for ( const QgsChunkNode *chunk : chunks ) + { + // check whether the hull intersects the search polygon + const QgsGeometry hull = box3DToPolygonInScreenSpace( chunk->box3D(), mapToPixel3D ); + if ( !hull.intersects( searchPolygon ) ) + continue; + + const QVector pts = selectedPointsInNode( searchPolygon, chunk, mapToPixel3D, pcIndex ); + if ( !pts.isEmpty() ) + { + const QgsPointCloudNodeId n( chunk->tileId().d, chunk->tileId().x, chunk->tileId().y, chunk->tileId().z ); + result.insert( n, pts ); + } + } + return result; +} + + +QVector Qgs3DMapToolPointCloudChangeAttribute::selectedPointsInNode( const QgsGeometry &searchPolygon, const QgsChunkNode *ch, const MapToPixel3D &mapToPixel3D, QgsPointCloudIndex &pcIndex ) +{ + QVector selected; + + const QgsPointCloudNodeId n( ch->tileId().d, ch->tileId().x, ch->tileId().y, ch->tileId().z ); + QgsPointCloudRequest request; + // TODO: apply filtering (if any) + request.setAttributes( pcIndex.attributes() ); + + // TODO: reuse cached block(s) if possible + + std::unique_ptr block( pcIndex.nodeData( n, request ) ); + if ( !block ) + return selected; + + const QgsVector3D blockScale = block->scale(); + const QgsVector3D blockOffset = block->offset(); + + const char *ptr = block->data(); + const QgsPointCloudAttributeCollection blockAttributes = block->attributes(); + const std::size_t recordSize = blockAttributes.pointRecordSize(); + int xOffset = 0, yOffset = 0, zOffset = 0; + const QgsPointCloudAttribute::DataType xType = blockAttributes.find( QStringLiteral( "X" ), xOffset )->type(); + const QgsPointCloudAttribute::DataType yType = blockAttributes.find( QStringLiteral( "Y" ), yOffset )->type(); + const QgsPointCloudAttribute::DataType zType = blockAttributes.find( QStringLiteral( "Z" ), zOffset )->type(); + for ( int i = 0; i < block->pointCount(); ++i ) + { + // get map coordinates + double x, y, z; + QgsPointCloudAttribute::getPointXYZ( ptr, i, recordSize, xOffset, xType, yOffset, yType, zOffset, zType, blockScale, blockOffset, x, y, z ); + + // project to screen (map coords -> world coords -> clip coords -> NDC -> screen coords) + const QPointF ptScreen = mapToPixel3D.transform( x, y, z ); + + if ( searchPolygon.intersects( QgsGeometry( new QgsPoint( ptScreen.x(), ptScreen.y() ) ) ) ) + { + selected.append( i ); + } + } + return selected; +} diff --git a/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.h b/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.h new file mode 100644 index 000000000000..0ff202a2751a --- /dev/null +++ b/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.h @@ -0,0 +1,89 @@ +/*************************************************************************** + qgs3dmaptoolpointcloudchangeattribute.h + --------------------- + begin : January 2025 + copyright : (C) 2025 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGS3DMAPTOOLPOINTCLOUDCHANGEATTRIBUTE_H +#define QGS3DMAPTOOLPOINTCLOUDCHANGEATTRIBUTE_H + +#include "qgs3dmaptool.h" +#include "qgspointxy.h" +#include "qgsgeometry.h" +#include "qgspointcloudindex.h" +#include "qgschunknode.h" + +#include +#include + +class QgsRubberBand3D; +class QgsPointCloudLayer; + + +struct MapToPixel3D +{ + QMatrix4x4 VP; // combined view-projection matrix + QgsVector3D origin; // shift of world coordinates + QSize canvasSize; + + QPointF transform( double x, double y, double z ) const + { + QVector4D cClip = VP * QVector4D( x - origin.x(), y - origin.y(), z - origin.z(), 1 ); + float xNdc = cClip.x() / cClip.w(); + float yNdc = cClip.y() / cClip.w(); + float xScreen = ( xNdc + 1 ) * 0.5 * canvasSize.width(); + float yScreen = ( -yNdc + 1 ) * 0.5 * canvasSize.height(); + return QPointF( xScreen, yScreen ); + } +}; + + +typedef QHash > SelectedPoints; + + +class Qgs3DMapToolPointCloudChangeAttribute : public Qgs3DMapTool +{ + Q_OBJECT + + public: + Qgs3DMapToolPointCloudChangeAttribute( Qgs3DMapCanvas *canvas ); + ~Qgs3DMapToolPointCloudChangeAttribute() override; + + void mousePressEvent( QMouseEvent *event ) override; + void mouseReleaseEvent( QMouseEvent *event ) override; + void mouseMoveEvent( QMouseEvent *event ) override; + void keyPressEvent( QKeyEvent *event ) override; + + void activate() override; + void deactivate() override; + + bool allowsCameraControls() const override { return false; } + + void setAttribute( const QString &attribute ); + void setNewValue( double value ); + + private: + void run(); + void restart(); + QgsPoint screenPointToMap( const QPoint &pos ) const; + SelectedPoints searchPoints( QgsPointCloudLayer *layer, const QgsGeometry &searchPolygon ) const; + static QVector selectedPointsInNode( const QgsGeometry &searchPolygon, const QgsChunkNode *ch, const MapToPixel3D &mapToPixel3D, QgsPointCloudIndex &pcIndex ); + static QgsGeometry box3DToPolygonInScreenSpace( QgsBox3D box, const MapToPixel3D &mapToPixel3D ); + + QVector mScreenPoints; + QgsRubberBand3D *mPolygonRubberBand = nullptr; + QString mAttributeName; + double mNewValue = 0; + QPoint mClickPoint; +}; + +#endif // QGS3DMAPTOOLPOINTCLOUDCHANGEATTRIBUTE_H diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 9f642ca0dfb1..d7e1797532dd 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -348,6 +348,7 @@ if (WITH_3D) 3d/qgs3dmapconfigwidget.cpp 3d/qgs3dmaptoolidentify.cpp 3d/qgs3dmaptoolmeasureline.cpp + 3d/qgs3dmaptoolpointcloudchangeattribute.cpp 3d/qgs3dmeasuredialog.cpp 3d/qgs3dmodelsourcelineedit.cpp 3d/qgs3dnavigationwidget.cpp diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 039ccef3c595..bc5c5eea0504 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -4792,6 +4792,17 @@ void QgisApp::closeAdditional3DMapCanvases() #endif } +void QgisApp::update3DMapViewsLayerRelatedActions() +{ +#ifdef HAVE_3D + QgsMapLayer *currentLayer = activeLayer(); + for ( Qgs3DMapCanvasWidget *w : mOpen3DMapViews ) + { + w->updateLayerRelatedActions( currentLayer ); + } +#endif +} + void QgisApp::freezeCanvases( bool frozen ) { const auto canvases = mapCanvases(); @@ -14874,6 +14885,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer *layer ) mActionPasteAsNewMemoryVector->setEnabled( clipboard() && !clipboard()->isEmpty() ); updateLayerModifiedActions(); + update3DMapViewsLayerRelatedActions(); QgsAbstractMapToolHandler::Context context; for ( QgsAbstractMapToolHandler *handler : std::as_const( mMapToolHandlers ) ) diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index d210ceb6ae16..b4ef0c428790 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -2298,6 +2298,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow //! Closes any existing 3D map docks void closeAdditional3DMapCanvases(); + //! Updates current layer related actions on all open 3d views + void update3DMapViewsLayerRelatedActions(); + QgsElevationProfileWidget *createNewElevationProfile(); /** diff --git a/src/core/pointcloud/qgspointcloudlayer.cpp b/src/core/pointcloud/qgspointcloudlayer.cpp index a506a5a19786..8a14e8d7b672 100644 --- a/src/core/pointcloud/qgspointcloudlayer.cpp +++ b/src/core/pointcloud/qgspointcloudlayer.cpp @@ -1082,6 +1082,8 @@ bool QgsPointCloudLayer::changeAttributeValue( const QgsPointCloudNodeId &n, con if ( success ) { emit layerModified(); + emit triggerRepaint(); + emit trigger3DUpdate(); } return success; diff --git a/src/core/pointcloud/qgspointcloudlayereditutils.cpp b/src/core/pointcloud/qgspointcloudlayereditutils.cpp index e29c19a8db5d..f7995a76167a 100644 --- a/src/core/pointcloud/qgspointcloudlayereditutils.cpp +++ b/src/core/pointcloud/qgspointcloudlayereditutils.cpp @@ -262,7 +262,7 @@ bool QgsPointCloudLayerEditUtils::isAttributeValueValid( const QgsPointCloudAttr return value >= 0 && value <= 15; if ( name == QLatin1String( "NUMBEROFRETURNS" ) ) return value >= 0 && value <= 15; - if ( name == QLatin1String( "SCANCHANNEL" ) ) + if ( name == QLatin1String( "SCANNERCHANNEL" ) ) return value >= 0 && value <= 3; if ( name == QLatin1String( "SCANDIRECTIONFLAG" ) ) return value >= 0 && value <= 1; @@ -272,7 +272,7 @@ bool QgsPointCloudLayerEditUtils::isAttributeValueValid( const QgsPointCloudAttr return value >= 0 && value <= 255; if ( name == QLatin1String( "USERDATA" ) ) return value >= 0 && value <= 255; - if ( name == QLatin1String( "SCANANGLE" ) ) + if ( name == QLatin1String( "SCANANGLERANK" ) ) return value >= -30'000 && value <= 30'000; if ( name == QLatin1String( "POINTSOURCEID" ) ) return value >= 0 && value <= 65535; @@ -292,6 +292,8 @@ bool QgsPointCloudLayerEditUtils::isAttributeValueValid( const QgsPointCloudAttr return value >= 0 && value <= 65535; if ( name == QLatin1String( "BLUE" ) ) return value >= 0 && value <= 65535; + if ( name == QLatin1String( "INFRARED" ) ) + return value >= 0 && value <= 65535; return true; }