3#include <QtCore/QtMath>
24Vec3 rotateCameraToWorld(
const Vec3& v, qreal headingDeg, qreal tiltDeg)
26 const double t = qDegreesToRadians(tiltDeg);
27 const double h = qDegreesToRadians(headingDeg);
30 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))};
32 return Vec3{(a.x * std::cos(h)) - (a.y * std::sin(h)), (a.x * std::sin(h)) + (a.y * std::cos(h)), a.z};
35QPointF rotated(
const QPointF& vec, qreal degrees)
37 const double rad = qDegreesToRadians(degrees);
38 const double c = std::cos(rad);
39 const double s = std::sin(rad);
40 return QPointF((vec.x() * c) - (vec.y() * s), (vec.x() * s) + (vec.y() * c));
52Vec3 cameraOffset(qreal headingDeg, qreal tiltDeg, qreal distance)
54 const double tiltRad = qDegreesToRadians(tiltDeg);
55 const double headingRad = qDegreesToRadians(headingDeg);
56 const double sinTilt = std::sin(tiltRad);
57 return Vec3{distance * sinTilt * std::sin(headingRad), -distance * sinTilt * std::cos(headingRad),
58 distance * std::cos(tiltRad)};
61Ray pickRay(
const QPointF& centerWorld, qreal heading, qreal tilt, qreal distance, qreal centerElevation,
62 const QSizeF& viewport, qreal fov,
const QPointF& screenPos)
64 const double aspect = viewport.width() / viewport.height();
65 const double tanHalfFov = std::tan(qDegreesToRadians(fov) / 2.0);
66 const double ndcX = ((2.0 * screenPos.x()) / viewport.width()) - 1.0;
67 const double ndcY = 1.0 - ((2.0 * screenPos.y()) / viewport.height());
69 const Vec3 dir = rotateCameraToWorld(Vec3{ndcX * tanHalfFov * aspect, ndcY * tanHalfFov, -1.0}, heading, tilt);
71 const Vec3 offset = cameraOffset(heading, tilt, distance);
72 return Ray{Vec3{centerWorld.x() + offset.x, centerWorld.y() + offset.y, centerElevation + offset.z}, dir};
89void GeoMapCamera::_setCenterWorld(
const QPointF& world)
92 const QPointF clamped(std::clamp(world.x(), -half, half), std::clamp(world.y(), -half, half));
93 const bool firstPosition = !_positioned;
95 if ((clamped == _centerWorld) && !firstPosition) {
98 _centerWorld = clamped;
105 const qreal normalized = _normalizedHeading(
heading);
106 if (qFuzzyCompare(normalized, _heading)) {
109 _heading = normalized;
117 if (qFuzzyCompare(clamped, _tilt)) {
128 if (qFuzzyCompare(clamped, _distance)) {
140 if (elevation == _centerElevation) {
143 _centerElevation = elevation;
150 if (size == _viewportSize) {
153 _viewportSize = size;
159 const qreal clamped = std::clamp(fov, 10.0, 120.0);
160 if (qFuzzyCompare(clamped, _fieldOfView)) {
163 _fieldOfView = clamped;
172 qCDebug(GeoMapCameraLog) <<
"mode" << _mode <<
"->" <<
mode;
194 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
195 return QVector3D(
static_cast<float>(_centerWorld.x() + offset.x),
static_cast<float>(_centerWorld.y() + offset.y),
196 static_cast<float>(_centerElevation + offset.z));
201 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
202 return QPointF(_centerWorld.x() + offset.x, _centerWorld.y() + offset.y);
207 if (origin == _sceneOrigin) {
210 _sceneOrigin = origin;
217 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
218 return QVector3D(
static_cast<float>((_centerWorld.x() - _sceneOrigin.x()) + offset.x),
219 static_cast<float>((_centerWorld.y() - _sceneOrigin.y()) + offset.y),
220 static_cast<float>(_centerElevation + offset.z));
227 return QQuaternion::fromAxisAndAngle(0, 0, 1,
static_cast<float>(_heading)) *
228 QQuaternion::fromAxisAndAngle(1, 0, 0,
static_cast<float>(_tilt));
233 if (_viewportSize.isEmpty()) {
238 pickRay(_centerWorld, _heading, _tilt, _distance, _centerElevation, _viewportSize, _fieldOfView, screenPos);
239 if (ray.dir.z >= 0.0) {
243 const double s = -ray.origin.z / ray.dir.z;
244 return QPointF(ray.origin.x + (s * ray.dir.x), ray.origin.y + (s * ray.dir.y));
249 if (_viewportSize.isEmpty()) {
254 pickRay(_centerWorld, _heading, _tilt, _distance, _centerElevation, _viewportSize, _fieldOfView, screenPos);
255 const QPointF cameraGround(ray.origin.x, ray.origin.y);
257 if (ray.dir.z < 0.0) {
258 const double s = -ray.origin.z / ray.dir.z;
259 const QPointF hit(ray.origin.x + (s * ray.dir.x), ray.origin.y + (s * ray.dir.y));
260 const QPointF offset = hit - cameraGround;
261 const double range = std::hypot(offset.x(), offset.y());
262 if (range <= maxRange) {
265 return cameraGround + (offset * (maxRange / range));
269 const double horizontal = std::hypot(ray.dir.x, ray.dir.y);
270 if (horizontal < 1e-12) {
273 return cameraGround + (QPointF(ray.dir.x, ray.dir.y) * (maxRange / horizontal));
278 if (_viewportSize.isEmpty()) {
282 const Vec3 offset = cameraOffset(_heading, _tilt, _distance);
283 const Vec3 d{worldGround.x() - (_centerWorld.x() + offset.x), worldGround.y() - (_centerWorld.y() + offset.y),
284 worldZ - (_centerElevation + offset.z)};
287 const double h = qDegreesToRadians(_heading);
288 const double t = qDegreesToRadians(_tilt);
289 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};
290 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))};
293 const double depth = -c.z;
298 const double aspect = _viewportSize.width() / _viewportSize.height();
299 const double tanHalfFov = std::tan(qDegreesToRadians(_fieldOfView) / 2.0);
300 const double ndcX = c.x / (depth * tanHalfFov * aspect);
301 const double ndcY = c.y / (depth * tanHalfFov);
302 return QPointF(((ndcX + 1.0) / 2.0) * _viewportSize.width(), ((1.0 - ndcY) / 2.0) * _viewportSize.height());
307 if (_viewportSize.isEmpty()) {
310 const QPointF screenCenter(_viewportSize.width() / 2.0, _viewportSize.height() / 2.0);
316 const QPointF d = *p1 - *p0;
317 return std::hypot(d.x(), d.y());
322 if (_viewportSize.isEmpty()) {
326 const double tanHalfFov = std::tan(qDegreesToRadians(_fieldOfView) / 2.0);
331 const QPointF& screenPos,
double worldZ,
332 double centerElevation)
const
334 if (_viewportSize.isEmpty()) {
343 pickRay(_centerWorld, _heading, _tilt, _distance, pivotElevation, _viewportSize, _fieldOfView, screenPos);
344 if (ray.dir.z >= 0.0) {
347 const double s = (worldZ - ray.origin.z) / ray.dir.z;
352 const QPointF hit(ray.origin.x + (s * ray.dir.x), ray.origin.y + (s * ray.dir.y));
364 if (!_panAnchorWorld) {
367 _anchorToScreen(*_panAnchorWorld, screenPos);
373 _orbitAnchorScreen = screenPos;
374 _orbitStartScreen = screenPos;
375 _orbitStartHeading = _heading;
376 _orbitStartTilt = _tilt;
381 if (!_orbitAnchorWorld || _viewportSize.isEmpty()) {
385 const QPointF delta = screenPos - _orbitStartScreen;
386 const qreal headingDelta = (delta.x() / _viewportSize.width()) * 360.0;
387 const qreal tiltDelta = (-delta.y() / _viewportSize.height()) * 180.0;
391 const qreal newHeading = _normalizedHeading(_orbitStartHeading + headingDelta);
392 const qreal appliedHeadingDelta = std::remainder(newHeading - _heading, 360.0);
393 _setCenterWorld(*_orbitAnchorWorld + rotated(_centerWorld - *_orbitAnchorWorld, appliedHeadingDelta));
396 setTilt(_orbitStartTilt + tiltDelta);
400 _anchorToScreen(*_orbitAnchorWorld, _orbitAnchorScreen);
409 _setCenterWorld(*anchor + rotated(_centerWorld - *anchor, degrees));
421 _anchorToScreen(*anchor, screenPos);
428 zoomBy(std::pow(0.8, amount / 120.0), screenPos);
439 _anchorToScreen(*anchor, screenPos);
443void GeoMapCamera::_anchorToScreen(
const QPointF& anchorWorld,
const QPointF& screenPos)
449 _setCenterWorld(_centerWorld + (anchorWorld - *current));
452qreal GeoMapCamera::_normalizedHeading(qreal heading)
454 const qreal wrapped = std::fmod(
heading, 360.0);
455 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 ground point picked at beginPan stays under the cursor.
void centerElevationChanged()
void setCenterElevation(qreal elevation)
void setFieldOfView(qreal fov)
std::optional< QPointF > worldToScreen(const QPointF &worldGround, double worldZ=0.0) const
Q_INVOKABLE void beginPan(const QPointF &screenPos)
Start a pan gesture at the given screen position (pixels)
static constexpr qreal kMinTilt
QPointF cameraGroundPosition() const
Q_INVOKABLE qreal sceneUnitsPerPixel() const
Q_INVOKABLE void zoom(qreal amount, const QPointF &screenPos)
std::optional< QPointF > screenToGround(const QPointF &screenPos) const
void setCenter(const QGeoCoordinate ¢er)
qreal centerElevation() const
static constexpr qreal kMinDistance
void setSceneOrigin(const QPointF &origin)
void fieldOfViewChanged()
Q_INVOKABLE void zoomBy(qreal factor, const QPointF &screenPos)
Q_INVOKABLE void orbitTo(const QPointF &screenPos)
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.
QVector3D cameraPosition() const
Q_INVOKABLE void beginOrbit(const QPointF &screenPos)
Start an orbit gesture at the given screen position (pixels)
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)
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
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)