/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the QtLocation module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL3$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see http://www.qt.io/terms-conditions. For further ** information use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPLv3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or later as published by the Free ** Software Foundation and appearing in the file LICENSE.GPL included in ** the packaging of this file. Please review the following information to ** ensure the GNU General Public License version 2.0 requirements will be ** met: http://www.gnu.org/licenses/gpl-2.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qgeoprojection_p.h" #include #include #include #include #include #include #include #include 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 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(screenSize.width() - 1, 1); qreal ydiffpct = diff.y() / qMax(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; } /* * 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; } // 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(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(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); } //wraps around center QDoubleVector2D QGeoProjectionWebMercator::wrapMapProjection(const QDoubleVector2D &projection) const { double x = projection.x(); if (m_cameraCenterXMercator < 0.5) { if (x - m_cameraCenterXMercator > 0.5 ) x -= 1.0; } else if (m_cameraCenterXMercator > 0.5) { if (x - m_cameraCenterXMercator < -0.5 ) x += 1.0; } return QDoubleVector2D(x, 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 QGeoProjectionWebMercator::visibleGeometry() const { if (m_visibleRegionDirty) const_cast(this)->updateVisibleRegion(); return m_visibleRegion; } QList QGeoProjectionWebMercator::visibleGeometryExpanded() const { if (m_visibleRegionDirty) const_cast(this)->updateVisibleRegion(); return m_visibleRegionExpanded; } QList QGeoProjectionWebMercator::projectableGeometry() const { if (m_visibleRegionDirty) const_cast(this)->updateVisibleRegion(); return m_projectableRegion; } QGeoShape QGeoProjectionWebMercator::visibleRegion() const { const QList &visibleRegion = visibleGeometry(); QGeoPolygon poly; for (int 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 */ QPair QGeoProjectionWebMercator::fitViewportToGeoRectangle(const QGeoRectangle &rectangle, const QMargins &m) const { QPair 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_centerMercator = geoToMapProjection(m_cameraData.center()); m_cameraCenterXMercator = m_centerMercator.x(); m_cameraCenterYMercator = m_centerMercator.y(); int intZoomLevel = static_cast(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_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; // In mercator space too QDoubleMatrix4x4 mTiltMercator; mTiltMercator.rotate(-m_cameraData.tilt(), m_sideMercator); m_eyeMercator = mTiltMercator * m_viewMercator + m_centerMercator; } 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)); QDoubleMatrix4x4 cameraMatrix; cameraMatrix.lookAt(m_eye, m_center, 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 * cameraMatrix; m_quickItemTransformation = m_transformation; m_transformation.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(-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 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 viewportRect; viewportRect.push_back(bl); viewportRect.push_back(br); viewportRect.push_back(tr); viewportRect.push_back(tl); c2t::clip2tri clipper; clipper.clearClipper(); clipper.addSubjectPath(QClipperUtils::qListToPath(mapRect), true); clipper.addClipPolygon(QClipperUtils::qListToPath(viewportRect)); Paths res = clipper.execute(c2t::clip2tri::Intersection); m_visibleRegion.clear(); if (res.size()) m_visibleRegion = QClipperUtils::pathToQList(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 projectableRect; projectableRect.push_back(bl); projectableRect.push_back(br); projectableRect.push_back(tr); projectableRect.push_back(tl); c2t::clip2tri clipperProjectable; clipperProjectable.clearClipper(); clipperProjectable.addSubjectPath(QClipperUtils::qListToPath(mapRect), true); clipperProjectable.addClipPolygon(QClipperUtils::qListToPath(projectableRect)); Paths resProjectable = clipperProjectable.execute(c2t::clip2tri::Intersection); if (resProjectable.size()) m_projectableRegion = QClipperUtils::pathToQList(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: qAsConst(m_visibleRegion)) centroid += v; centroid /= m_visibleRegion.size(); m_visibleRegionExpanded.clear(); for (const QDoubleVector2D &v: qAsConst(m_visibleRegion)) { const QDoubleVector2D vc = v - centroid; m_visibleRegionExpanded.push_back(centroid + vc * 1.2); // fixing expansion factor to 1.2 } c2t::clip2tri clipperExpanded; clipperExpanded.clearClipper(); clipperExpanded.addSubjectPath(QClipperUtils::qListToPath(m_visibleRegionExpanded), true); clipperExpanded.addClipPolygon(QClipperUtils::qListToPath(m_projectableRegion)); Paths resVisibleExpanded = clipperExpanded.execute(c2t::clip2tri::Intersection); if (resVisibleExpanded.size()) m_visibleRegionExpanded = QClipperUtils::pathToQList(resVisibleExpanded[0]); // Intersection between two convex quadrilaterals should always be a single polygon else m_visibleRegionExpanded = m_visibleRegion; } 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