Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qgeoprojection_p.h"
++
++ #include <QSize>
++ #include <QtGui/QMatrix4x4>
++ #include <QtPositioning/QGeoPolygon>
++ #include <QtPositioning/QGeoRectangle>
++
++ #include <QtPositioning/private/qwebmercator_p.h>
++ #include <QtPositioning/private/qlocationutils_p.h>
++ #include <QtPositioning/private/qclipperutils_p.h>
++
++ #include <cmath>
++
++ namespace {
++ static const double defaultTileSize = 256.0;
++ static const QDoubleVector3D xyNormal(0.0, 0.0, 1.0);
++ static const QGeoProjectionWebMercator::Plane xyPlane(QDoubleVector3D(0,0,0), QDoubleVector3D(0,0,1));
++ static const QList<QDoubleVector2D> mercatorGeometry = {
++ QDoubleVector2D(-1.0,0.0),
++ QDoubleVector2D( 2.0,0.0),
++ QDoubleVector2D( 2.0,1.0),
++ QDoubleVector2D(-1.0,1.0) };
++ }
++
++ static QMatrix4x4 toMatrix4x4(const QDoubleMatrix4x4 &m)
++ {
++ return QMatrix4x4(m(0,0), m(0,1), m(0,2), m(0,3),
++ m(1,0), m(1,1), m(1,2), m(1,3),
++ m(2,0), m(2,1), m(2,2), m(2,3),
++ m(3,0), m(3,1), m(3,2), m(3,3));
++ }
++
++ static QPointF centerOffset(const QSizeF &screenSize, const QRectF &visibleArea)
++ {
++ QRectF va = visibleArea;
++ if (va.isNull())
++ va = QRectF(0, 0, screenSize.width(), screenSize.height());
++
++ QRectF screen = QRectF(QPointF(0,0),screenSize);
++ QPointF vaCenter = va.center();
++
++ QPointF screenCenter = screen.center();
++ QPointF diff = screenCenter - vaCenter;
++
++ return diff;
++ }
++
++ static QPointF marginsOffset(const QSizeF &screenSize, const QRectF &visibleArea)
++ {
++ QPointF diff = centerOffset(screenSize, visibleArea);
++ qreal xdiffpct = diff.x() / qMax<double>(screenSize.width() - 1, 1);
++ qreal ydiffpct = diff.y() / qMax<double>(screenSize.height() - 1, 1);
++
++ return QPointF(-xdiffpct, -ydiffpct);
++ }
++
++ QT_BEGIN_NAMESPACE
++
++ QGeoProjection::QGeoProjection()
++ {
++
++ }
++
++ QGeoProjection::~QGeoProjection()
++ {
++
++ }
++
++ QGeoCoordinate QGeoProjection::anchorCoordinateToPoint(const QGeoCoordinate &coordinate, const QPointF &anchorPoint) const
++ {
++ Q_UNUSED(coordinate);
++ Q_UNUSED(anchorPoint);
++ return QGeoCoordinate();
++ }
++
++ QGeoShape QGeoProjection::visibleRegion() const
++ {
++ return QGeoShape();
++ }
++
++ bool QGeoProjection::setBearing(qreal bearing, const QGeoCoordinate &coordinate)
++ {
++ Q_UNUSED(bearing);
++ Q_UNUSED(coordinate);
++ return false;
++ }
++
++ void QGeoProjection::setItemToWindowTransform(const QTransform &itemToWindowTransform)
++ {
++ if (m_itemToWindowTransform == itemToWindowTransform)
++ return;
++ m_qsgTransformDirty = true;
++ m_itemToWindowTransform = itemToWindowTransform;
++ }
++
++ QTransform QGeoProjection::itemToWindowTransform() const
++ {
++ return m_itemToWindowTransform;
++ }
++
++
++ /*
++ * QGeoProjectionWebMercator implementation
++ */
++
++ QGeoCoordinate QGeoProjectionWebMercator::anchorCoordinateToPoint(const QGeoCoordinate &coordinate, const QPointF &anchorPoint) const
++ {
++ // Approach: find the displacement in (wrapped) mercator space, and apply that to the center
++ QDoubleVector2D centerProj = geoToWrappedMapProjection(cameraData().center());
++ QDoubleVector2D coordProj = geoToWrappedMapProjection(coordinate);
++
++ QDoubleVector2D anchorProj = itemPositionToWrappedMapProjection(QDoubleVector2D(anchorPoint));
++ // Y-clamping done in mercatorToCoord
++ return wrappedMapProjectionToGeo(centerProj + coordProj - anchorProj);
++ }
++
++ bool QGeoProjectionWebMercator::setBearing(qreal bearing, const QGeoCoordinate &coordinate)
++ {
++ const QDoubleVector2D coordWrapped = geoToWrappedMapProjection(coordinate);
++ if (!isProjectable(coordWrapped))
++ return false;
++ const QPointF rotationPoint = wrappedMapProjectionToItemPosition(coordWrapped).toPointF();
++
++ QGeoCameraData camera = cameraData();
++ // first set bearing
++ camera.setBearing(bearing);
++ setCameraData(camera);
++ camera = cameraData();
++
++ // then reanchor
++ const QGeoCoordinate center = anchorCoordinateToPoint(coordinate, rotationPoint);
++ camera.setCenter(center);
++ setCameraData(camera);
++ return true;
++ }
++
++ QGeoProjectionWebMercator::QGeoProjectionWebMercator()
++ : QGeoProjection(),
++ m_mapEdgeSize(256), // at zl 0
++ m_minimumZoom(0),
++ m_cameraCenterXMercator(0),
++ m_cameraCenterYMercator(0),
++ m_viewportWidth(1),
++ m_viewportHeight(1),
++ m_1_viewportWidth(0),
++ m_1_viewportHeight(0),
++ m_sideLengthPixels(256),
++ m_aperture(0.0),
++ m_nearPlane(0.0),
++ m_farPlane(0.0),
++ m_halfWidth(0.0),
++ m_halfHeight(0.0),
++ m_minimumUnprojectableY(0.0),
++ m_verticalEstateToSkip(0.0),
++ m_visibleRegionDirty(false)
++ {
++ }
++
++ QGeoProjectionWebMercator::~QGeoProjectionWebMercator()
++ {
++
++ }
++
++ // This method returns the minimum zoom level that this specific qgeomap type allows
++ // at the current viewport size and for the default tile size of 256^2.
++ double QGeoProjectionWebMercator::minimumZoom() const
++ {
++ return m_minimumZoom;
++ }
++
++ QMatrix4x4 QGeoProjectionWebMercator::projectionTransformation() const
++ {
++ return toMatrix4x4(m_transformation);
++ }
++
++ QMatrix4x4 QGeoProjectionWebMercator::projectionTransformation_centered() const
++ {
++ return toMatrix4x4(m_transformation0);
++ }
++
++ const QMatrix4x4 &QGeoProjectionWebMercator::qsgTransform() const
++ {
++ if (m_qsgTransformDirty) {
++ m_qsgTransformDirty = false;
++ m_qsgTransform = QMatrix4x4(m_itemToWindowTransform) * toMatrix4x4(m_transformation0);
++ // qDebug() << "QGeoProjectionWebMercator::qsgTransform" << m_itemToWindowTransform << toMatrix4x4(m_transformation0);
++ }
++ return m_qsgTransform;
++ }
++
++ QDoubleVector3D QGeoProjectionWebMercator::centerMercator() const
++ {
++ return geoToMapProjection(m_cameraData.center()).toVector3D();
++ }
++
++ // This method recalculates the "no-trespassing" limits for the map center.
++ // This has to be used when:
++ // 1) the map is resized, because the meters per pixel remain the same, but
++ // the amount of pixels between the center and the borders changes
++ // 2) when the zoom level changes, because the amount of pixels between the center
++ // and the borders stays the same, but the meters per pixel change
++ double QGeoProjectionWebMercator::maximumCenterLatitudeAtZoom(const QGeoCameraData &cameraData) const
++ {
++ double mapEdgeSize = std::pow(2.0, cameraData.zoomLevel()) * defaultTileSize;
++
++ // At init time weird things happen
++ int clampedWindowHeight = (m_viewportHeight > mapEdgeSize) ? mapEdgeSize : m_viewportHeight;
++ QPointF offsetPct = centerOffset(QSizeF(m_viewportWidth, m_viewportHeight), m_visibleArea);
++ double hpct = offsetPct.y() / qMax<double>(m_viewportHeight - 1, 1);
++
++ // Use the window height divided by 2 as the topmost allowed center, with respect to the map size in pixels
++ double mercatorTopmost = (clampedWindowHeight * (0.5 - hpct)) / mapEdgeSize ;
++ QGeoCoordinate topMost = QWebMercator::mercatorToCoord(QDoubleVector2D(0.0, mercatorTopmost));
++ return topMost.latitude();
++ }
++
++ double QGeoProjectionWebMercator::minimumCenterLatitudeAtZoom(const QGeoCameraData &cameraData) const
++ {
++ double mapEdgeSize = std::pow(2.0, cameraData.zoomLevel()) * defaultTileSize;
++
++ // At init time weird things happen
++ int clampedWindowHeight = (m_viewportHeight > mapEdgeSize) ? mapEdgeSize : m_viewportHeight;
++ QPointF offsetPct = centerOffset(QSizeF(m_viewportWidth, m_viewportHeight), m_visibleArea);
++ double hpct = offsetPct.y() / qMax<double>(m_viewportHeight - 1, 1);
++
++ // Use the window height divided by 2 as the topmost allowed center, with respect to the map size in pixels
++ double mercatorTopmost = (clampedWindowHeight * (0.5 + hpct)) / mapEdgeSize ;
++ QGeoCoordinate topMost = QWebMercator::mercatorToCoord(QDoubleVector2D(0.0, mercatorTopmost));
++ return -topMost.latitude();
++ }
++
++ void QGeoProjectionWebMercator::setVisibleArea(const QRectF &visibleArea)
++ {
++ m_visibleArea = visibleArea;
++ setupCamera();
++ }
++
++ double QGeoProjectionWebMercator::mapWidth() const
++ {
++ return m_mapEdgeSize;
++ }
++
++ double QGeoProjectionWebMercator::mapHeight() const
++ {
++ return m_mapEdgeSize;
++ }
++
++ void QGeoProjectionWebMercator::setViewportSize(const QSize &size)
++ {
++ if (int(m_viewportWidth) == size.width() && int(m_viewportHeight) == size.height())
++ return;
++
++ m_viewportWidth = size.width();
++ m_viewportHeight = size.height();
++ m_1_viewportWidth = 1.0 / m_viewportWidth;
++ m_1_viewportHeight = 1.0 / m_viewportHeight;
++ m_minimumZoom = std::log(qMax(m_viewportWidth, m_viewportHeight) / defaultTileSize) / std::log(2.0);
++ setupCamera();
++ }
++
++ void QGeoProjectionWebMercator::setCameraData(const QGeoCameraData &cameraData, bool force)
++ {
++ if (m_cameraData == cameraData && !force)
++ return;
++
++ m_cameraData = cameraData;
++ m_mapEdgeSize = std::pow(2.0, cameraData.zoomLevel()) * defaultTileSize;
++ setupCamera();
++ }
++
++ QDoubleVector2D QGeoProjectionWebMercator::geoToMapProjection(const QGeoCoordinate &coordinate) const
++ {
++ return QWebMercator::coordToMercator(coordinate);
++ }
++
++ QGeoCoordinate QGeoProjectionWebMercator::mapProjectionToGeo(const QDoubleVector2D &projection) const
++ {
++ return QWebMercator::mercatorToCoord(projection);
++ }
++
++ int QGeoProjectionWebMercator::projectionWrapFactor(const QDoubleVector2D &projection) const
++ {
++ const double &x = projection.x();
++ if (m_cameraCenterXMercator < 0.5) {
++ if (x - m_cameraCenterXMercator > 0.5 )
++ return -1;
++ } else if (m_cameraCenterXMercator > 0.5) {
++ if (x - m_cameraCenterXMercator < -0.5 )
++ return 1;
++ }
++ return 0;
++ }
++
++ //wraps around center
++ QDoubleVector2D QGeoProjectionWebMercator::wrapMapProjection(const QDoubleVector2D &projection) const
++ {
++ return QDoubleVector2D(projection.x() + double(projectionWrapFactor(projection)), projection.y());
++ }
++
++ QDoubleVector2D QGeoProjectionWebMercator::unwrapMapProjection(const QDoubleVector2D &wrappedProjection) const
++ {
++ double x = wrappedProjection.x();
++ if (x > 1.0)
++ return QDoubleVector2D(x - 1.0, wrappedProjection.y());
++ if (x <= 0.0)
++ return QDoubleVector2D(x + 1.0, wrappedProjection.y());
++ return wrappedProjection;
++ }
++
++ QDoubleVector2D QGeoProjectionWebMercator::wrappedMapProjectionToItemPosition(const QDoubleVector2D &wrappedProjection) const
++ {
++ return (m_transformation * wrappedProjection).toVector2D();
++ }
++
++ QDoubleVector2D QGeoProjectionWebMercator::itemPositionToWrappedMapProjection(const QDoubleVector2D &itemPosition) const
++ {
++ const QPointF centerOff = centerOffset(QSizeF(m_viewportWidth, m_viewportHeight), m_visibleArea);
++ QDoubleVector2D pos = itemPosition + QDoubleVector2D(centerOff);
++ pos *= QDoubleVector2D(m_1_viewportWidth, m_1_viewportHeight);
++ pos *= 2.0;
++ pos -= QDoubleVector2D(1.0,1.0);
++
++ double s;
++ QDoubleVector2D res = viewportToWrappedMapProjection(pos, s);
++
++ // a positive s means a point behind the camera. So do it again, after clamping Y. See QTBUG-61813
++ if (s > 0.0) {
++ pos = itemPosition;
++ // when the camera is tilted, picking a point above the horizon returns a coordinate behind the camera
++ pos.setY(m_minimumUnprojectableY);
++ pos *= QDoubleVector2D(m_1_viewportWidth, m_1_viewportHeight);
++ pos *= 2.0;
++ pos -= QDoubleVector2D(1.0,1.0);
++ res = viewportToWrappedMapProjection(pos, s);
++ }
++
++ return res;
++ }
++
++ /* Default implementations */
++ QGeoCoordinate QGeoProjectionWebMercator::itemPositionToCoordinate(const QDoubleVector2D &pos, bool clipToViewport) const
++ {
++ if (qIsNaN(pos.x()) || qIsNaN(pos.y()))
++ return QGeoCoordinate();
++
++ if (clipToViewport) {
++ int w = m_viewportWidth;
++ int h = m_viewportHeight;
++
++ if ((pos.x() < 0) || (w < pos.x()) || (pos.y() < 0) || (h < pos.y()))
++ return QGeoCoordinate();
++ }
++
++ QDoubleVector2D wrappedMapProjection = itemPositionToWrappedMapProjection(pos);
++ // With rotation/tilting, a screen position might end up outside the projection space.
++ if (!isProjectable(wrappedMapProjection))
++ return QGeoCoordinate();
++ return mapProjectionToGeo(unwrapMapProjection(wrappedMapProjection));
++ }
++
++ QDoubleVector2D QGeoProjectionWebMercator::coordinateToItemPosition(const QGeoCoordinate &coordinate, bool clipToViewport) const
++ {
++ if (!coordinate.isValid())
++ return QDoubleVector2D(qQNaN(), qQNaN());
++
++ QDoubleVector2D wrappedProjection = wrapMapProjection(geoToMapProjection(coordinate));
++ if (!isProjectable(wrappedProjection))
++ return QDoubleVector2D(qQNaN(), qQNaN());
++
++ QDoubleVector2D pos = wrappedMapProjectionToItemPosition(wrappedProjection);
++
++ if (clipToViewport) {
++ int w = m_viewportWidth;
++ int h = m_viewportHeight;
++ double x = pos.x();
++ double y = pos.y();
++ if ((x < -0.5) || (x > w + 0.5) || (y < -0.5) || (y > h + 0.5) || qIsNaN(x) || qIsNaN(y))
++ return QDoubleVector2D(qQNaN(), qQNaN());
++ }
++ return pos;
++ }
++
++ QDoubleVector2D QGeoProjectionWebMercator::geoToWrappedMapProjection(const QGeoCoordinate &coordinate) const
++ {
++ return wrapMapProjection(geoToMapProjection(coordinate));
++ }
++
++ QGeoCoordinate QGeoProjectionWebMercator::wrappedMapProjectionToGeo(const QDoubleVector2D &wrappedProjection) const
++ {
++ return mapProjectionToGeo(unwrapMapProjection(wrappedProjection));
++ }
++
++ QMatrix4x4 QGeoProjectionWebMercator::quickItemTransformation(const QGeoCoordinate &coordinate, const QPointF &anchorPoint, qreal zoomLevel) const
++ {
++ const QDoubleVector2D coordWrapped = geoToWrappedMapProjection(coordinate);
++ double scale = std::pow(0.5, zoomLevel - m_cameraData.zoomLevel());
++ const QDoubleVector2D anchorScaled = QDoubleVector2D(anchorPoint.x(), anchorPoint.y()) * scale;
++ const QDoubleVector2D anchorMercator = anchorScaled / mapWidth();
++
++ const QDoubleVector2D coordAnchored = coordWrapped - anchorMercator;
++ const QDoubleVector2D coordAnchoredScaled = coordAnchored * m_sideLengthPixels;
++ QDoubleMatrix4x4 matTranslateScale;
++ matTranslateScale.translate(coordAnchoredScaled.x(), coordAnchoredScaled.y(), 0.0);
++
++ scale = std::pow(0.5, (zoomLevel - std::floor(zoomLevel)) +
++ (std::floor(zoomLevel) - std::floor(m_cameraData.zoomLevel())));
++ matTranslateScale.scale(scale);
++
++ /*
++ * The full transformation chain for quickItemTransformation() would be:
++ * matScreenShift * m_quickItemTransformation * matTranslate * matScale
++ * where:
++ * matScreenShift = translate(-coordOnScreen.x(), -coordOnScreen.y(), 0)
++ * matTranslate = translate(coordAnchoredScaled.x(), coordAnchoredScaled.y(), 0.0)
++ * matScale = scale(scale)
++ *
++ * However, matScreenShift is removed, as setPosition(0,0) is used in place of setPositionOnScreen.
++ */
++
++ return toMatrix4x4(m_quickItemTransformation * matTranslateScale);
++ }
++
++ bool QGeoProjectionWebMercator::isProjectable(const QDoubleVector2D &wrappedProjection) const
++ {
++ if (m_cameraData.tilt() == 0.0)
++ return true;
++
++ QDoubleVector3D pos = wrappedProjection * m_sideLengthPixels;
++ // use m_centerNearPlane in order to add an offset to m_eye.
++ QDoubleVector3D p = m_centerNearPlane - pos;
++ double dot = QDoubleVector3D::dotProduct(p , m_viewNormalized);
++
++ if (dot < 0.0) // behind the near plane
++ return false;
++ return true;
++ }
++
++ QList<QDoubleVector2D> QGeoProjectionWebMercator::visibleGeometry() const
++ {
++ if (m_visibleRegionDirty)
++ const_cast<QGeoProjectionWebMercator *>(this)->updateVisibleRegion();
++ return m_visibleRegion;
++ }
++
++ QList<QDoubleVector2D> QGeoProjectionWebMercator::visibleGeometryExpanded() const
++ {
++ if (m_visibleRegionDirty)
++ const_cast<QGeoProjectionWebMercator *>(this)->updateVisibleRegion();
++ return m_visibleRegionExpanded;
++ }
++
++ QList<QDoubleVector2D> QGeoProjectionWebMercator::projectableGeometry() const
++ {
++ if (m_visibleRegionDirty)
++ const_cast<QGeoProjectionWebMercator *>(this)->updateVisibleRegion();
++ return m_projectableRegion;
++ }
++
++ QGeoShape QGeoProjectionWebMercator::visibleRegion() const
++ {
++ const QList<QDoubleVector2D> &visibleRegion = visibleGeometry();
++ QGeoPolygon poly;
++ for (qsizetype i = 0; i < visibleRegion.size(); ++i) {
++ const QDoubleVector2D &c = visibleRegion.at(i);
++ // If a segment spans more than half of the map longitudinally, split in 2.
++ if (i && qAbs(visibleRegion.at(i - 1).x() - c.x()) >= 0.5) { // This assumes a segment is never >= 1.0 (whole map span)
++ QDoubleVector2D extraPoint = (visibleRegion.at(i - 1) + c) * 0.5;
++ poly.addCoordinate(wrappedMapProjectionToGeo(extraPoint));
++ }
++ poly.addCoordinate(wrappedMapProjectionToGeo(c));
++ }
++ if (visibleRegion.size() >= 2 && qAbs(visibleRegion.last().x() - visibleRegion.first().x()) >= 0.5) {
++ QDoubleVector2D extraPoint = (visibleRegion.last() + visibleRegion.first()) * 0.5;
++ poly.addCoordinate(wrappedMapProjectionToGeo(extraPoint));
++ }
++
++ return poly;
++ }
++
++ QDoubleVector2D QGeoProjectionWebMercator::viewportToWrappedMapProjection(const QDoubleVector2D &itemPosition) const
++ {
++ double s;
++ return viewportToWrappedMapProjection(itemPosition, s);
++ }
++
++ /*
++ actual implementation of itemPositionToWrappedMapProjection
++ */
++ QDoubleVector2D QGeoProjectionWebMercator::viewportToWrappedMapProjection(const QDoubleVector2D &itemPosition, double &s) const
++ {
++ QDoubleVector2D pos = itemPosition;
++ pos *= QDoubleVector2D(m_halfWidth, m_halfHeight);
++
++ // determine itemPosition on the near plane
++ QDoubleVector3D p = m_centerNearPlane;
++ p += m_up * pos.y();
++ p += m_side * pos.x();
++
++ // compute the ray using the eye position
++ QDoubleVector3D ray = m_eye - p;
++ ray.normalize();
++
++ return (xyPlane.lineIntersection(m_eye, ray, s) / m_sideLengthPixels).toVector2D();
++ }
++
++ /*
++ Returns a pair of <newCenter, newZoom>
++ */
++ QPair<QGeoCoordinate, qreal> QGeoProjectionWebMercator::fitViewportToGeoRectangle(const QGeoRectangle &rectangle,
++ const QMargins &m) const
++ {
++ QPair<QGeoCoordinate, qreal> res;
++ res.second = qQNaN();
++ if (m_viewportWidth <= m.left() + m.right() || m_viewportHeight <= m.top() + m.bottom())
++ return res;
++
++ QDoubleVector2D topLeftPoint = geoToMapProjection(rectangle.topLeft());
++ QDoubleVector2D bottomRightPoint = geoToMapProjection(rectangle.bottomRight());
++ if (bottomRightPoint.x() < topLeftPoint.x()) // crossing the dateline
++ bottomRightPoint.setX(bottomRightPoint.x() + 1.0);
++
++ // find center of the bounding box
++ QDoubleVector2D center = (topLeftPoint + bottomRightPoint) * 0.5;
++ center.setX(center.x() > 1.0 ? center.x() - 1.0 : center.x());
++ res.first = mapProjectionToGeo(center);
++
++ // if the shape is empty we just change center position, not zoom
++ double bboxWidth = (bottomRightPoint.x() - topLeftPoint.x()) * mapWidth();
++ double bboxHeight = (bottomRightPoint.y() - topLeftPoint.y()) * mapHeight();
++
++ if (bboxHeight == 0.0 && bboxWidth == 0.0)
++ return res;
++
++ double zoomRatio = qMax(bboxWidth / (m_viewportWidth - m.left() - m.right()),
++ bboxHeight / (m_viewportHeight - m.top() - m.bottom()));
++ zoomRatio = std::log(zoomRatio) / std::log(2.0);
++ res.second = m_cameraData.zoomLevel() - zoomRatio;
++
++ return res;
++ }
++
++ QGeoProjection::ProjectionGroup QGeoProjectionWebMercator::projectionGroup() const
++ {
++ return QGeoProjection::ProjectionCylindrical;
++ }
++
++ QGeoProjection::Datum QGeoProjectionWebMercator::datum() const
++ {
++ return QGeoProjection::DatumWGS84;
++ }
++
++ QGeoProjection::ProjectionType QGeoProjectionWebMercator::projectionType() const
++ {
++ return QGeoProjection::ProjectionWebMercator;
++ }
++
++ void QGeoProjectionWebMercator::setupCamera()
++ {
++ m_qsgTransformDirty = true;
++ m_centerMercator = geoToMapProjection(m_cameraData.center());
++ m_cameraCenterXMercator = m_centerMercator.x();
++ m_cameraCenterYMercator = m_centerMercator.y();
++
++ int intZoomLevel = static_cast<int>(std::floor(m_cameraData.zoomLevel()));
++ m_sideLengthPixels = (1 << intZoomLevel) * defaultTileSize;
++ m_center = m_centerMercator * m_sideLengthPixels;
++ //aperture(90 / 2) = 1
++ m_aperture = tan(QLocationUtils::radians(m_cameraData.fieldOfView()) * 0.5);
++
++ double f = m_viewportHeight;
++ double z = std::pow(2.0, m_cameraData.zoomLevel() - intZoomLevel) * defaultTileSize;
++ double altitude = f / (2.0 * z);
++ // Also in mercator space
++ double z_mercator = std::pow(2.0, m_cameraData.zoomLevel()) * defaultTileSize;
++ double altitude_mercator = f / (2.0 * z_mercator);
++
++ // calculate eye
++ m_eye = m_center;
++ m_eye.setZ(altitude * defaultTileSize / m_aperture);
++
++ // And in mercator space
++ m_eyeMercator = m_centerMercator;
++ m_eyeMercator.setZ(altitude_mercator / m_aperture);
++ m_eyeMercator0 = QDoubleVector3D(0,0,0);
++ m_eyeMercator0.setZ(altitude_mercator / m_aperture);
++ QDoubleVector3D eye0(0,0,0);
++ eye0.setZ(altitude * defaultTileSize / m_aperture);
++
++ m_view = m_eye - m_center;
++ QDoubleVector3D side = QDoubleVector3D::normal(m_view, QDoubleVector3D(0.0, 1.0, 0.0));
++ m_up = QDoubleVector3D::normal(side, m_view);
++
++ // In mercator space too
++ m_viewMercator = m_eyeMercator - m_centerMercator;
++ QDoubleVector3D sideMercator = QDoubleVector3D::normal(m_viewMercator, QDoubleVector3D(0.0, 1.0, 0.0));
++ m_upMercator = QDoubleVector3D::normal(sideMercator, m_viewMercator);
++
++ if (m_cameraData.bearing() > 0.0) {
++ QDoubleMatrix4x4 mBearing;
++ mBearing.rotate(m_cameraData.bearing(), m_view);
++ m_up = mBearing * m_up;
++
++ // In mercator space too
++ QDoubleMatrix4x4 mBearingMercator;
++ mBearingMercator.rotate(m_cameraData.bearing(), m_viewMercator);
++ m_upMercator = mBearingMercator * m_upMercator;
++ }
++
++ m_side = QDoubleVector3D::normal(m_up, m_view);
++ m_sideMercator = QDoubleVector3D::normal(m_upMercator, m_viewMercator);
++
++ if (m_cameraData.tilt() > 0.0) { // tilt has been already thresholded by QGeoCameraData::setTilt
++ QDoubleMatrix4x4 mTilt;
++ mTilt.rotate(-m_cameraData.tilt(), m_side);
++ m_eye = mTilt * m_view + m_center;
++ eye0 = mTilt * m_view;
++
++ // In mercator space too
++ QDoubleMatrix4x4 mTiltMercator;
++ mTiltMercator.rotate(-m_cameraData.tilt(), m_sideMercator);
++ m_eyeMercator = mTiltMercator * m_viewMercator + m_centerMercator;
++ m_eyeMercator0 = mTiltMercator * m_viewMercator;
++ }
++
++ m_view = m_eye - m_center; // ToDo: this should be inverted (center - eye), and the rest should follow
++ m_viewNormalized = m_view.normalized();
++ m_up = QDoubleVector3D::normal(m_view, m_side);
++
++ m_nearPlane = 1.0;
++ // At ZL 20 the map has 2^20 tiles per side. That is 1048576.
++ // Placing the camera on one corner of the map, rotated toward the opposite corner, and tilted
++ // at almost 90 degrees would require a frustum that can span the whole size of this map.
++ // For this reason, the far plane is set to 2 * 2^20 * defaultTileSize.
++ // That is, in order to make sure that the whole map would fit in the frustum at this ZL.
++ // Since we are using a double matrix, and since the largest value in the matrix is going to be
++ // 2 * m_farPlane (as near plane is 1.0), there should be sufficient precision left.
++ //
++ // TODO: extend this to support clip distance.
++ m_farPlane = (altitude + 2097152.0) * defaultTileSize;
++
++ m_viewMercator = m_eyeMercator - m_centerMercator;
++ m_upMercator = QDoubleVector3D::normal(m_viewMercator, m_sideMercator);
++ m_nearPlaneMercator = 0.000002; // this value works until ZL 18. Above that, a better progressive formula is needed, or
++ // else, this clips too much.
++
++ double aspectRatio = 1.0 * m_viewportWidth / m_viewportHeight;
++
++ m_halfWidth = m_aperture * aspectRatio;
++ m_halfHeight = m_aperture;
++
++ double verticalHalfFOV = QLocationUtils::degrees(atan(m_aperture));
++
++ m_cameraMatrix.setToIdentity();
++ m_cameraMatrix.lookAt(m_eye, m_center, m_up);
++ m_cameraMatrix0.setToIdentity();
++ m_cameraMatrix0.lookAt(eye0, QDoubleVector3D(0,0,0), m_up);
++
++ QDoubleMatrix4x4 projectionMatrix;
++ projectionMatrix.frustum(-m_halfWidth, m_halfWidth, -m_halfHeight, m_halfHeight, m_nearPlane, m_farPlane);
++
++ /*
++ * The full transformation chain for m_transformation is:
++ * matScreen * matScreenFit * matShift * projectionMatrix * cameraMatrix * matZoomLevelScale
++ * where:
++ * matZoomLevelScale = scale(m_sideLength, m_sideLength, 1.0)
++ * matShift = translate(1.0, 1.0, 0.0)
++ * matScreenFit = scale(0.5, 0.5, 1.0)
++ * matScreen = scale(m_viewportWidth, m_viewportHeight, 1.0)
++ */
++
++ QPointF offsetPct = marginsOffset(QSizeF(m_viewportWidth, m_viewportHeight), m_visibleArea);
++ QDoubleMatrix4x4 matScreenTransformation;
++ matScreenTransformation.scale(0.5 * m_viewportWidth, 0.5 * m_viewportHeight, 1.0);
++ matScreenTransformation(0,3) = (0.5 + offsetPct.x()) * m_viewportWidth;
++ matScreenTransformation(1,3) = (0.5 + offsetPct.y()) * m_viewportHeight;
++
++ m_transformation = matScreenTransformation * projectionMatrix * m_cameraMatrix;
++ m_quickItemTransformation = m_transformation;
++ m_transformation.scale(m_sideLengthPixels, m_sideLengthPixels, 1.0);
++
++ m_transformation0 = matScreenTransformation * projectionMatrix * m_cameraMatrix0;
++ m_transformation0.scale(m_sideLengthPixels, m_sideLengthPixels, 1.0);
++
++ m_centerNearPlane = m_eye - m_viewNormalized;
++ m_centerNearPlaneMercator = m_eyeMercator - m_viewNormalized * m_nearPlaneMercator;
++
++ // The method does not support tilting angles >= 90.0 or < 0.
++
++ // The following formula is used to have a growing epsilon with the zoom level,
++ // in order not to have too large values at low zl, which would overflow when converted to Clipper::cInt.
++ const double upperBoundEpsilon = 1.0 / std::pow(10, 1.0 + m_cameraData.zoomLevel() / 5.0);
++ const double elevationUpperBound = 90.0 - upperBoundEpsilon;
++ const double maxRayElevation = qMin(elevationUpperBound - m_cameraData.tilt(), verticalHalfFOV);
++ double maxHalfAperture = 0;
++ m_verticalEstateToSkip = 0;
++ if (maxRayElevation < verticalHalfFOV) {
++ maxHalfAperture = tan(QLocationUtils::radians(maxRayElevation));
++ m_verticalEstateToSkip = 1.0 - maxHalfAperture / m_aperture;
++ }
++
++ m_minimumUnprojectableY = m_verticalEstateToSkip * 0.5 * m_viewportHeight; // m_verticalEstateToSkip is relative to half aperture
++ m_visibleRegionDirty = true;
++ }
++
++ void QGeoProjectionWebMercator::updateVisibleRegion()
++ {
++ m_visibleRegionDirty = false;
++
++ double viewportHalfWidth = (!m_visibleArea.isEmpty()) ? m_visibleArea.width() / m_viewportWidth : 1.0;
++ double viewportHalfHeight = (!m_visibleArea.isEmpty()) ? m_visibleArea.height() / m_viewportHeight : 1.0;
++
++ double top = qMax<double>(-viewportHalfHeight, -1 + m_verticalEstateToSkip);
++ double bottom = viewportHalfHeight;
++ double left = -viewportHalfWidth;
++ double right = viewportHalfWidth;
++
++ QDoubleVector2D tl = viewportToWrappedMapProjection(QDoubleVector2D(left, top ));
++ QDoubleVector2D tr = viewportToWrappedMapProjection(QDoubleVector2D(right, top ));
++ QDoubleVector2D bl = viewportToWrappedMapProjection(QDoubleVector2D(left, bottom ));
++ QDoubleVector2D br = viewportToWrappedMapProjection(QDoubleVector2D(right, bottom ));
++
++ // To make sure that what is returned can be safely converted back to lat/lon without risking overlaps
++ double mapLeftLongitude = QLocationUtils::mapLeftLongitude(m_cameraData.center().longitude());
++ double mapRightLongitude = QLocationUtils::mapRightLongitude(m_cameraData.center().longitude());
++ double leftX = geoToWrappedMapProjection(QGeoCoordinate(0, mapLeftLongitude)).x();
++ double rightX = geoToWrappedMapProjection(QGeoCoordinate(0, mapRightLongitude)).x();
++
++ QList<QDoubleVector2D> mapRect;
++ mapRect.push_back(QDoubleVector2D(leftX, 1.0));
++ mapRect.push_back(QDoubleVector2D(rightX, 1.0));
++ mapRect.push_back(QDoubleVector2D(rightX, 0.0));
++ mapRect.push_back(QDoubleVector2D(leftX, 0.0));
++
++ QList<QDoubleVector2D> viewportRect;
++ viewportRect.push_back(bl);
++ viewportRect.push_back(br);
++ viewportRect.push_back(tr);
++ viewportRect.push_back(tl);
++
++ QClipperUtils clipper;
++ clipper.clearClipper();
++ clipper.addSubjectPath(mapRect, true);
++ clipper.addClipPolygon(viewportRect);
++
++ const auto res = clipper.execute(QClipperUtils::Intersection);
++ m_visibleRegion.clear();
++ if (res.size())
++ m_visibleRegion = res[0]; // Intersection between two convex quadrilaterals should always be a single polygon
++
++ m_projectableRegion.clear();
++ mapRect.clear();
++ // The full map rectangle in extended mercator space
++ mapRect.push_back(QDoubleVector2D(-1.0, 1.0));
++ mapRect.push_back(QDoubleVector2D( 2.0, 1.0));
++ mapRect.push_back(QDoubleVector2D( 2.0, 0.0));
++ mapRect.push_back(QDoubleVector2D(-1.0, 0.0));
++ if (m_cameraData.tilt() == 0) {
++ m_projectableRegion = mapRect;
++ } else {
++ QGeoProjectionWebMercator::Plane nearPlane(m_centerNearPlaneMercator, m_viewNormalized);
++ Line2D nearPlaneXYIntersection = nearPlane.planeXYIntersection();
++ double squareHalfSide = qMax(5.0, nearPlaneXYIntersection.m_point.length());
++ QDoubleVector2D viewDirectionProjected = -m_viewNormalized.toVector2D().normalized();
++
++
++ QDoubleVector2D tl = nearPlaneXYIntersection.m_point
++ - squareHalfSide * nearPlaneXYIntersection.m_direction
++ + 2 * squareHalfSide * viewDirectionProjected;
++ QDoubleVector2D tr = nearPlaneXYIntersection.m_point
++ + squareHalfSide * nearPlaneXYIntersection.m_direction
++ + 2 * squareHalfSide * viewDirectionProjected;
++ QDoubleVector2D bl = nearPlaneXYIntersection.m_point
++ - squareHalfSide * nearPlaneXYIntersection.m_direction;
++ QDoubleVector2D br = nearPlaneXYIntersection.m_point
++ + squareHalfSide * nearPlaneXYIntersection.m_direction;
++
++ QList<QDoubleVector2D> projectableRect;
++ projectableRect.push_back(bl);
++ projectableRect.push_back(br);
++ projectableRect.push_back(tr);
++ projectableRect.push_back(tl);
++
++
++ QClipperUtils clipperProjectable;
++ clipperProjectable.clearClipper();
++ clipperProjectable.addSubjectPath(mapRect, true);
++ clipperProjectable.addClipPolygon(projectableRect);
++
++ const auto resProjectable = clipperProjectable.execute(QClipperUtils::Intersection);
++ if (resProjectable.size())
++ m_projectableRegion = resProjectable[0]; // Intersection between two convex quadrilaterals should always be a single polygon
++ else
++ m_projectableRegion = viewportRect;
++ }
++
++ // Compute m_visibleRegionExpanded as a clipped expanded version of m_visibleRegion
++ QDoubleVector2D centroid;
++ for (const QDoubleVector2D &v: std::as_const(m_visibleRegion))
++ centroid += v;
++ centroid /= m_visibleRegion.size();
++
++ m_visibleRegionExpanded.clear();
++ for (const QDoubleVector2D &v: std::as_const(m_visibleRegion)) {
++ const QDoubleVector2D vc = v - centroid;
++ m_visibleRegionExpanded.push_back(centroid + vc * 1.2); // fixing expansion factor to 1.2
++ }
++
++ }
++
++ QGeoCameraData QGeoProjectionWebMercator::cameraData() const
++ {
++ return m_cameraData;
++ }
++
++ /*
++ *
++ * Line implementation
++ *
++ */
++
++ QGeoProjectionWebMercator::Line2D::Line2D()
++ {
++
++ }
++
++ QGeoProjectionWebMercator::Line2D::Line2D(const QDoubleVector2D &linePoint, const QDoubleVector2D &lineDirection)
++ : m_point(linePoint), m_direction(lineDirection.normalized())
++ {
++
++ }
++
++ bool QGeoProjectionWebMercator::Line2D::isValid() const
++ {
++ return (m_direction.length() > 0.5);
++ }
++
++ /*
++ *
++ * Plane implementation
++ *
++ */
++
++ QGeoProjectionWebMercator::Plane::Plane()
++ {
++
++ }
++
++ QGeoProjectionWebMercator::Plane::Plane(const QDoubleVector3D &planePoint, const QDoubleVector3D &planeNormal)
++ : m_point(planePoint), m_normal(planeNormal.normalized()) { }
++
++ QDoubleVector3D QGeoProjectionWebMercator::Plane::lineIntersection(const QDoubleVector3D &linePoint, const QDoubleVector3D &lineDirection) const
++ {
++ double s;
++ return lineIntersection(linePoint, lineDirection, s);
++ }
++
++ QDoubleVector3D QGeoProjectionWebMercator::Plane::lineIntersection(const QDoubleVector3D &linePoint, const QDoubleVector3D &lineDirection, double &s) const
++ {
++ QDoubleVector3D w = linePoint - m_point;
++ // s = -n.dot(w) / n.dot(u). p = p0 + su; u is lineDirection
++ s = QDoubleVector3D::dotProduct(-m_normal, w) / QDoubleVector3D::dotProduct(m_normal, lineDirection);
++ return linePoint + lineDirection * s;
++ }
++
++ QGeoProjectionWebMercator::Line2D QGeoProjectionWebMercator::Plane::planeXYIntersection() const
++ {
++ // cross product of the two normals for the line direction
++ QDoubleVector3D lineDirection = QDoubleVector3D::crossProduct(m_normal, xyNormal);
++ lineDirection.setZ(0.0);
++ lineDirection.normalize();
++
++ // cross product of the line direction and the plane normal to find the direction on the plane
++ // intersecting the xy plane
++ QDoubleVector3D directionToXY = QDoubleVector3D::crossProduct(m_normal, lineDirection);
++ QDoubleVector3D p = xyPlane.lineIntersection(m_point, directionToXY);
++ return Line2D(p.toVector2D(), lineDirection.toVector2D());
++ }
++
++ bool QGeoProjectionWebMercator::Plane::isValid() const
++ {
++ return (m_normal.length() > 0.5);
++ }
++
++ QT_END_NAMESPACE