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