1 /***************************************************************************
2   qgs3dmaptoolidentify.cpp
3   --------------------------------------
4   Date                 : Sep 2018
5   Copyright            : (C) 2018 by Martin Dobias
6   Email                : wonder dot sk at gmail dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 
16 #include "qgs3dmaptoolidentify.h"
17 
18 #include "qgsapplication.h"
19 #include "qgs3dmapcanvas.h"
20 #include "qgs3dmapcanvasdockwidget.h"
21 #include "qgs3dmapscene.h"
22 #include "qgs3dutils.h"
23 #include "qgsterrainentity_p.h"
24 #include "qgsvector3d.h"
25 
26 #include "qgisapp.h"
27 #include "qgsmapcanvas.h"
28 #include "qgsmaptoolidentifyaction.h"
29 
30 #include <Qt3DRender/QObjectPicker>
31 #include <Qt3DRender/QPickEvent>
32 
33 #include "qgspointcloudlayer.h"
34 #include "qgspointcloudlayerelevationproperties.h"
35 #include "qgspointcloudattribute.h"
36 #include "qgspointcloudrequest.h"
37 #include "qgspointcloudlayer3drenderer.h"
38 
39 #include "qgs3dmapscenepickhandler.h"
40 #include "qgs3dutils.h"
41 #include "qgscameracontroller.h"
42 
43 class Qgs3DMapToolIdentifyPickHandler : public Qgs3DMapScenePickHandler
44 {
45   public:
Qgs3DMapToolIdentifyPickHandler(Qgs3DMapToolIdentify * identifyTool)46     Qgs3DMapToolIdentifyPickHandler( Qgs3DMapToolIdentify *identifyTool ): mIdentifyTool( identifyTool ) {}
47     void handlePickOnVectorLayer( QgsVectorLayer *vlayer, QgsFeatureId id, const QVector3D &worldIntersection, Qt3DRender::QPickEvent *event ) override;
48   private:
49     Qgs3DMapToolIdentify *mIdentifyTool = nullptr;
50 };
51 
52 
handlePickOnVectorLayer(QgsVectorLayer * vlayer,QgsFeatureId id,const QVector3D & worldIntersection,Qt3DRender::QPickEvent * event)53 void Qgs3DMapToolIdentifyPickHandler::handlePickOnVectorLayer( QgsVectorLayer *vlayer, QgsFeatureId id, const QVector3D &worldIntersection, Qt3DRender::QPickEvent *event )
54 {
55   if ( event->button() == Qt3DRender::QPickEvent::LeftButton )
56   {
57     const QgsVector3D mapCoords = Qgs3DUtils::worldToMapCoordinates( QgsVector3D( worldIntersection.x(),
58                                   worldIntersection.y(),
59                                   worldIntersection.z() ), mIdentifyTool->mCanvas->map()->origin() );
60     const QgsPoint pt( mapCoords.x(), mapCoords.y(), mapCoords.z() );
61 
62     QgsMapToolIdentifyAction *identifyTool2D = QgisApp::instance()->identifyMapTool();
63     identifyTool2D->showResultsForFeature( vlayer, id, pt );
64   }
65 }
66 
67 
68 //////
69 
70 
Qgs3DMapToolIdentify(Qgs3DMapCanvas * canvas)71 Qgs3DMapToolIdentify::Qgs3DMapToolIdentify( Qgs3DMapCanvas *canvas )
72   : Qgs3DMapTool( canvas )
73 {
74   mPickHandler.reset( new Qgs3DMapToolIdentifyPickHandler( this ) );
75   connect( canvas, &Qgs3DMapCanvas::mapSettingsChanged, this, &Qgs3DMapToolIdentify::onMapSettingsChanged );
76 }
77 
78 Qgs3DMapToolIdentify::~Qgs3DMapToolIdentify() = default;
79 
80 
mousePressEvent(QMouseEvent * event)81 void Qgs3DMapToolIdentify::mousePressEvent( QMouseEvent *event )
82 {
83   Q_UNUSED( event )
84 
85   QgsMapToolIdentifyAction *identifyTool2D = QgisApp::instance()->identifyMapTool();
86   identifyTool2D->clearResults();
87 }
88 
mouseReleaseEvent(QMouseEvent * event)89 void Qgs3DMapToolIdentify::mouseReleaseEvent( QMouseEvent *event )
90 {
91   if ( event->button() != Qt::MouseButton::LeftButton )
92     return;
93 
94   // point cloud identification
95   QVector<QPair<QgsMapLayer *, QVector<QVariantMap>>> layerPoints;
96   Qgs3DMapCanvas *canvas = this->canvas();
97 
98   const QgsRay3D ray = Qgs3DUtils::rayFromScreenPoint( event->pos(), canvas->windowSize(), canvas->cameraController()->camera() );
99 
100   QMap<QgsPointCloudLayer *, QVector<IndexedPointCloudNode>> layerChunks;
101   for ( QgsMapLayer *layer : canvas->map()->layers() )
102   {
103     if ( QgsPointCloudLayer *pc = qobject_cast<QgsPointCloudLayer *>( layer ) )
104     {
105       QVector<IndexedPointCloudNode> pointCloudNodes;
106       for ( const QgsChunkNode *n : canvas->scene()->getLayerActiveChunkNodes( pc ) )
107       {
108         const QgsChunkNodeId id = n->tileId();
109         pointCloudNodes.push_back( IndexedPointCloudNode( id.d, id.x, id.y, id.z ) );
110       }
111       if ( pointCloudNodes.empty() )
112         continue;
113       layerChunks[ pc ] = pointCloudNodes;
114     }
115   }
116 
117   for ( QgsPointCloudLayer *layer : layerChunks.keys() )
118   {
119     // transform ray
120     const QgsVector3D originMapCoords = canvas->map()->worldToMapCoordinates( ray.origin() );
121     const QgsVector3D pointMapCoords = canvas->map()->worldToMapCoordinates( ray.origin() + ray.origin().length() * ray.direction().normalized() );
122     QgsVector3D directionMapCoords = pointMapCoords - originMapCoords;
123     directionMapCoords.normalize();
124 
125     const QVector3D rayOriginMapCoords( originMapCoords.x(), originMapCoords.y(), originMapCoords.z() );
126     const QVector3D rayDirectionMapCoords( directionMapCoords.x(), directionMapCoords.y(), directionMapCoords.z() );
127 
128     const QRect rect = canvas->cameraController()->viewport();
129     const int screenSizePx = std::max( rect.width(), rect.height() ); // TODO: is this correct? (see _sceneState)
130     QgsPointCloudLayer3DRenderer *renderer = dynamic_cast<QgsPointCloudLayer3DRenderer *>( layer->renderer3D() );
131     const QgsPointCloud3DSymbol *symbol = renderer->symbol();
132     // Symbol can be null in case of no rendering enabled
133     if ( !symbol )
134       continue;
135     const double pointSize = symbol->pointSize();
136     const double limitAngle = 2 * pointSize / screenSizePx * canvas->cameraController()->camera()->fieldOfView();
137 
138     // adjust ray to elevation properties
139     QgsPointCloudLayerElevationProperties *elevationProps = dynamic_cast<QgsPointCloudLayerElevationProperties *>( layer->elevationProperties() );
140     const QVector3D adjutedRayOrigin = QVector3D( rayOriginMapCoords.x(), rayOriginMapCoords.y(), ( rayOriginMapCoords.z() -  elevationProps->zOffset() ) / elevationProps->zScale() );
141     QVector3D adjutedRayDirection = QVector3D( rayDirectionMapCoords.x(), rayDirectionMapCoords.y(), rayDirectionMapCoords.z() / elevationProps->zScale() );
142     adjutedRayDirection.normalize();
143 
144     const QgsRay3D layerRay( adjutedRayOrigin, adjutedRayDirection );
145 
146     QgsPointCloudDataProvider *provider = layer->dataProvider();
147     QgsPointCloudIndex *index = provider->index();
148     QVector<QVariantMap> points;
149     const QgsPointCloudAttributeCollection attributeCollection = index->attributes();
150     QgsPointCloudRequest request;
151     request.setAttributes( attributeCollection );
152     for ( const IndexedPointCloudNode &n : layerChunks[layer] )
153     {
154       if ( !index->hasNode( n ) )
155         continue;
156       std::unique_ptr<QgsPointCloudBlock> block( index->nodeData( n, request ) );
157       if ( !block )
158         continue;
159 
160       const QgsVector3D blockScale = block->scale();
161       const QgsVector3D blockOffset = block->offset();
162 
163       const char *ptr = block->data();
164       const QgsPointCloudAttributeCollection blockAttributes = block->attributes();
165       const std::size_t recordSize = blockAttributes.pointRecordSize();
166       int xOffset = 0, yOffset = 0, zOffset = 0;
167       const QgsPointCloudAttribute::DataType xType = blockAttributes.find( QStringLiteral( "X" ), xOffset )->type();
168       const QgsPointCloudAttribute::DataType yType = blockAttributes.find( QStringLiteral( "Y" ), yOffset )->type();
169       const QgsPointCloudAttribute::DataType zType = blockAttributes.find( QStringLiteral( "Z" ), zOffset )->type();
170       for ( int i = 0; i < block->pointCount(); ++i )
171       {
172         double x, y, z;
173         QgsPointCloudAttribute::getPointXYZ( ptr, i, recordSize, xOffset, xType, yOffset, yType, zOffset, zType, blockScale, blockOffset, x, y, z );
174         const QVector3D point( x, y, z );
175 
176         // check whether point is in front of the ray
177         if ( !layerRay.isInFront( point ) )
178           continue;
179 
180         // calculate the angle between the point and the projected point
181         if ( layerRay.angleToPoint( point ) > limitAngle )
182           continue;
183 
184         // Note : applying elevation properties is done in fromPointCloudIdentificationToIdentifyResults
185         QVariantMap pointAttr = QgsPointCloudAttribute::getAttributeMap( ptr, i * recordSize, blockAttributes );
186         pointAttr[ QStringLiteral( "X" ) ] = x;
187         pointAttr[ QStringLiteral( "Y" ) ] = y;
188         pointAttr[ QStringLiteral( "Z" ) ] = z;
189         pointAttr[ tr( "Distance to camera" ) ] = ( point - layerRay.origin() ).length();
190         points.push_back( pointAttr );
191       }
192 
193     }
194     layerPoints.push_back( qMakePair( layer, points ) );
195   }
196 
197   QList<QgsMapToolIdentify::IdentifyResult> identifyResults;
198   for ( int i = 0; i < layerPoints.size(); ++i )
199   {
200     QgsPointCloudLayer *pcLayer = qobject_cast< QgsPointCloudLayer * >( layerPoints[i].first );
201     QgsMapToolIdentify::fromPointCloudIdentificationToIdentifyResults( pcLayer, layerPoints[i].second, identifyResults );
202   }
203 
204   QgsMapToolIdentifyAction *identifyTool2D = QgisApp::instance()->identifyMapTool();
205   identifyTool2D->showIdentifyResults( identifyResults );
206 }
207 
activate()208 void Qgs3DMapToolIdentify::activate()
209 {
210   if ( QgsTerrainEntity *terrainEntity = mCanvas->scene()->terrainEntity() )
211   {
212     bool disableTerrainPicker = false;
213     const Qgs3DMapSettings &map = terrainEntity->map3D();
214     // if the terrain contains point cloud data disable the terrain picker signals
215     for ( QgsMapLayer *layer : map.layers() )
216     {
217       if ( layer->type() == QgsMapLayerType::PointCloudLayer )
218       {
219         disableTerrainPicker = true;
220         break;
221       }
222     }
223     if ( !disableTerrainPicker )
224       connect( terrainEntity->terrainPicker(), &Qt3DRender::QObjectPicker::clicked, this, &Qgs3DMapToolIdentify::onTerrainPicked );
225   }
226 
227   mCanvas->scene()->registerPickHandler( mPickHandler.get() );
228   mIsActive = true;
229 }
230 
deactivate()231 void Qgs3DMapToolIdentify::deactivate()
232 {
233   if ( QgsTerrainEntity *terrainEntity = mCanvas->scene()->terrainEntity() )
234   {
235     disconnect( terrainEntity->terrainPicker(), &Qt3DRender::QObjectPicker::clicked, this, &Qgs3DMapToolIdentify::onTerrainPicked );
236   }
237 
238   mCanvas->scene()->unregisterPickHandler( mPickHandler.get() );
239   mIsActive = false;
240 }
241 
cursor() const242 QCursor Qgs3DMapToolIdentify::cursor() const
243 {
244   return QgsApplication::getThemeCursor( QgsApplication::Cursor::Identify );
245 }
246 
onMapSettingsChanged()247 void Qgs3DMapToolIdentify::onMapSettingsChanged()
248 {
249   if ( !mIsActive )
250     return;
251   connect( mCanvas->scene(), &Qgs3DMapScene::terrainEntityChanged, this, &Qgs3DMapToolIdentify::onTerrainEntityChanged );
252 }
253 
onTerrainPicked(Qt3DRender::QPickEvent * event)254 void Qgs3DMapToolIdentify::onTerrainPicked( Qt3DRender::QPickEvent *event )
255 {
256   if ( event->button() != Qt3DRender::QPickEvent::LeftButton )
257     return;
258 
259   const QVector3D worldIntersection = event->worldIntersection();
260   const QgsVector3D mapCoords = Qgs3DUtils::worldToMapCoordinates( QgsVector3D( worldIntersection.x(),
261                                 worldIntersection.y(),
262                                 worldIntersection.z() ), mCanvas->map()->origin() );
263   const QgsPointXY mapPoint( mapCoords.x(), mapCoords.y() );
264 
265   // estimate search radius
266   Qgs3DMapScene *scene = mCanvas->scene();
267   const double searchRadiusMM = QgsMapTool::searchRadiusMM();
268   const double pixelsPerMM = mCanvas->logicalDpiX() / 25.4;
269   const double searchRadiusPx = searchRadiusMM * pixelsPerMM;
270   const double searchRadiusMapUnits = scene->worldSpaceError( searchRadiusPx, event->distance() );
271 
272   QgsMapToolIdentifyAction *identifyTool2D = QgisApp::instance()->identifyMapTool();
273   QgsMapCanvas *canvas2D = identifyTool2D->canvas();
274 
275   // transform the point and search radius to CRS of the map canvas (if they are different)
276   const QgsCoordinateTransform ct( mCanvas->map()->crs(), canvas2D->mapSettings().destinationCrs(), canvas2D->mapSettings().transformContext() );
277 
278   QgsPointXY mapPointCanvas2D = mapPoint;
279   double searchRadiusCanvas2D = searchRadiusMapUnits;
280   try
281   {
282     mapPointCanvas2D = ct.transform( mapPoint );
283     const QgsPointXY mapPointSearchRadius( mapPoint.x() + searchRadiusMapUnits, mapPoint.y() );
284     const QgsPointXY mapPointSearchRadiusCanvas2D = ct.transform( mapPointSearchRadius );
285     searchRadiusCanvas2D = mapPointCanvas2D.distance( mapPointSearchRadiusCanvas2D );
286   }
287   catch ( QgsException &e )
288   {
289     Q_UNUSED( e )
290     QgsDebugMsg( QStringLiteral( "Caught exception %1" ).arg( e.what() ) );
291   }
292 
293   identifyTool2D->identifyAndShowResults( QgsGeometry::fromPointXY( mapPointCanvas2D ), searchRadiusCanvas2D );
294 }
295 
onTerrainEntityChanged()296 void Qgs3DMapToolIdentify::onTerrainEntityChanged()
297 {
298   if ( !mIsActive )
299     return;
300   // no need to disconnect from the previous entity: it has been destroyed
301   // start listening to the new terrain entity
302   if ( QgsTerrainEntity *terrainEntity = mCanvas->scene()->terrainEntity() )
303   {
304     connect( terrainEntity->terrainPicker(), &Qt3DRender::QObjectPicker::clicked, this, &Qgs3DMapToolIdentify::onTerrainPicked );
305   }
306 }
307