QGIS API Documentation 4.1.0-Master (3b8ef1f72a3)
Loading...
Searching...
No Matches
qgscameracontroller.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscameracontroller.cpp
3 --------------------------------------
4 Date : July 2017
5 Copyright : (C) 2017 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 "qgscameracontroller.h"
17
18#include <cmath>
19
20#include "qgis.h"
21#include "qgs3dmapscene.h"
22#include "qgs3dutils.h"
23#include "qgscrosssection.h"
24#include "qgseventtracing.h"
25#include "qgslogger.h"
26#include "qgsray3d.h"
27#include "qgsraycastcontext.h"
28#include "qgsraycasthit.h"
29#include "qgsterrainentity.h"
30#include "qgsvector3d.h"
31#include "qgswindow3dengine.h"
32
33#include <QDomDocument>
34#include <QQuaternion>
35#include <QString>
36#include <QStringLiteral>
37#include <Qt3DInput>
38#include <Qt3DRender/QCamera>
39
40#include "moc_qgscameracontroller.cpp"
41
42using namespace Qt::StringLiterals;
43
45 : Qt3DCore::QEntity( scene )
46 , mScene( scene )
47 , mCamera( scene->engine()->camera() )
48 , mCameraBefore( new Qt3DRender::QCamera )
49 , mMouseHandler( new Qt3DInput::QMouseHandler )
50 , mKeyboardHandler( new Qt3DInput::QKeyboardHandler )
51 , mOrigin( scene->mapSettings()->origin() )
52{
53 mMouseHandler->setSourceDevice( new Qt3DInput::QMouseDevice() );
54 connect( mMouseHandler, &Qt3DInput::QMouseHandler::positionChanged, this, &QgsCameraController::onPositionChanged );
55 connect( mMouseHandler, &Qt3DInput::QMouseHandler::wheel, this, &QgsCameraController::onWheel );
56 connect( mMouseHandler, &Qt3DInput::QMouseHandler::pressed, this, &QgsCameraController::onMousePressed );
57 connect( mMouseHandler, &Qt3DInput::QMouseHandler::released, this, &QgsCameraController::onMouseReleased );
58 addComponent( mMouseHandler );
59
60 // Disable the handlers when the entity is disabled
61 connect( this, &Qt3DCore::QEntity::enabledChanged, mMouseHandler, &Qt3DInput::QMouseHandler::setEnabled );
62 connect( this, &Qt3DCore::QEntity::enabledChanged, mKeyboardHandler, &Qt3DInput::QKeyboardHandler::setEnabled );
63
64 mFpsNavTimer = new QTimer( this );
65 mFpsNavTimer->setInterval( 10 );
66 connect( mFpsNavTimer, &QTimer::timeout, this, &QgsCameraController::applyFlyModeKeyMovements );
67 mFpsNavTimer->start();
68
69 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
70 {
71 setCameraNavigationMode( mScene->mapSettings()->cameraNavigationMode() );
72 mGlobeCrsToLatLon = QgsCoordinateTransform( mScene->mapSettings()->crs(), mScene->mapSettings()->crs().toGeographicCrs(), mScene->mapSettings()->transformContext() );
73 }
74}
75
77
78QWindow *QgsCameraController::window() const
79{
80 QgsWindow3DEngine *windowEngine = qobject_cast<QgsWindow3DEngine *>( mScene->engine() );
81 return windowEngine ? windowEngine->window() : nullptr;
82}
83
85{
86 if ( navigationMode == mCameraNavigationMode )
87 return;
88
89 mCameraNavigationMode = navigationMode;
90 mIgnoreNextMouseMove = true;
91 emit navigationModeChanged( mCameraNavigationMode );
92}
93
95{
96 if ( movementSpeed == mCameraMovementSpeed )
97 return;
98
99 // If the speed becomes 0, navigation does not work anymore
100 // If the speed becomes too important, only one walk can move the view far from the scene.
101 mCameraMovementSpeed = std::clamp( movementSpeed, 0.05, 150.0 );
102 emit cameraMovementSpeedChanged( mCameraMovementSpeed );
103}
104
106{
107 mVerticalAxisInversion = inversion;
108}
109
110void QgsCameraController::rotateCamera( float diffPitch, float diffHeading )
111{
112 float newPitch = mCameraPose.pitchAngle() + diffPitch;
113 float newHeading = mCameraPose.headingAngle() + diffHeading;
114
115 newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
116
117 switch ( mScene->mapSettings()->sceneMode() )
118 {
120 {
121 // When on a globe, we need to calculate "where is up" (the normal of a tangent plane).
122 // Also it uses different axes than the standard plane-based view.
123 // See QgsCameraPose::updateCameraGlobe(), we basically want to make sure
124 // that after an update, the camera stays in the same spot.
125 QgsVector3D viewCenterLatLon;
126 try
127 {
128 viewCenterLatLon = mGlobeCrsToLatLon.transform( mCameraPose.centerPoint() + mOrigin );
129 }
130 catch ( const QgsCsException & )
131 {
132 QgsDebugError( u"rotateCamera: ECEF -> lat,lon transform failed!"_s );
133 return;
134 }
135 QQuaternion qLatLon = QQuaternion::fromAxisAndAngle( QVector3D( 0, 0, 1 ), static_cast<float>( viewCenterLatLon.x() ) )
136 * QQuaternion::fromAxisAndAngle( QVector3D( 0, -1, 0 ), static_cast<float>( viewCenterLatLon.y() ) );
137 QQuaternion qPitchHeading = QQuaternion::fromAxisAndAngle( QVector3D( 1, 0, 0 ), newHeading ) * QQuaternion::fromAxisAndAngle( QVector3D( 0, 1, 0 ), newPitch );
138 QVector3D newCameraToCenter = ( qLatLon * qPitchHeading * QVector3D( -1, 0, 0 ) ) * mCameraPose.distanceFromCenterPoint();
139
140 mCameraPose.setCenterPoint( mCamera->position() + newCameraToCenter );
141 mCameraPose.setPitchAngle( newPitch );
142 mCameraPose.setHeadingAngle( newHeading );
143 updateCameraFromPose();
144 return;
145 }
146
148 {
149 QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
150 QVector3D newCameraToCenter = q * QVector3D( 0, 0, -mCameraPose.distanceFromCenterPoint() );
151 mCameraPose.setCenterPoint( mCamera->position() + newCameraToCenter );
152 mCameraPose.setPitchAngle( newPitch );
153 mCameraPose.setHeadingAngle( newHeading );
154 updateCameraFromPose();
155 return;
156 }
157 }
158}
159
160void QgsCameraController::rotateCameraAroundPivot( float newPitch, float newHeading, const QVector3D &pivotPoint )
161{
162 const float oldPitch = mCameraPose.pitchAngle();
163 const float oldHeading = mCameraPose.headingAngle();
164
165 newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
166
167 // First undo the previously applied rotation, then apply the new rotation
168 // (We can't just apply our euler angles difference because the camera may be already rotated)
169 const QQuaternion qNew = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
170 const QQuaternion qOld = Qgs3DUtils::rotationFromPitchHeadingAngles( oldPitch, oldHeading );
171 const QQuaternion q = qNew * qOld.conjugated();
172
173 const QVector3D newViewCenter = q * ( mCamera->viewCenter() - pivotPoint ) + pivotPoint;
174
175 mCameraPose.setCenterPoint( newViewCenter );
176 mCameraPose.setPitchAngle( newPitch );
177 mCameraPose.setHeadingAngle( newHeading );
178 updateCameraFromPose();
179}
180
181void QgsCameraController::zoomCameraAroundPivot( const QVector3D &oldCameraPosition, double zoomFactor, const QVector3D &pivotPoint )
182{
183 // step 1: move camera along the line connecting reference camera position and our pivot point
184 QVector3D newCamPosition = pivotPoint + ( oldCameraPosition - pivotPoint ) * zoomFactor;
185 double newDistance = mCameraPose.distanceFromCenterPoint() * zoomFactor;
186
187 // step 2: using the new camera position and distance from center, calculate new view center
188 QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( mCameraPose.pitchAngle(), mCameraPose.headingAngle() );
189 QVector3D cameraToCenter = q * QVector3D( 0, 0, -newDistance );
190 QVector3D newViewCenter = newCamPosition + cameraToCenter;
191
192 mCameraPose.setDistanceFromCenterPoint( newDistance );
193 mCameraPose.setCenterPoint( newViewCenter );
194 updateCameraFromPose();
195}
196
197void QgsCameraController::zoomCameraAroundPivot( const QVector3D &oldCameraPosition, double oldDistanceFromCenterPoint, double zoomFactor, const QVector3D &pivotPoint )
198{
199 // step 1: move camera along the line connecting reference camera position and our pivot point
200 QVector3D newCamPosition = pivotPoint + ( oldCameraPosition - pivotPoint ) * zoomFactor;
201 const double newDistance = oldDistanceFromCenterPoint * zoomFactor;
202
203 // step 2: using the new camera position and distance from center, calculate new view center
204 QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( mCameraPose.pitchAngle(), mCameraPose.headingAngle() );
205 QVector3D cameraToCenter = q * QVector3D( 0, 0, -newDistance );
206 QVector3D newViewCenter = newCamPosition + cameraToCenter;
207
208 mCameraPose.setDistanceFromCenterPoint( newDistance );
209 mCameraPose.setCenterPoint( newViewCenter );
210 updateCameraFromPose();
211}
212
214{
215 Q_UNUSED( dt )
216
217 // In case viewport size has changed
218 updateOrthographicProjectionPlane();
219
220 if ( mCameraChanged )
221 {
222 emit cameraChanged();
223 mCameraChanged = false;
224 }
225}
226
228{
229 QgsPointXY extentCenter = mScene->mapSettings()->extent().center();
230 QgsVector3D origin = mScene->mapSettings()->origin();
231 setViewFromTop( extentCenter.x() - origin.x(), extentCenter.y() - origin.y(), distance );
232}
233
234void QgsCameraController::setViewFromTop( float worldX, float worldY, float distance, float yaw )
235{
236 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
237 {
238 QgsDebugError( u"setViewFromTop() should not be used with globe!"_s );
239 return;
240 }
241
242 QgsCameraPose camPose;
243 QgsTerrainEntity *terrain = mScene->terrainEntity();
244 const float terrainElevationOffset = terrain ? terrain->terrainElevationOffset() : 0.0f;
245 camPose.setCenterPoint( QgsVector3D( worldX, worldY, terrainElevationOffset - mScene->mapSettings()->origin().z() ) );
247 camPose.setHeadingAngle( yaw );
248
249 // a basic setup to make frustum depth range long enough that it does not cull everything
250 mCamera->setNearPlane( distance / 2 );
251 mCamera->setFarPlane( distance * 2 );
252 // we force the updateCameraNearFarPlanes() in Qgs3DMapScene to properly set the planes
253 setCameraPose( camPose, true );
254}
255
257{
258 return mCameraPose.centerPoint();
259}
260
261void QgsCameraController::setLookingAtPoint( const QgsVector3D &point, float distance, float pitch, float yaw )
262{
263 QgsCameraPose camPose;
264 camPose.setCenterPoint( point );
266 camPose.setPitchAngle( pitch );
267 camPose.setHeadingAngle( yaw );
268 setCameraPose( camPose );
269}
270
272{
273 return lookingAtPoint() + mOrigin;
274}
275
277{
278 setLookingAtPoint( point - mOrigin, distance, pitch, yaw );
279}
280
281void QgsCameraController::setCameraPose( const QgsCameraPose &camPose, bool force )
282{
283 if ( camPose == mCameraPose && !force )
284 return;
285
286 mCameraPose = camPose;
287 updateCameraFromPose();
288}
289
290QDomElement QgsCameraController::writeXml( QDomDocument &doc ) const
291{
292 QDomElement elemCamera = doc.createElement( u"camera"_s );
293 // Save center point in map coordinates, since our world origin won't be
294 // the same on loading
295 QgsVector3D centerPoint = mCameraPose.centerPoint() + mOrigin;
296 elemCamera.setAttribute( u"xMap"_s, centerPoint.x() );
297 elemCamera.setAttribute( u"yMap"_s, centerPoint.y() );
298 elemCamera.setAttribute( u"zMap"_s, centerPoint.z() );
299 elemCamera.setAttribute( u"dist"_s, mCameraPose.distanceFromCenterPoint() );
300 elemCamera.setAttribute( u"pitch"_s, mCameraPose.pitchAngle() );
301 elemCamera.setAttribute( u"yaw"_s, mCameraPose.headingAngle() );
302 return elemCamera;
303}
304
305void QgsCameraController::readXml( const QDomElement &elem, QgsVector3D savedOrigin )
306{
307 const float dist = elem.attribute( u"dist"_s ).toFloat();
308 const float pitch = elem.attribute( u"pitch"_s ).toFloat();
309 const float yaw = elem.attribute( u"yaw"_s ).toFloat();
310
311 QgsVector3D centerPoint;
312 if ( elem.hasAttribute( "xMap" ) )
313 {
314 // Prefer newer point saved in map coordinates ...
315 const double x = elem.attribute( u"xMap"_s ).toDouble();
316 const double y = elem.attribute( u"yMap"_s ).toDouble();
317 const double z = elem.attribute( u"zMap"_s ).toDouble();
318 centerPoint = QgsVector3D( x, y, z ) - mOrigin;
319 }
320 else
321 {
322 // ... but allow use of older origin-relative coordinates.
323 const double x = elem.attribute( u"x"_s ).toDouble();
324 const double y = elem.attribute( u"y"_s ).toDouble();
325 const double elev = elem.attribute( u"elev"_s ).toDouble();
326 centerPoint = QgsVector3D( x, elev, y ) - savedOrigin + mOrigin;
327 }
328 setLookingAtPoint( centerPoint, dist, pitch, yaw );
329}
330
331double QgsCameraController::sampleDepthBuffer( int px, int py )
332{
333 if ( !mDepthBufferIsReady )
334 {
335 QgsDebugError( u"Asked to sample depth buffer, but depth buffer not ready!"_s );
336 }
337
338 double depth = 1;
339
340 if ( QWindow *win = window() )
341 {
342 // on high DPI screens, the mouse position is in device-independent pixels,
343 // but the depth buffer is in physical pixels...
344 px = static_cast<int>( px * win->devicePixelRatio() );
345 py = static_cast<int>( py * win->devicePixelRatio() );
346 }
347
348 // Sample the neighbouring pixels for the closest point to the camera
349 for ( int x = px - 3; x <= px + 3; ++x )
350 {
351 for ( int y = py - 3; y <= py + 3; ++y )
352 {
353 if ( mDepthBufferImage.valid( x, y ) )
354 {
355 depth = std::min( depth, Qgs3DUtils::decodeDepth( mDepthBufferImage.pixel( x, y ) ) );
356 }
357 }
358 }
359 return depth;
360}
361
362double QgsCameraController::depthBufferNonVoidAverage()
363{
364 // Cache the computed depth, since averaging over all pixels can be expensive
365 if ( mDepthBufferNonVoidAverage != -1 )
366 return mDepthBufferNonVoidAverage;
367
368 // Returns the average of depth values that are not 1 (void area)
369 double depth = 0;
370 int samplesCount = 0;
371 // Make sure we can do the cast
372 Q_ASSERT( mDepthBufferImage.format() == QImage::Format_RGB32 );
373 for ( int y = 0; y < mDepthBufferImage.height(); ++y )
374 {
375 const QRgb *line = reinterpret_cast<const QRgb *>( mDepthBufferImage.constScanLine( y ) );
376 for ( int x = 0; x < mDepthBufferImage.width(); ++x )
377 {
378 double d = Qgs3DUtils::decodeDepth( line[x] );
379 if ( d < 1 )
380 {
381 depth += d;
382 samplesCount += 1;
383 }
384 }
385 }
386
387 // if the whole buffer is white, a depth cannot be computed
388 if ( samplesCount == 0 )
389 depth = 1.0;
390 else
391 depth /= samplesCount;
392
393 mDepthBufferNonVoidAverage = depth;
394
395 return depth;
396}
397
398QgsVector3D QgsCameraController::moveGeocentricPoint( const QgsVector3D &point, double latDiff, double lonDiff )
399{
400 try
401 {
402 QgsVector3D pointLatLon = mGlobeCrsToLatLon.transform( point );
403 pointLatLon.setX( pointLatLon.x() + lonDiff );
404 pointLatLon.setY( std::clamp( pointLatLon.y() + latDiff, -90., 90. ) );
405
406 return mGlobeCrsToLatLon.transform( pointLatLon, Qgis::TransformDirection::Reverse );
407 }
408 catch ( const QgsCsException & )
409 {
410 QgsDebugError( u"moveGeocentricPoint: transform failed!"_s );
411 return point;
412 }
413}
414
415void QgsCameraController::globeMoveCenterPoint( double latDiff, double lonDiff )
416{
417 const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
418 const QgsVector3D newViewCenter = moveGeocentricPoint( viewCenter, latDiff, lonDiff );
419 mCameraPose.setCenterPoint( newViewCenter - mOrigin );
420 updateCameraFromPose();
421}
422
424{
425 mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * factor );
426 updateCameraFromPose();
427}
428
430{
431 mCameraPose.setPitchAngle( std::clamp( mCameraPose.pitchAngle() + angleDiff, 0.f, 90.f ) );
432 updateCameraFromPose();
433}
434
436{
437 mCameraPose.setHeadingAngle( mCameraPose.headingAngle() + angleDiff );
438 updateCameraFromPose();
439}
440
441void QgsCameraController::resetGlobe( float distance, double lat, double lon )
442{
443 QgsVector3D mapPoint;
444 try
445 {
446 mapPoint = mGlobeCrsToLatLon.transform( QgsVector3D( lon, lat, 0 ), Qgis::TransformDirection::Reverse );
447 }
448 catch ( const QgsCsException & )
449 {
450 QgsDebugError( u"resetGlobe: transform failed!"_s );
451 return;
452 }
453
454 QgsCameraPose cp;
455 cp.setCenterPoint( mapPoint - mOrigin );
457 setCameraPose( cp );
458}
459
460void QgsCameraController::updateOrthographicProjectionPlane()
461{
462 // When using orthographic projection, reuse the distance to center (which
463 // ordinarily wouldn't do anything) to set the viewport size in the world.
464 if ( mScene->mapSettings()->projectionType() == Qt3DRender::QCameraLens::OrthographicProjection )
465 {
466 const QSize viewportRect = mScene->engine()->size();
467 const float viewWidthFromCenter = distance();
468 const float viewHeightFromCenter = distance() * static_cast<float>( viewportRect.height() ) / static_cast<float>( viewportRect.width() );
469 mCamera->lens()->setOrthographicProjection( -viewWidthFromCenter, viewWidthFromCenter, -viewHeightFromCenter, viewHeightFromCenter, mCamera->nearPlane(), mCamera->farPlane() );
470 }
471}
472
473void QgsCameraController::updateCameraFromPose()
474{
475 if ( mCamera )
476 {
477 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
478 {
479 const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
480
481 QgsVector3D viewCenterLatLon;
482 try
483 {
484 viewCenterLatLon = mGlobeCrsToLatLon.transform( viewCenter );
485 }
486 catch ( const QgsCsException & )
487 {
488 QgsDebugError( u"updateCameraFromPose: transform failed!"_s );
489 return;
490 }
491
492 mCameraPose.updateCameraGlobe( mCamera, viewCenterLatLon.y(), viewCenterLatLon.x() );
493 }
494 else
495 {
496 mCameraPose.updateCamera( mCamera );
497 }
498
499 updateOrthographicProjectionPlane();
500
501 mCameraChanged = true;
502 }
503}
504
505void QgsCameraController::moveCenterPoint( const QVector3D &posDiff )
506{
507 mCameraPose.setCenterPoint( mCameraPose.centerPoint() + posDiff );
508 updateCameraFromPose();
509}
510
511void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse )
512{
513 if ( !mInputHandlersEnabled )
514 return;
515
516 QgsEventTracing::ScopedEvent traceEvent( u"3D"_s, u"QgsCameraController::onPositionChanged"_s );
517
518 switch ( mCameraNavigationMode )
519 {
521 onPositionChangedTerrainNavigation( mouse );
522 break;
523
525 onPositionChangedFlyNavigation( mouse );
526 break;
527
529 onPositionChangedGlobeTerrainNavigation( mouse );
530 break;
531 }
532}
533
534bool QgsCameraController::screenPointToWorldPos( QPoint position, double &depth, QVector3D &worldPosition )
535{
536 depth = sampleDepthBuffer( position.x(), position.y() );
537
538 // if there's nothing around the given position, try to get just any depth
539 // from the scene as a coarse approximation...
540 if ( depth == 1 )
541 {
542 depth = depthBufferNonVoidAverage();
543 }
544
545 worldPosition = Qgs3DUtils::screenPointToWorldPos( position, depth, mScene->engine()->size(), mDepthBufferCamera.get() );
546 if ( !std::isfinite( worldPosition.x() ) || !std::isfinite( worldPosition.y() ) || !std::isfinite( worldPosition.z() ) )
547 {
548 QgsDebugMsgLevel( u"screenPointToWorldPos: position is NaN or Inf. This should not happen."_s, 2 );
549 return false;
550 }
551
552 return true;
553}
554
555void QgsCameraController::onPositionChangedTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
556{
557 if ( mIgnoreNextMouseMove )
558 {
559 mIgnoreNextMouseMove = false;
560 mMousePos = QPoint( mouse->x(), mouse->y() );
561 return;
562 }
563
564 const int dx = mouse->x() - mMousePos.x();
565 const int dy = mouse->y() - mMousePos.y();
566
567 const bool hasShift = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ShiftModifier );
568 const bool hasCtrl = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier );
569 const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
570 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
571 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
572
573 if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
574 {
575 // rotate/tilt using mouse (camera moves as it rotates around the clicked point)
576 setMouseParameters( MouseOperation::RotationCenter, mMousePos );
577
578 float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
579 float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
580 float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
581
582 if ( !mDepthBufferIsReady )
583 return;
584
585 if ( !mRotationCenterCalculated )
586 {
587 double depth;
588 QVector3D worldPosition;
589 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
590 {
591 mRotationCenter = worldPosition;
592 mRotationDistanceFromCenter = ( mRotationCenter - mCameraBefore->position() ).length();
593 emit cameraRotationCenterChanged( mRotationCenter );
594 mRotationCenterCalculated = true;
595 }
596 }
597
598 rotateCameraAroundPivot( mRotationPitch + pitchDiff, mRotationYaw + yawDiff, mRotationCenter );
599 }
600 else if ( hasLeftButton && hasCtrl && !hasShift )
601 {
602 setMouseParameters( MouseOperation::RotationCamera );
603 // rotate/tilt using mouse (camera stays at one position as it rotates)
604 const float diffPitch = 0.2f * dy;
605 const float diffYaw = -0.2f * dx;
606 rotateCamera( diffPitch, diffYaw );
607 }
608 else if ( hasLeftButton && !hasShift && !hasCtrl )
609 {
610 // translation works as if one grabbed a point on the 3D viewer and dragged it
611 setMouseParameters( MouseOperation::Translation, mMousePos );
612
613 if ( !mDepthBufferIsReady )
614 return;
615
616 if ( !mDragPointCalculated )
617 {
618 double depth;
619 QVector3D worldPosition;
620 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
621 {
622 mDragDepth = depth;
623 mDragPoint = worldPosition;
624 mDragPointCalculated = true;
625 }
626 }
627
628 QVector3D cameraBeforeDragPos = mCameraBefore->position();
629 QVector3D moveToPosition = Qgs3DUtils::screenPointToWorldPos( { mouse->x(), mouse->y() }, mDragDepth, mScene->engine()->size(), mCameraBefore.get() );
630
631 QVector3D shiftVector;
632 // Compute angle of camera's view vector to ground and decide if moving
633 // the cursor up-down should change altitude or latitude/longitude.
634 float angle = std::fabs( std::acos( QVector3D::dotProduct( QVector3D( 0, 0, 1 ), mCameraBefore->viewVector().normalized() ) ) - M_PI / 2 );
635 bool changeAltitude = false;
636 // Choose threshold angle based on projection type so it "feels right".
637 switch ( mScene->mapSettings()->projectionType() )
638 {
639 case Qt3DRender::QCameraLens::PerspectiveProjection:
640 changeAltitude = angle < M_PI / 30;
641 break;
642 case Qt3DRender::QCameraLens::OrthographicProjection:
643 changeAltitude = angle < M_PI / 3;
644 break;
645 default:
646 QgsDebugError( "Unhandled 3D projection type" );
647 }
648
649 if ( changeAltitude )
650 shiftVector = mDragPoint - moveToPosition;
651 else
652 switch ( mScene->mapSettings()->projectionType() )
653 {
654 case Qt3DRender::QCameraLens::OrthographicProjection:
655 {
656 // Project change to XY plane.
657 // This isn't quite accurate at higher angles, "desyncing" the mouse
658 // cursor and the dragged point.
659 shiftVector = { mDragPoint.x() - moveToPosition.x(), mDragPoint.y() - moveToPosition.y(), 0 };
660 break;
661 }
662 case Qt3DRender::QCameraLens::PerspectiveProjection:
663 {
664 QVector3D cameraBeforeToMoveToPos = ( moveToPosition - mCameraBefore->position() ).normalized();
665 QVector3D cameraBeforeToDragPointPos = ( mDragPoint - mCameraBefore->position() ).normalized();
666
667 // Make sure the rays are not horizontal (add small z shift if it is)
668 if ( cameraBeforeToMoveToPos.z() == 0 )
669 {
670 cameraBeforeToMoveToPos.setZ( 0.01 );
671 cameraBeforeToMoveToPos = cameraBeforeToMoveToPos.normalized();
672 }
673
674 if ( cameraBeforeToDragPointPos.z() == 0 )
675 {
676 cameraBeforeToDragPointPos.setZ( 0.01 );
677 cameraBeforeToDragPointPos = cameraBeforeToDragPointPos.normalized();
678 }
679
680 float d1 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToMoveToPos.z();
681 float d2 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToDragPointPos.z();
682
683 QVector3D from = cameraBeforeDragPos + d1 * cameraBeforeToMoveToPos;
684 QVector3D to = cameraBeforeDragPos + d2 * cameraBeforeToDragPointPos;
685
686 shiftVector = to - from;
687 break;
688 }
689 default:
690 QgsDebugError( "Unhandled 3D projection type" );
691 }
692
693 mCameraPose.setCenterPoint( mCameraBefore->viewCenter() + shiftVector );
694 updateCameraFromPose();
695 }
696 else if ( hasLeftButton && hasShift && hasCtrl )
697 {
698 // change the camera elevation, similar to pageUp/pageDown
699 QgsVector3D center = mCameraPose.centerPoint();
700 double tElev = mMousePos.y() - mouse->y();
701 center.set( center.x(), center.y(), center.z() + tElev * 0.5 );
702 mCameraPose.setCenterPoint( center );
703 updateCameraFromPose();
704 }
705 else if ( hasRightButton && !hasShift && !hasCtrl )
706 {
707 setMouseParameters( MouseOperation::Zoom, mMousePos );
708 if ( !mDepthBufferIsReady )
709 return;
710
711 if ( !mDragPointCalculated )
712 {
713 double depth;
714 QVector3D worldPosition;
715 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
716 {
717 mDragPoint = worldPosition;
718 mDragPointCalculated = true;
719 }
720 }
721
722 const double oldDist = ( QgsVector3D( mCameraBefore->position() ) - QgsVector3D( mDragPoint ) ).length();
723 double newDist = oldDist;
724
725 int yOffset = 0;
726 int screenHeight = mScene->engine()->size().height();
727 QWindow *win = window();
728 if ( win )
729 {
730 yOffset = win->mapToGlobal( QPoint( 0, 0 ) ).y();
731 screenHeight = win->screen()->virtualSize().height();
732 }
733
734 // Applies smoothing
735 if ( mMousePos.y() > mClickPoint.y() ) // zoom in
736 {
737 double f = ( double ) ( mMousePos.y() - mClickPoint.y() ) / ( double ) ( screenHeight - mClickPoint.y() - yOffset );
738 f = std::max( 0.0, std::min( 1.0, f ) );
739 f = 1 - ( std::expm1( -2 * f ) ) / ( std::expm1( -2 ) );
740 newDist = newDist * f;
741 }
742 else // zoom out
743 {
744 double f = 1 - ( double ) ( mMousePos.y() + yOffset ) / ( double ) ( mClickPoint.y() + yOffset );
745 f = std::max( 0.0, std::min( 1.0, f ) );
746 f = ( std::expm1( 2 * f ) ) / ( std::expm1( 2 ) );
747 newDist = newDist + 2 * newDist * f;
748 }
749
750 const double zoomFactor = newDist / oldDist;
751 zoomCameraAroundPivot( mCameraBefore->position(), mCameraBefore->viewCenter().distanceToPoint( mCameraBefore->position() ), zoomFactor, mDragPoint );
752 }
753
754 mMousePos = QPoint( mouse->x(), mouse->y() );
755}
756
757void QgsCameraController::onPositionChangedGlobeTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
758{
759 const bool hasShift = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ShiftModifier );
760 const bool hasCtrl = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier );
761 const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
762 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
763
764 if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
765 {
766 setMouseParameters( MouseOperation::RotationCenter, mMousePos );
767
768 const float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
769 const float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
770 const float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
771
772 mCameraPose.setPitchAngle( mRotationPitch + pitchDiff );
773 mCameraPose.setHeadingAngle( mRotationYaw + yawDiff );
774 updateCameraFromPose();
775 return;
776 }
777
778 if ( !( mouse->buttons() & Qt::LeftButton ) )
779 return;
780
781 // translation works as if one grabbed a point on the 3D viewer and dragged it
782 setMouseParameters( MouseOperation::Translation, mMousePos );
783
784 if ( !mDepthBufferIsReady )
785 return;
786
787 if ( !mDragPointCalculated )
788 {
789 double depth;
790 QVector3D worldPosition;
791 if ( !screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
792 return;
793
794 mDragDepth = depth;
795 mDragPoint = worldPosition;
796 mDragPointCalculated = true;
797 }
798
799 // Approximate the globe as a sphere with a center in (0,0,0) map coords and
800 // of radius the same as at startPosMap.
801 const QgsVector3D startPosMap = QgsVector3D( mDragPoint ) + mOrigin;
802 const double sphereRadiusMap = startPosMap.length();
803 // Find the intersection of this sphere and the ray from the current clicked point.
804 const QgsRay3D ray = Qgs3DUtils::rayFromScreenPoint( QPoint( mouse->x(), mouse->y() ), mScene->engine()->size(), mCameraBefore.get() );
805 const QgsVector3D rayOriginMap = QgsVector3D( ray.origin() ) + mOrigin;
806 // From equations of ray and sphere
807 const double quadA = QVector3D::dotProduct( ray.direction(), ray.direction() );
808 const double quadB = 2 * QgsVector3D::dotProduct( ray.direction(), rayOriginMap );
809 const double quadC = QgsVector3D::dotProduct( rayOriginMap, rayOriginMap ) - sphereRadiusMap * sphereRadiusMap;
810 const double disc = quadB * quadB - 4 * quadA * quadC;
811 if ( disc < 0 )
812 // Ray misses sphere
813 return;
814 // Distance to intersection along ray (take smaller root, closer to camera)
815 const double rayDistMap = ( -quadB - sqrt( disc ) ) / ( 2 * quadA );
816 if ( rayDistMap < 0 )
817 {
818 QgsDebugError( u"Sphere intersection result negative, canceling move"_s );
819 return;
820 }
821 const QgsVector3D newPosMap = rayOriginMap + QgsVector3D( ray.direction() ) * rayDistMap;
822
823 // now that we have old and new mouse position in ECEF coordinates,
824 // let's figure out the difference in lat/lon angles and update the center point
825
826 QgsVector3D oldLatLon, newLatLon;
827 try
828 {
829 oldLatLon = mGlobeCrsToLatLon.transform( startPosMap );
830 newLatLon = mGlobeCrsToLatLon.transform( newPosMap );
831 }
832 catch ( const QgsCsException & )
833 {
834 QgsDebugError( u"onPositionChangedGlobeTerrainNavigation: transform failed!"_s );
835 return;
836 }
837
838 const double latDiff = oldLatLon.y() - newLatLon.y();
839 const double lonDiff = oldLatLon.x() - newLatLon.x();
840
841 const QgsVector3D newVC = moveGeocentricPoint( mMousePressViewCenter, latDiff, lonDiff );
842 const QgsVector3D newVCWorld = newVC - mOrigin;
843
844 mCameraPose.setCenterPoint( newVCWorld );
845 updateCameraFromPose();
846}
847
848
849void QgsCameraController::zoom( float factor )
850{
851 // zoom in/out
852 float dist = mCameraPose.distanceFromCenterPoint();
853 dist -= dist * factor * 0.01f;
854 mCameraPose.setDistanceFromCenterPoint( dist );
855 updateCameraFromPose();
856}
857
858void QgsCameraController::handleTerrainNavigationWheelZoom()
859{
860 if ( !mDepthBufferIsReady )
861 return;
862
863 if ( !mZoomPointCalculated )
864 {
865 double depth;
866 QVector3D worldPosition;
867 if ( screenPointToWorldPos( mMousePos, depth, worldPosition ) )
868 {
869 mZoomPoint = worldPosition;
870 mZoomPointCalculated = true;
871 }
872 }
873
874 double oldDist = ( mZoomPoint - mCameraBefore->position() ).length();
875 // Each step of the scroll wheel decreases distance by 20%
876 double newDist = std::pow( 0.8, mCumulatedWheelY ) * oldDist;
877 // Make sure we don't clip the thing we're zooming to.
878 newDist = std::max( newDist, 2.0 );
879 double zoomFactor = newDist / oldDist;
880 // Don't change the distance too suddenly to hopefully prevent numerical instability
881 zoomFactor = std::clamp( zoomFactor, 0.01, 100.0 );
882
883 zoomCameraAroundPivot( mCameraBefore->position(), mCameraBefore->viewCenter().distanceToPoint( mCameraBefore->position() ), zoomFactor, mZoomPoint );
884
885 mCumulatedWheelY = 0;
886 setMouseParameters( MouseOperation::None );
887}
888
889void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel )
890{
891 if ( !mInputHandlersEnabled )
892 return;
893
894 switch ( mCameraNavigationMode )
895 {
897 {
898 const float scaling = ( ( wheel->modifiers() & Qt3DInput::QWheelEvent::Modifiers::ControlModifier ) != 0 ? 0.1f : 1.0f ) / 1000.f;
899 setCameraMovementSpeed( mCameraMovementSpeed + mCameraMovementSpeed * scaling * wheel->angleDelta().y() );
900 break;
901 }
902
904 {
905 // Scale our variable to roughly "number of normal steps", with Ctrl
906 // increasing granularity 10x
907 const double scaling = ( 1.0 / 120.0 ) * ( ( wheel->modifiers() & Qt3DInput::QWheelEvent::Modifiers::ControlModifier ) != 0 ? 0.1 : 1.0 );
908
909 // Apparently angleDelta needs to be accumulated
910 // see: https://doc.qt.io/qt-5/qwheelevent.html#angleDelta
911 mCumulatedWheelY += scaling * wheel->angleDelta().y();
912
913 if ( mCurrentOperation != MouseOperation::ZoomWheel )
914 {
915 setMouseParameters( MouseOperation::ZoomWheel );
916 // The actual zooming will happen after we get a new depth buffer
917 }
918 else
919 {
920 handleTerrainNavigationWheelZoom();
921 }
922 break;
923 }
924
926 {
927 float wheelAmount = static_cast<float>( wheel->angleDelta().y() );
928 float factor = abs( wheelAmount ) / 1000.f;
929 float mulFactor = wheelAmount > 0 ? ( 1 - factor ) : ( 1 + factor );
930 mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * mulFactor );
931 updateCameraFromPose();
932 break;
933 }
934 }
935}
936
937void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
938{
939 if ( !mInputHandlersEnabled )
940 return;
941
942 mKeyboardHandler->setFocus( true );
943
944 if ( mouse->button() == Qt3DInput::QMouseEvent::MiddleButton
945 || ( ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ShiftModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton )
946 || ( ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) )
947 {
948 mMousePos = QPoint( mouse->x(), mouse->y() );
949
950 if ( mCaptureFpsMouseMovements )
951 mIgnoreNextMouseMove = true;
952
953 const MouseOperation operation {
954 ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ? MouseOperation::RotationCamera
955 : MouseOperation::RotationCenter
956 };
957 setMouseParameters( operation, mMousePos );
958 }
959
960 else if ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton || mouse->button() == Qt3DInput::QMouseEvent::RightButton )
961 {
962 mMousePos = QPoint( mouse->x(), mouse->y() );
963
964 if ( mCaptureFpsMouseMovements )
965 mIgnoreNextMouseMove = true;
966
967 const MouseOperation operation = ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) ? MouseOperation::Translation : MouseOperation::Zoom;
968 setMouseParameters( operation, mMousePos );
969 }
970}
971
972void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
973{
974 Q_UNUSED( mouse )
975 if ( !mInputHandlersEnabled )
976 return;
977
978
979 setMouseParameters( MouseOperation::None );
980}
981
982bool QgsCameraController::onKeyPressedTerrainNavigation( QKeyEvent *event )
983{
984 const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
985 const bool hasCtrl = ( event->modifiers() & Qt::ControlModifier );
986
987 int tx = 0, ty = 0, tElev = 0;
988 switch ( event->key() )
989 {
990 case Qt::Key_Left:
991 tx -= 1;
992 break;
993 case Qt::Key_Right:
994 tx += 1;
995 break;
996
997 case Qt::Key_Up:
998 ty += 1;
999 break;
1000 case Qt::Key_Down:
1001 ty -= 1;
1002 break;
1003
1004 case Qt::Key_PageDown:
1005 tElev -= 1;
1006 break;
1007 case Qt::Key_PageUp:
1008 tElev += 1;
1009 break;
1010 default:
1011 break;
1012 }
1013
1014 if ( tx || ty )
1015 {
1016 if ( !hasShift && !hasCtrl )
1017 {
1018 moveView( tx, ty );
1019 }
1020 else if ( hasShift && !hasCtrl )
1021 {
1022 // rotate/tilt using keyboard (camera moves as it rotates around its view center)
1025 }
1026 else if ( hasCtrl && !hasShift )
1027 {
1028 // rotate/tilt using keyboard (camera stays at one position as it rotates)
1029 const float diffPitch = ty; // down key = rotating camera down
1030 const float diffYaw = -tx; // right key = rotating camera to the right
1031 rotateCamera( diffPitch, diffYaw );
1032 }
1033 return true;
1034 }
1035
1036 if ( tElev )
1037 {
1038 QgsVector3D center = mCameraPose.centerPoint();
1039 center.set( center.x(), center.y(), center.z() + tElev * 10 );
1040 mCameraPose.setCenterPoint( center );
1041 return true;
1042 }
1043
1044 return false;
1045}
1046
1047bool QgsCameraController::onKeyPressedGlobeTerrainNavigation( QKeyEvent *event )
1048{
1049 // both move factor and zoom factor are just empirically picked numbers
1050 // that seem to work well (providing steps that are not too big / not too small)
1051 constexpr float MOVE_FACTOR = 0.000001f; // multiplied by distance to get angle
1052 constexpr float ZOOM_FACTOR = 0.9f;
1053
1054 const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
1055
1056 switch ( event->key() )
1057 {
1058 case Qt::Key_Left:
1059 if ( hasShift )
1061 else
1062 globeMoveCenterPoint( 0, -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
1063 return true;
1064 case Qt::Key_Right:
1065 if ( hasShift )
1067 else
1068 globeMoveCenterPoint( 0, MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
1069 return true;
1070 case Qt::Key_Up:
1071 if ( hasShift )
1073 else
1074 globeMoveCenterPoint( MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
1075 return true;
1076 case Qt::Key_Down:
1077 if ( hasShift )
1079 else
1080 globeMoveCenterPoint( -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
1081 return true;
1082 case Qt::Key_PageDown:
1083 globeZoom( ZOOM_FACTOR );
1084 return true;
1085 case Qt::Key_PageUp:
1086 globeZoom( 1 / ZOOM_FACTOR );
1087 return true;
1088 default:
1089 break;
1090 }
1091 return false;
1092}
1093
1094static const QSet<int> walkNavigationSavedKeys = {
1095 Qt::Key_Left,
1096 Qt::Key_A,
1097 Qt::Key_Right,
1098 Qt::Key_D,
1099 Qt::Key_Up,
1100 Qt::Key_W,
1101 Qt::Key_Down,
1102 Qt::Key_S,
1103 Qt::Key_PageUp,
1104 Qt::Key_E,
1105 Qt::Key_PageDown,
1106 Qt::Key_Q,
1107};
1108
1109bool QgsCameraController::onKeyPressedFlyNavigation( QKeyEvent *event )
1110{
1111 switch ( event->key() )
1112 {
1113 case Qt::Key_QuoteLeft:
1114 {
1115 // toggle mouse lock mode
1116 mCaptureFpsMouseMovements = !mCaptureFpsMouseMovements;
1117 mIgnoreNextMouseMove = true;
1118 if ( mCaptureFpsMouseMovements )
1119 {
1120 qApp->setOverrideCursor( QCursor( Qt::BlankCursor ) );
1121 }
1122 else
1123 {
1124 qApp->restoreOverrideCursor();
1125 }
1126 event->accept();
1127 return true;
1128 }
1129
1130 case Qt::Key_Escape:
1131 {
1132 // always exit mouse lock mode
1133 if ( mCaptureFpsMouseMovements )
1134 {
1135 mCaptureFpsMouseMovements = false;
1136 mIgnoreNextMouseMove = true;
1137 qApp->restoreOverrideCursor();
1138 event->accept();
1139 return true;
1140 }
1141 break;
1142 }
1143 default:
1144 break;
1145 }
1146
1147 if ( walkNavigationSavedKeys.contains( event->key() ) )
1148 {
1149 if ( !event->isAutoRepeat() )
1150 {
1151 mDepressedKeys.insert( event->key() );
1152 }
1153 event->accept();
1154 return true;
1155 }
1156 return false;
1157}
1158
1159void QgsCameraController::walkView( double tx, double ty, double tz )
1160{
1161 const QVector3D cameraUp = mCamera->upVector().normalized();
1162 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1163 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
1164
1165 QVector3D cameraPosDiff( 0.0f, 0.0f, 0.0f );
1166
1167 if ( tx != 0.0 )
1168 {
1169 cameraPosDiff += static_cast<float>( tx ) * cameraFront;
1170 }
1171 if ( ty != 0.0 )
1172 {
1173 cameraPosDiff += static_cast<float>( ty ) * cameraLeft;
1174 }
1175 if ( tz != 0.0 )
1176 {
1177 cameraPosDiff += static_cast<float>( tz ) * QVector3D( 0.0f, 0.0f, 1.0f );
1178 }
1179
1180 moveCenterPoint( cameraPosDiff );
1181}
1182
1183void QgsCameraController::applyFlyModeKeyMovements()
1184{
1185 if ( mCameraNavigationMode != Qgis::NavigationMode::Walk )
1186 return;
1187
1188 // shift = "run", ctrl = "slow walk"
1189 const bool shiftPressed = mDepressedKeys.contains( Qt::Key_Shift );
1190 const bool ctrlPressed = mDepressedKeys.contains( Qt::Key_Control );
1191
1192 const double movementSpeed = mCameraMovementSpeed * ( shiftPressed ? 2 : 1 ) * ( ctrlPressed ? 0.1 : 1 );
1193
1194 bool changed = false;
1195 double x = 0.0;
1196 double y = 0.0;
1197 double z = 0.0;
1198 if ( mDepressedKeys.contains( Qt::Key_Left ) || mDepressedKeys.contains( Qt::Key_A ) )
1199 {
1200 changed = true;
1201 y += movementSpeed;
1202 }
1203
1204 if ( mDepressedKeys.contains( Qt::Key_Right ) || mDepressedKeys.contains( Qt::Key_D ) )
1205 {
1206 changed = true;
1207 y -= movementSpeed;
1208 }
1209
1210 if ( mDepressedKeys.contains( Qt::Key_Up ) || mDepressedKeys.contains( Qt::Key_W ) )
1211 {
1212 changed = true;
1213 x += movementSpeed;
1214 }
1215
1216 if ( mDepressedKeys.contains( Qt::Key_Down ) || mDepressedKeys.contains( Qt::Key_S ) )
1217 {
1218 changed = true;
1219 x -= movementSpeed;
1220 }
1221
1222 // note -- vertical axis movements are slower by default then horizontal ones, as GIS projects
1223 // tend to have much more limited elevation range vs ground range
1224 static constexpr double ELEVATION_MOVEMENT_SCALE = 0.5;
1225 if ( mDepressedKeys.contains( Qt::Key_PageUp ) || mDepressedKeys.contains( Qt::Key_E ) )
1226 {
1227 changed = true;
1228 z += ELEVATION_MOVEMENT_SCALE * movementSpeed;
1229 }
1230
1231 if ( mDepressedKeys.contains( Qt::Key_PageDown ) || mDepressedKeys.contains( Qt::Key_Q ) )
1232 {
1233 changed = true;
1234 z -= ELEVATION_MOVEMENT_SCALE * movementSpeed;
1235 }
1236
1237 if ( changed )
1238 walkView( x, y, z );
1239}
1240
1241void QgsCameraController::onPositionChangedFlyNavigation( Qt3DInput::QMouseEvent *mouse )
1242{
1243 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
1244 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
1245
1246 const double dx = mCaptureFpsMouseMovements ? QCursor::pos().x() - mMousePos.x() : mouse->x() - mMousePos.x();
1247 const double dy = mCaptureFpsMouseMovements ? QCursor::pos().y() - mMousePos.y() : mouse->y() - mMousePos.y();
1248 mMousePos = mCaptureFpsMouseMovements ? QCursor::pos() : QPoint( mouse->x(), mouse->y() );
1249
1250 if ( mIgnoreNextMouseMove )
1251 {
1252 mIgnoreNextMouseMove = false;
1253 return;
1254 }
1255
1256 if ( hasMiddleButton )
1257 {
1258 // middle button drag = pan camera in place (strafe)
1259 const QVector3D cameraUp = mCamera->upVector().normalized();
1260 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1261 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
1262 const QVector3D cameraPosDiff = -dx * cameraLeft - dy * cameraUp;
1263 moveCenterPoint( static_cast<float>( mCameraMovementSpeed ) * cameraPosDiff / 10.0 );
1264 }
1265 else if ( hasRightButton )
1266 {
1267 // right button drag = camera dolly
1268 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1269 const QVector3D cameraPosDiff = dy * cameraFront;
1270 moveCenterPoint( static_cast<float>( mCameraMovementSpeed ) * cameraPosDiff / 5.0 );
1271 }
1272 else
1273 {
1274 if ( mCaptureFpsMouseMovements )
1275 {
1276 float diffPitch = -0.2f * dy;
1277 switch ( mVerticalAxisInversion )
1278 {
1280 diffPitch *= -1;
1281 break;
1282
1285 break;
1286 }
1287
1288 const float diffYaw = -0.2f * dx;
1289 rotateCamera( diffPitch, diffYaw );
1290 }
1291 else if ( mouse->buttons() & Qt::LeftButton )
1292 {
1293 float diffPitch = -0.2f * dy;
1294 switch ( mVerticalAxisInversion )
1295 {
1298 diffPitch *= -1;
1299 break;
1300
1302 break;
1303 }
1304 const float diffYaw = -0.2f * dx;
1305 rotateCamera( diffPitch, diffYaw );
1306 }
1307 }
1308
1309 if ( mCaptureFpsMouseMovements )
1310 {
1311 mIgnoreNextMouseMove = true;
1312
1313 // reset cursor back to center of map widget
1314 emit setCursorPosition( QPoint( mScene->engine()->size().width() / 2, mScene->engine()->size().height() / 2 ) );
1315 }
1316}
1317
1319{
1320 // Tilt up the view by deltaPitch around the view center (camera moves)
1321 float pitch = mCameraPose.pitchAngle();
1322 pitch -= deltaPitch; // down key = moving camera toward terrain
1323 mCameraPose.setPitchAngle( pitch );
1324 updateCameraFromPose();
1325}
1326
1328{
1329 // Rotate clockwise the view by deltaYaw around the view center (camera moves)
1330 float yaw = mCameraPose.headingAngle();
1331 yaw -= deltaYaw; // right key = moving camera clockwise
1332 mCameraPose.setHeadingAngle( yaw );
1333 updateCameraFromPose();
1334}
1335
1337{
1338 mCameraPose.setHeadingAngle( angle );
1339 updateCameraFromPose();
1340}
1341
1342void QgsCameraController::moveView( float tx, float ty )
1343{
1344 const float yaw = mCameraPose.headingAngle();
1345 const float dist = mCameraPose.distanceFromCenterPoint();
1346 const float x = tx * dist * 0.02f;
1347 const float y = -ty * dist * 0.02f;
1348
1349 // moving with keyboard - take into account yaw of camera
1350 const float t = sqrt( x * x + y * y );
1351 const float a = atan2( y, x ) - yaw * M_PI / 180;
1352 const float dx = cos( a ) * t;
1353 const float dy = sin( a ) * t;
1354
1355 QgsVector3D center = mCameraPose.centerPoint();
1356 center.set( center.x() + dx, center.y() - dy, center.z() );
1357 mCameraPose.setCenterPoint( center );
1358 updateCameraFromPose();
1359}
1360
1362{
1363 if ( !mInputHandlersEnabled )
1364 return false;
1365
1366 if ( event->type() == QKeyEvent::Type::KeyRelease )
1367 {
1368 if ( !event->isAutoRepeat() && mDepressedKeys.contains( event->key() ) )
1369 {
1370 mDepressedKeys.remove( event->key() );
1371 return true;
1372 }
1373 }
1374 else if ( event->type() == QEvent::ShortcutOverride )
1375 {
1376 if ( event->modifiers() & Qt::ControlModifier )
1377 {
1378 switch ( event->key() )
1379 {
1380 case Qt::Key_QuoteLeft:
1381 {
1382 // switch navigation mode
1383 switch ( mCameraNavigationMode )
1384 {
1386 // clang-format off
1388 mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe
1391 );
1392 // clang-format on
1393 break;
1397 break;
1398 }
1399 event->accept();
1400 return true;
1401 }
1402
1403 // Make sure to sync the key combinations with strings in Qgs3DAxis::createMenu()!
1404 case Qt::Key_8:
1406 return true;
1407 case Qt::Key_6:
1409 return true;
1410 case Qt::Key_2:
1412 return true;
1413 case Qt::Key_4:
1415 return true;
1416 case Qt::Key_9:
1418 return true;
1419 case Qt::Key_3:
1421 return true;
1422 case Qt::Key_5:
1424 return true;
1425
1426 default:
1427 break;
1428 }
1429 }
1430
1431 switch ( mCameraNavigationMode )
1432 {
1434 return onKeyPressedFlyNavigation( event );
1435
1437 return onKeyPressedTerrainNavigation( event );
1438
1440 return onKeyPressedGlobeTerrainNavigation( event );
1441 }
1442 }
1443 return false;
1444}
1445
1446void QgsCameraController::depthBufferCaptured( const QImage &depthImage )
1447{
1448 mDepthBufferImage = depthImage;
1449 mDepthBufferIsReady = true;
1450 mDepthBufferNonVoidAverage = -1;
1451
1452 // To read distances from the captured depth buffer, we need to know the
1453 // camera parameters it was rendered with. This seems like the closest
1454 // place to save them, though I have no idea if they can't be changed
1455 // between the rendering and now anyway...
1456 mDepthBufferCamera = Qgs3DUtils::copyCamera( mCamera );
1457
1458 if ( mCurrentOperation == MouseOperation::ZoomWheel )
1459 {
1460 handleTerrainNavigationWheelZoom();
1461 }
1462}
1463
1464bool QgsCameraController::isATranslationRotationSequence( MouseOperation newOperation ) const
1465{
1466 return std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), newOperation ) != std::end( mTranslateOrRotate )
1467 && std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), mCurrentOperation ) != std::end( mTranslateOrRotate );
1468}
1469
1470void QgsCameraController::setMouseParameters( const MouseOperation &newOperation, const QPoint &clickPoint )
1471{
1472 if ( newOperation == mCurrentOperation )
1473 {
1474 return;
1475 }
1476
1477 if ( newOperation == MouseOperation::None )
1478 {
1479 mClickPoint = QPoint();
1480 }
1481 // click point and rotation angles are updated if:
1482 // - it has never been computed
1483 // - the current and new operations are both rotation and translation
1484 // Indeed, if the sequence such as rotation -> zoom -> rotation updating mClickPoint on
1485 // the click point does not need to be updated because the relative mouse position is kept
1486 // during a zoom operation
1487 else if ( mClickPoint.isNull() || isATranslationRotationSequence( newOperation ) )
1488 {
1489 mClickPoint = clickPoint;
1490 mRotationPitch = mCameraPose.pitchAngle();
1491 mRotationYaw = mCameraPose.headingAngle();
1492 }
1493 mCurrentOperation = newOperation;
1494 mDepthBufferIsReady = false;
1495 mRotationCenterCalculated = false;
1496 mDragPointCalculated = false;
1497 mZoomPointCalculated = false;
1498
1499 if ( mCurrentOperation != MouseOperation::None && mCurrentOperation != MouseOperation::RotationCamera )
1500 {
1501 mMousePressViewCenter = mCameraPose.centerPoint() + mOrigin;
1502 mCameraBefore = Qgs3DUtils::copyCamera( mCamera );
1503
1505 }
1506}
1507
1509{
1510 QgsVector3D diff = origin - mOrigin;
1511 mCameraPose.setCenterPoint( mCameraPose.centerPoint() - diff );
1512
1513 // update other members that depend on world coordinates
1514 mCameraBefore->setPosition( ( QgsVector3D( mCameraBefore->position() ) - diff ).toVector3D() );
1515 mCameraBefore->setViewCenter( ( QgsVector3D( mCameraBefore->viewCenter() ) - diff ).toVector3D() );
1516 mDragPoint = ( QgsVector3D( mDragPoint ) - diff ).toVector3D();
1517 mRotationCenter = ( QgsVector3D( mRotationCenter ) - diff ).toVector3D();
1518
1519 mOrigin = origin;
1520
1521 updateCameraFromPose();
1522}
1523
1524void QgsCameraController::rotateToRespectingTerrain( float pitch, float yaw )
1525{
1527 double elevation = 0.0;
1528 if ( mScene->mapSettings()->terrainRenderingEnabled() )
1529 {
1530 QgsDebugMsgLevel( "Checking elevation from terrain...", 2 );
1531 QVector3D camPos = mCamera->position();
1532 const QgsRay3D ray( camPos, pos.toVector3D() - camPos );
1533 QgsRayCastContext context;
1534 context.setSingleResult( true );
1535 context.setMaximumDistance( mCamera->farPlane() );
1536 const QList<QgsRayCastHit> results = mScene->terrainEntity()->rayIntersection( ray, context );
1537
1538 if ( !results.isEmpty() )
1539 {
1540 elevation = results.constFirst().mapCoordinates().z() - mOrigin.z();
1541 QgsDebugMsgLevel( QString( "Computed elevation from terrain: %1" ).arg( elevation ), 2 );
1542 }
1543 else
1544 {
1545 QgsDebugMsgLevel( "Unable to obtain elevation from terrain", 2 );
1546 }
1547 }
1548 pos.set( pos.x(), pos.y(), elevation + mScene->terrainEntity()->terrainElevationOffset() );
1549
1550 setLookingAtPoint( pos, ( mCamera->position() - pos.toVector3D() ).length(), pitch, yaw );
1551}
1552
1554{
1555 if ( !crossSection.isValid() )
1556 return;
1557
1558 const QgsPoint startPoint = crossSection.startPoint();
1559 const QgsPoint endPoint = crossSection.endPoint();
1560 const double width = crossSection.halfWidth();
1561
1562 const QgsVector3D startVec { startPoint.x(), startPoint.y(), 0 };
1563 const QgsVector3D endVec { endPoint.x(), endPoint.y(), 0 };
1564
1565 QgsVector linePerpVec( ( endPoint - startPoint ).x(), ( endPoint - startPoint ).y() );
1566 linePerpVec = -linePerpVec.normalized().perpVector();
1567
1568 const QgsVector3D linePerpVec3D( linePerpVec.x(), linePerpVec.y(), 0 );
1569 const QgsVector3D frontStartPoint( startVec + linePerpVec3D * width );
1570 const QgsVector3D frontEndPoint( endVec + linePerpVec3D * width );
1571
1572 const QgsCameraPose camPose = Qgs3DUtils::lineSegmentToCameraPose( frontStartPoint, frontEndPoint, mScene->elevationRange( true ), mCamera->fieldOfView(), mOrigin );
1573
1574 setCameraPose( camPose );
1575}
VerticalAxisInversion
Vertical axis inversion options for 3D views.
Definition qgis.h:4382
@ Always
Always invert vertical axis movements.
Definition qgis.h:4385
@ Never
Never invert vertical axis movements.
Definition qgis.h:4383
@ WhenDragging
Invert vertical axis movements when dragging in first person modes.
Definition qgis.h:4384
NavigationMode
The navigation mode used by 3D cameras.
Definition qgis.h:4357
@ TerrainBased
The default navigation based on the terrain.
Definition qgis.h:4358
@ Walk
Uses WASD keys or arrows to navigate in walking (first person) manner.
Definition qgis.h:4359
@ GlobeTerrainBased
Navigation similar to TerrainBased, but for use with globe.
Definition qgis.h:4360
@ Globe
Scene is represented as a globe using a geocentric CRS.
Definition qgis.h:4372
@ Local
Local scene based on a projected CRS.
Definition qgis.h:4371
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2818
Entity that encapsulates our 3D scene - contains all other entities (such as terrain) as children.
QgsTerrainEntity * terrainEntity() SIP_SKIP
Returns terrain entity (may be nullptr if using globe scene, terrain rendering is disabled or when te...
Qgs3DMapSettings * mapSettings() const
Returns the 3D map settings.
QgsAbstract3DEngine * engine() const SIP_SKIP
Returns the abstract 3D engine.
Qt3DRender::QCameraLens::ProjectionType projectionType() const
Returns the camera lens' projection type.
bool terrainRenderingEnabled() const
Returns whether the 2D terrain surface will be rendered.
static QQuaternion rotationFromPitchHeadingAngles(float pitchAngle, float headingAngle)
Returns rotation quaternion that performs rotation around X axis by pitchAngle, followed by rotation ...
static std::unique_ptr< Qt3DRender::QCamera > copyCamera(Qt3DRender::QCamera *cam)
Returns new camera object with copied properties.
static double decodeDepth(const QRgb &pixel)
Decodes the depth value from the pixel's color value The depth value is encoded from OpenGL side (the...
Definition qgs3dutils.h:286
static QVector3D screenPointToWorldPos(const QPoint &screenPoint, double depth, const QSize &screenSize, Qt3DRender::QCamera *camera)
Converts the clicked mouse position to the corresponding 3D world coordinates.
static QgsRay3D rayFromScreenPoint(const QPoint &point, const QSize &windowSize, Qt3DRender::QCamera *camera)
Convert from clicked point on the screen to a ray in world coordinates.
static QgsCameraPose lineSegmentToCameraPose(const QgsVector3D &startPoint, const QgsVector3D &endPoint, const QgsDoubleRange &elevationRange, float fieldOfView, const QgsVector3D &worldOrigin)
Returns the camera pose for a camera looking at mid-point between startPoint and endPoint.
virtual QSize size() const =0
Returns size of the engine's rendering area in pixels.
void navigationModeChanged(Qgis::NavigationMode mode)
Emitted when the navigation mode is changed using the hotkey ctrl + ~.
void rotateCameraToBottom()
Rotate to bottom-up view.
void setLookingAtMapPoint(const QgsVector3D &point, float distance, float pitch, float yaw)
Sets camera configuration like setLookingAtPoint(), but the point is given in map coordinates.
float pitch() const
Returns pitch angle in degrees (0 = looking from the top, 90 = looking from the side).
Qt3DRender::QCamera * camera() const
Returns camera that is being controlled.
~QgsCameraController() override
float yaw() const
Returns yaw angle in degrees.
void requestDepthBufferCapture()
Emitted to ask for the depth buffer image.
void rotateCameraToTop()
Rotate to top-down view.
void tiltUpAroundViewCenter(float deltaPitch)
Tilt up the view by deltaPitch around the view center (camera moves).
void globeMoveCenterPoint(double latDiff, double lonDiff)
Orbits camera around the globe by the specified amount given as the difference in latitude/longitude ...
void rotateCameraToEast()
Rotate to view from the east.
void setVerticalAxisInversion(Qgis::VerticalAxisInversion inversion)
Sets the vertical axis inversion behavior.
const QgsVector3D origin() const
Returns the origin of the scene in map coordinates.
void rotateCameraToHome()
Rotate to diagonal view.
void rotateCameraToNorth()
Rotate to view from the north.
float distance() const
Returns distance of the camera from the point it is looking at.
void globeUpdatePitchAngle(float angleDiff)
Updates pitch angle by the specified amount given as the angular difference in degrees.
Q_DECL_DEPRECATED void zoomCameraAroundPivot(const QVector3D &oldCameraPosition, double zoomFactor, const QVector3D &pivotPoint)
Zooms camera by given zoom factor (>1 one means zoom in) while keeping the pivot point (given in worl...
void globeZoom(float factor)
Moves camera closer or further away from the globe.
void rotateCamera(float diffPitch, float diffYaw)
Rotates the camera on itself.
void rotateCameraToWest()
Rotate to view from the west.
void setCameraNavigationMode(Qgis::NavigationMode navigationMode)
Sets the navigation mode used by the camera controller.
void cameraChanged()
Emitted when camera has been updated.
void moveCenterPoint(const QVector3D &posDiff)
Moves camera position by the given difference vector in world coordinates.
void cameraMovementSpeedChanged(double speed)
Emitted whenever the camera movement speed is changed by the controller.
void setCrossSectionSideView(const QgsCrossSection &crossSection)
Sets the cross section side view definition for the 3D map canvas.
QgsCameraController(Qgs3DMapScene *scene)
Constructs the camera controller with optional parent node that will take ownership.
void frameTriggered(float dt)
Called internally from 3D scene when a new frame is generated. Updates camera according to keyboard/m...
void resetView(float distance)
Move camera back to the initial position (looking down towards origin of world's coordinates).
void setLookingAtPoint(const QgsVector3D &point, float distance, float pitch, float yaw)
Sets the complete camera configuration: the point towards it is looking (in 3D world coordinates),...
QgsVector3D lookingAtPoint() const
Returns the point in the world coordinates towards which the camera is looking.
QgsVector3D lookingAtMapPoint() const
Returns the point in the map coordinates towards which the camera is looking.
void rotateCameraToSouth()
Rotate to view from the south.
QDomElement writeXml(QDomDocument &doc) const
Writes camera configuration to the given DOM element.
void setViewFromTop(float worldX, float worldY, float distance, float yaw=0)
Sets camera to look down towards given point in world coordinate, in given distance from plane with z...
bool keyboardEventFilter(QKeyEvent *event)
If the event is relevant, handles the event and returns true, otherwise false.
void resetGlobe(float distance, double lat=0, double lon=0)
Resets view of the globe to look at a particular location given as latitude and longitude (in degrees...
void readXml(const QDomElement &elem, QgsVector3D savedOrigin)
Reads camera configuration from the given DOM element.
void zoom(float factor)
Zoom the map by factor.
void globeUpdateHeadingAngle(float angleDiff)
Updates heading angle by the specified amount given as the angular difference in degrees.
void rotateAroundViewCenter(float deltaYaw)
Rotate clockwise the view by deltaYaw around the view center (camera moves).
void walkView(double tx, double ty, double tz)
Walks into the map by tx, ty, and tz.
void setCameraPose(const QgsCameraPose &camPose, bool force=false)
Sets camera pose.
void setOrigin(const QgsVector3D &origin)
Reacts to the shift of origin of the scene, updating camera pose and any other member variables so th...
void setCameraHeadingAngle(float angle)
Set camera heading to angle (used for rotating the view).
void setCameraMovementSpeed(double movementSpeed)
Sets the camera movement speed.
void rotateCameraAroundPivot(float newPitch, float newHeading, const QVector3D &pivotPoint)
Rotates the camera around the pivot point (in world coordinates) to the given new pitch and heading a...
void depthBufferCaptured(const QImage &depthImage)
Sets the depth buffer image used by the camera controller to calculate world position from a pixel's ...
void cameraRotationCenterChanged(QVector3D position)
Emitted when the camera rotation center changes.
void setCursorPosition(QPoint point)
Emitted when the mouse cursor position should be moved to the specified point on the map viewport.
void moveView(float tx, float ty)
Move the map by tx and ty.
Encapsulates camera pose in a 3D scene.
void setPitchAngle(float pitch)
Sets pitch angle in degrees.
void setCenterPoint(const QgsVector3D &point)
Sets center point (towards which point the camera is looking).
void setHeadingAngle(float heading)
Sets heading (yaw) angle in degrees.
void setDistanceFromCenterPoint(float distance)
Sets distance of the camera from the center point.
Handles coordinate transforms between two coordinate systems.
Encapsulates the definition of a cross section in 3D map coordinates.
double halfWidth() const
Returns the half-width of the cross section.
QgsPoint endPoint() const
Returns the end point of the cross section.
bool isValid() const
Returns cross section validity.
QgsPoint startPoint() const
Returns the start point of the cross section.
Custom exception class for Coordinate Reference System related exceptions.
Represents a 2D point.
Definition qgspointxy.h:62
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
double x
Definition qgspoint.h:56
double y
Definition qgspoint.h:57
A representation of a ray in 3D.
Definition qgsray3d.h:31
QVector3D origin() const
Returns the origin of the ray.
Definition qgsray3d.h:43
QVector3D direction() const
Returns the direction of the ray see setDirection().
Definition qgsray3d.h:49
Responsible for defining parameters of the ray casting operations in 3D map canvases.
void setSingleResult(bool enable)
Sets whether to fetch only the closest hit for each layer or entity type.
void setMaximumDistance(float distance)
Sets the maximum distance from ray origin to look for hits when casting a ray.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:33
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:60
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:62
QVector3D toVector3D() const
Converts the current object to QVector3D.
static double dotProduct(const QgsVector3D &v1, const QgsVector3D &v2)
Returns the dot product of two vectors.
double x() const
Returns X coordinate.
Definition qgsvector3d.h:58
void setX(double x)
Sets X coordinate.
Definition qgsvector3d.h:68
void set(double x, double y, double z)
Sets vector coordinates.
Definition qgsvector3d.h:83
void setY(double y)
Sets Y coordinate.
Definition qgsvector3d.h:74
double length() const
Returns the length of the vector.
Represent a 2-dimensional vector.
Definition qgsvector.h:34
double y() const
Returns the vector's y-component.
Definition qgsvector.h:155
QgsVector normalized() const
Returns the vector's normalized (or "unit") vector (ie same angle but length of 1....
Definition qgsvector.cpp:33
QgsVector perpVector() const
Returns the perpendicular vector to this vector (rotated 90 degrees counter-clockwise).
Definition qgsvector.h:163
double x() const
Returns the vector's x-component.
Definition qgsvector.h:146
On-screen 3D engine: it creates an OpenGL window (QWindow) and displays rendered 3D scenes there.
QWindow * window()
Returns the internal 3D window where all the rendered output is displayed.
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored).
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
#define QgsDebugError(str)
Definition qgslogger.h:59