blob: f64060e2ccd6cfec97683f9fc644749621bd2a9e [file] [log] [blame]
/****************************************************************************
**
** 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 <QtPositioning/private/qwebmercator_p.h>
#include <QtPositioning/private/qlocationutils_p.h>
#include <QtPositioning/private/qclipperutils_p.h>
#include <QtPositioning/QGeoPolygon>
#include <QtPositioning/QGeoRectangle>
#include <QSize>
#include <QtGui/QMatrix4x4>
#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;
}
/*
* 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<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);
}
//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<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 (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 <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_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_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<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);
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<QDoubleVector2D> 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