7#include <QtCore/QRectF>
8#include <QtCore/QtMath>
26Vec3 rotateCameraToWorld(
const Vec3& v, qreal headingDeg, qreal tiltDeg)
28 const double t = qDegreesToRadians(tiltDeg);
29 const double h = qDegreesToRadians(headingDeg);
32 const Vec3 a{v.x, (v.y * std::cos(t)) - (v.z * std::sin(t)), (v.y * std::sin(t)) + (v.z * std::cos(t))};
34 return Vec3{(a.x * std::cos(h)) - (a.y * std::sin(h)), (a.x * std::sin(h)) + (a.y * std::cos(h)), a.z};
37QPointF rotated(
const QPointF& vec, qreal degrees)
39 const double rad = qDegreesToRadians(degrees);
40 const double c = std::cos(rad);
41 const double s = std::sin(rad);
42 return QPointF((vec.x() * c) - (vec.y() * s), (vec.x() * s) + (vec.y() * c));
54Vec3 cameraOffset(qreal headingDeg, qreal tiltDeg, qreal distance)
56 const double tiltRad = qDegreesToRadians(tiltDeg);
57 const double headingRad = qDegreesToRadians(headingDeg);
58 const double sinTilt = std::sin(tiltRad);
59 return Vec3{
distance * sinTilt * std::sin(headingRad), -
distance * sinTilt * std::cos(headingRad),
63Ray pickRay(
const QPointF& centerWorld, qreal heading, qreal tilt, qreal distance, qreal centerElevation,
64 const QSizeF& viewport, qreal fov,
const QPointF& screenPos)
66 const double aspect = viewport.width() / viewport.height();
67 const double tanHalfFov = std::tan(qDegreesToRadians(fov) / 2.0);
68 const double ndcX = ((2.0 * screenPos.x()) / viewport.width()) - 1.0;
69 const double ndcY = 1.0 - ((2.0 * screenPos.y()) / viewport.height());
71 const Vec3 dir = rotateCameraToWorld(Vec3{ndcX * tanHalfFov * aspect, ndcY * tanHalfFov, -1.0}, heading, tilt);
73 const Vec3 offset = cameraOffset(heading, tilt, distance);
74 return Ray{Vec3{centerWorld.x() + offset.x, centerWorld.y() + offset.y, centerElevation + offset.z}, dir};
79std::optional<QPointF> projectToScreen(
const QPointF& centerWorld, qreal heading, qreal tilt, qreal distance,
80 qreal centerElevation,
const QSizeF& viewport, qreal fov,
81 const QPointF& worldGround,
double worldZ)
83 const Vec3 offset = cameraOffset(heading, tilt, distance);
84 const Vec3 d{worldGround.x() - (centerWorld.x() + offset.x), worldGround.y() - (centerWorld.y() + offset.y),
85 worldZ - (centerElevation + offset.z)};
88 const double h = qDegreesToRadians(heading);
89 const double t = qDegreesToRadians(tilt);
90 const Vec3 a{(d.x * std::cos(h)) + (d.y * std::sin(h)), (-d.x * std::sin(h)) + (d.y * std::cos(h)), d.z};
91 const Vec3 c{a.x, (a.y * std::cos(t)) + (a.z * std::sin(t)), (-a.y * std::sin(t)) + (a.z * std::cos(t))};
94 const double depth = -c.z;
99 const double aspect = viewport.width() / viewport.height();
100 const double tanHalfFov = std::tan(qDegreesToRadians(fov) / 2.0);
101 const double ndcX = c.x / (depth * tanHalfFov * aspect);
102 const double ndcY = c.y / (depth * tanHalfFov);
103 return QPointF(((ndcX + 1.0) / 2.0) * viewport.width(), ((1.0 - ndcY) / 2.0) * viewport.height());
120void GeoMapCamera::_setCenterWorld(
const QPointF& world)
123 const QPointF clamped(std::clamp(world.x(), -half, half), std::clamp(world.y(), -half, half));
124 const bool firstPosition = !_positioned;
126 if ((clamped == _centerWorld) && !firstPosition) {
129 _centerWorld = clamped;
136 const qreal normalized = _normalizedHeading(
heading);
137 if (qFuzzyCompare(normalized, _heading)) {
140 _heading = normalized;
148 if (qFuzzyCompare(clamped, _tilt)) {
159 if (qFuzzyCompare(clamped, _distance)) {
171 if (elevation == _centerElevation) {
174 _centerElevation = elevation;
181 if (size == _viewportSize) {
185 _viewportSize = size;
195 const qreal clamped = std::clamp(fov, 10.0, 120.0);
196 if (qFuzzyCompare(clamped, _fieldOfView)) {
199 _fieldOfView = clamped;
207 if (_viewportSize.isEmpty() || _viewportSize.width() <= _viewportSize.height()) {
210 const double aspect = _viewportSize.width() / _viewportSize.height();
211 return qRadiansToDegrees(2.0 * std::atan(std::tan(qDegreesToRadians(_fieldOfView) / 2.0) / aspect));
216 if (_viewportSize.height() <= 0) {
219 return 2.0 * std::tan(qDegreesToRadians(
verticalFieldOfView()) / 2.0) / _viewportSize.height();
227 qCDebug(GeoMapCameraLog) <<
"mode" << _mode <<
"->" <<
mode;
249 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
250 return QVector3D(
static_cast<float>(_centerWorld.x() + offset.x),
static_cast<float>(_centerWorld.y() + offset.y),
251 static_cast<float>(_centerElevation + offset.z));
256 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
257 return QPointF(_centerWorld.x() + offset.x, _centerWorld.y() + offset.y);
262 if (origin == _sceneOrigin) {
265 _sceneOrigin = origin;
272 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
273 return QVector3D(
static_cast<float>((_centerWorld.x() - _sceneOrigin.x()) + offset.x),
274 static_cast<float>((_centerWorld.y() - _sceneOrigin.y()) + offset.y),
275 static_cast<float>(_centerElevation + offset.z));
282 return QQuaternion::fromAxisAndAngle(0, 0, 1,
static_cast<float>(_heading)) *
283 QQuaternion::fromAxisAndAngle(1, 0, 0,
static_cast<float>(_tilt));
288 if (_viewportSize.isEmpty()) {
292 const Ray ray = pickRay(_centerWorld, _heading, _tilt, _distance, _centerElevation, _viewportSize,
294 return PickRay{ray.origin.x, ray.origin.y, ray.origin.z, ray.dir.x, ray.dir.y, ray.dir.z};
299 if (_viewportSize.isEmpty()) {
304 pickRay(_centerWorld, _heading, _tilt, _distance, _centerElevation, _viewportSize,
verticalFieldOfView(), screenPos);
305 if (ray.dir.z >= 0.0) {
309 const double s = (planeZ - ray.origin.z) / ray.dir.z;
313 return QPointF(ray.origin.x + (s * ray.dir.x), ray.origin.y + (s * ray.dir.y));
320 return QGeoCoordinate();
327 if (_viewportSize.isEmpty()) {
332 pickRay(_centerWorld, _heading, _tilt, _distance, _centerElevation, _viewportSize,
verticalFieldOfView(), screenPos);
333 const QPointF cameraGround(ray.origin.x, ray.origin.y);
335 if (ray.dir.z < 0.0) {
336 const double s = -ray.origin.z / ray.dir.z;
337 const QPointF hit(ray.origin.x + (s * ray.dir.x), ray.origin.y + (s * ray.dir.y));
338 const QPointF offset = hit - cameraGround;
339 const double range = std::hypot(offset.x(), offset.y());
340 if (range <= maxRange) {
343 return cameraGround + (offset * (maxRange / range));
347 const double horizontal = std::hypot(ray.dir.x, ray.dir.y);
348 if (horizontal < 1e-12) {
351 return cameraGround + (QPointF(ray.dir.x, ray.dir.y) * (maxRange / horizontal));
356 if (_viewportSize.isEmpty()) {
360 return projectToScreen(_centerWorld, _heading, _tilt, _distance, _centerElevation, _viewportSize,
366 if (_viewportSize.isEmpty()) {
369 const QPointF screenCenter(_viewportSize.width() / 2.0, _viewportSize.height() / 2.0);
375 const QPointF d = *p1 - *p0;
376 return std::hypot(d.x(), d.y());
381 if (_viewportSize.isEmpty()) {
391 if (_viewportSize.isEmpty()) {
398 const double metersPerPixel = (clamped * 2.0 * tanHalfFov) / _viewportSize.height();
403 const QPointF& screenPos,
double worldZ,
404 double centerElevation)
const
406 if (_viewportSize.isEmpty()) {
415 pickRay(_centerWorld, _heading, _tilt, _distance, pivotElevation, _viewportSize,
verticalFieldOfView(), screenPos);
416 if (ray.dir.z >= 0.0) {
419 const double s = (worldZ - ray.origin.z) / ray.dir.z;
424 const QPointF hit(ray.origin.x + (s * ray.dir.x), ray.origin.y + (s * ray.dir.y));
431 if (_viewportSize.isEmpty() || !region.isValid()) {
437 const std::array<QPointF, 4> corners{topLeft, bottomRight, QPointF(topLeft.x(), bottomRight.y()),
438 QPointF(bottomRight.x(), topLeft.y())};
442 _setCenterWorld((topLeft + bottomRight) / 2.0);
444 const QRectF viewport(QPointF(0, 0), _viewportSize);
446 const auto fits = [&](
double distance) {
447 return std::ranges::all_of(corners, [&](
const QPointF& corner) {
448 const auto screen = projectToScreen(_centerWorld, _heading, _tilt,
distance, _centerElevation,
449 _viewportSize, fov, corner, _centerElevation);
450 return screen && viewport.contains(*screen);
460 while ((fitDistance / tooClose) > (1.0 + 1e-6)) {
461 const double mid = std::sqrt(tooClose * fitDistance);
474 _panAnchorZ = anchorZ;
480 if (!_panAnchorWorld) {
483 _anchorToScreen(*_panAnchorWorld, screenPos, _panAnchorZ);
488 _orbitAnchorZ = anchorZ;
490 _orbitAnchorScreen = screenPos;
491 _orbitStartScreen = screenPos;
492 _orbitStartHeading = _heading;
493 _orbitStartTilt = _tilt;
498 if (!_orbitAnchorWorld || _viewportSize.isEmpty()) {
502 const QPointF delta = screenPos - _orbitStartScreen;
503 const qreal headingDelta = (delta.x() / _viewportSize.width()) * 360.0;
504 const qreal tiltDelta = (-delta.y() / _viewportSize.height()) * 180.0;
508 const qreal newHeading = _normalizedHeading(_orbitStartHeading + headingDelta);
509 const qreal appliedHeadingDelta = std::remainder(newHeading - _heading, 360.0);
510 _setCenterWorld(*_orbitAnchorWorld + rotated(_centerWorld - *_orbitAnchorWorld, appliedHeadingDelta));
513 setTilt(_orbitStartTilt + tiltDelta);
517 _anchorToScreen(*_orbitAnchorWorld, _orbitAnchorScreen, _orbitAnchorZ);
522 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
523 _lookCameraGround = QPointF(_centerWorld.x() + offset.x, _centerWorld.y() + offset.y);
524 _lookCameraZ = _centerElevation + offset.z;
525 _lookStartScreen = screenPos;
526 _lookStartHeading = _heading;
527 _lookStartTilt = _tilt;
532 if (!_lookCameraGround || _viewportSize.isEmpty()) {
536 const QPointF delta = screenPos - _lookStartScreen;
537 const qreal headingDelta = (delta.x() / _viewportSize.width()) * 360.0;
538 const qreal tiltDelta = (-delta.y() / _viewportSize.height()) * 180.0;
540 const qreal newHeading = _normalizedHeading(_lookStartHeading + headingDelta);
550 const double height = _lookCameraZ - _centerElevation;
558 const Vec3 offset = cameraOffset(newHeading, newTilt, newDistance);
559 _setCenterWorld(QPointF(_lookCameraGround->x() - offset.x, _lookCameraGround->y() - offset.y));
571 _setCenterWorld(*anchor + rotated(_centerWorld - *anchor, degrees));
583 _anchorToScreen(*anchor, screenPos);
590 zoomBy(std::pow(0.8, amount / 120.0), screenPos);
601 _anchorToScreen(*anchor, screenPos);
605void GeoMapCamera::_anchorToScreen(
const QPointF& anchorWorld,
const QPointF& screenPos,
double anchorZ)
611 _setCenterWorld(_centerWorld + (anchorWorld - *current));
614qreal GeoMapCamera::_normalizedHeading(qreal heading)
616 const qreal wrapped = std::fmod(
heading, 360.0);
617 return (wrapped < 0.0) ? (wrapped + 360.0) : wrapped;
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Q_INVOKABLE void panTo(const QPointF &screenPos)
Continue a pan gesture: the surface point picked at beginPan stays under the cursor.
void unitsPerPixelAtUnitDistanceChanged()
Q_INVOKABLE QGeoCoordinate coordinateAtScreenPoint(const QPointF &screenPos, double worldZ=0.0) const
void centerElevationChanged()
qreal unitsPerPixelAtUnitDistance() const
void setCenterElevation(qreal elevation)
Q_INVOKABLE void beginPan(const QPointF &screenPos, double anchorZ=0.0)
Q_INVOKABLE qreal zoomLevelForDistance(qreal distance) const
Q_INVOKABLE void lookTo(const QPointF &screenPos)
void setFieldOfView(qreal fov)
std::optional< QPointF > worldToScreen(const QPointF &worldGround, double worldZ=0.0) const
static constexpr qreal kMinTilt
void verticalFieldOfViewChanged()
QPointF cameraGroundPosition() const
Q_INVOKABLE void fitToRegion(const QGeoRectangle ®ion)
Q_INVOKABLE qreal sceneUnitsPerPixel() const
Q_INVOKABLE void zoom(qreal amount, const QPointF &screenPos)
void setCenter(const QGeoCoordinate ¢er)
qreal centerElevation() const
static constexpr qreal kMinDistance
void setSceneOrigin(const QPointF &origin)
Q_INVOKABLE void beginLook(const QPointF &screenPos)
Start a first-person look gesture at the given screen position (pixels)
void fieldOfViewChanged()
Q_INVOKABLE void zoomBy(qreal factor, const QPointF &screenPos)
Q_INVOKABLE void beginOrbit(const QPointF &screenPos, double anchorZ=0.0)
Start an orbit gesture at the given screen position (pixels); anchorZ as in beginPan.
Q_INVOKABLE void orbitTo(const QPointF &screenPos)
qreal verticalFieldOfView() const
void setDistance(qreal distance)
GeoMapCamera(QObject *parent=nullptr)
Q_INVOKABLE void lookAt(const QGeoCoordinate ¢er, qreal heading, qreal tilt, qreal distance)
Set the full pose in one call. tilt/distance are clamped.
std::optional< QPointF > screenToGround(const QPointF &screenPos, double planeZ=0.0) const
QVector3D cameraPosition() const
void setHeading(qreal heading)
QVector3D scenePosition() const
void setViewportSize(const QSizeF &size)
void viewportSizeChanged()
static constexpr qreal kDefaultDistance
Q_INVOKABLE void reset()
Restore the default pose at the current center (heading 0, tilt 0, default distance)
static constexpr qreal kMaxTilt
Q_INVOKABLE void tiltBy(qreal degrees, const QPointF &screenPos)
std::optional< QPointF > groundPointCapped(const QPointF &screenPos, double maxRange) const
Q_INVOKABLE void rotateBy(qreal degrees, const QPointF &screenPos)
std::optional< PickRay > pickRayAt(const QPointF &screenPos) const
Q_INVOKABLE qreal distanceForZoomLevel(qreal zoomLevel) const
Q_INVOKABLE QGeoCoordinate centerForCoordinateAtScreenPoint(const QGeoCoordinate &coordinate, const QPointF &screenPos, double worldZ=0.0, double centerElevation=qQNaN()) const
QGeoCoordinate center() const
static constexpr qreal kMaxDistance
QQuaternion sceneRotation() const
double distance(const Ecef &left, const Ecef &right)
Straight-line distance between left and right, in metres.
constexpr int kTilePixels
Nominal tile edge used for meters-per-pixel.
double worldSize()
Full mercator world extent (2*pi*R) in world meters.
QPointF geoToWorld(const QGeoCoordinate &coord)
Geo -> world meters. Latitude is clamped to +/-kMaxLatitude.
QGeoCoordinate worldToGeo(const QPointF &world)
World meters -> geo (altitude 0)