Revision control

++ // Copyright (C) 2015 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 "qdeclarativecirclemapitem_p.h"
++ #include "qdeclarativecirclemapitem_p_p.h"
++
++ #include <QtCore/QScopedValueRollback>
++ #include <QPen>
++ #include <qgeocircle.h>
++
++ #include <QtGui/private/qtriangulator_p.h>
++ #include <QtLocation/private/qgeomap_p.h>
++ #include <QtPositioning/private/qlocationutils_p.h>
++
++ #include <qmath.h>
++ #include <algorithm>
++
++ #include <QtQuick/private/qquickitem_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ /*!
++ \qmltype MapCircle
++ \nativetype QDeclarativeCircleMapItem
++ \inqmlmodule QtLocation
++ \ingroup qml-QtLocation5-maps
++ \since QtLocation 5.5
++
++ \brief The MapCircle type displays a geographic circle on a Map.
++
++ The MapCircle type displays a geographic circle on a Map, which
++ consists of all points that are within a set distance from one
++ central point. Depending on map projection, a geographic circle
++ may not always be a perfect circle on the screen: for instance, in
++ the Mercator projection, circles become ovoid in shape as they near
++ the poles. To display a perfect screen circle around a point, use a
++ MapQuickItem containing a relevant Qt Quick type instead.
++
++ By default, the circle is displayed as a 1 pixel black border with
++ no fill. To change its appearance, use the \l {color}, \l {border.color}
++ and \l {border.width} properties.
++
++ Internally, a MapCircle is implemented as a many-sided polygon. To
++ calculate the radius points it uses a spherical model of the Earth,
++ similar to the atDistanceAndAzimuth method of
++ \l [QtPositioning]{geoCoordinate}. These two things can occasionally
++ have implications for the accuracy of the circle's shape, depending
++ on position and map projection.
++
++ \note Dragging a MapCircle (through the use of \l {MouseArea} or \l {PointHandler})
++ causes new points to be generated at the same distance (in meters)
++ from the center. This is in contrast to other map items which store
++ their dimensions in terms of latitude and longitude differences between
++ vertices.
++
++ \section2 Example Usage
++
++ The following snippet shows a map containing a MapCircle, centered at
++ the coordinate (-27, 153) with a radius of 5km. The circle is
++ filled in green, with a 3 pixel black border.
++
++ \code
++ Map {
++ MapCircle {
++ center {
++ latitude: -27.5
++ longitude: 153.0
++ }
++ radius: 5000.0
++ color: 'green'
++ border.width: 3
++ }
++ }
++ \endcode
++
++ \image api-mapcircle.png {Green circle on a map}
++ */
++
++ /*!
++ \qmlproperty bool QtLocation::MapCircle::autoFadeIn
++
++ This property holds whether the item automatically fades in when zooming into the map
++ starting from very low zoom levels. By default this is \c true.
++ Setting this property to \c false causes the map item to always have the opacity specified
++ with the \l QtQuick::Item::opacity property, which is 1.0 by default.
++
++ \since 5.14
++ */
++
++ /*!
++ \qmlproperty enum QtLocation::MapCircle::referenceSurface
++
++ This property determines the reference surface of the circle. If it is set to
++ \l QLocation::ReferenceSurface::Map the circle is drawn as a circe on the map with
++ \l radius approximated to match the map scale at the center of the circle.
++ If it is set to \l QLocation::ReferenceSurface::Globe the circle is mapped onto
++ a sphere and the great circle path is used to determine the coverage of the circle.
++ Default value is \l QLocation::ReferenceSurface::Map.
++
++ \since 6.5
++ */
++
++ struct Vertex
++ {
++ QVector2D position;
++ };
++
++ QGeoMapCircleGeometry::QGeoMapCircleGeometry()
++ {
++ }
++
++ QDeclarativeCircleMapItem::QDeclarativeCircleMapItem(QQuickItem *parent)
++ : QDeclarativeGeoMapItemBase(parent), m_border(this), m_color(Qt::transparent),
++ m_updatingGeometry(false)
++ , m_d(new QDeclarativeCircleMapItemPrivateCPU(*this))
++ {
++ // ToDo: handle envvar, and switch implementation.
++ m_itemType = QGeoMap::MapCircle;
++ setFlag(ItemHasContents, true);
++ QObject::connect(&m_border, &QDeclarativeMapLineProperties::colorChanged,
++ this, &QDeclarativeCircleMapItem::onLinePropertiesChanged);
++ QObject::connect(&m_border, &QDeclarativeMapLineProperties::widthChanged,
++ this, &QDeclarativeCircleMapItem::onLinePropertiesChanged);
++ QObject::connect(this, &QDeclarativeCircleMapItem::referenceSurfaceChanged, this,
++ [this]() {m_d->onGeoGeometryChanged();});
++ }
++
++ QDeclarativeCircleMapItem::~QDeclarativeCircleMapItem()
++ {
++ }
++
++ /*!
++ \qmlpropertygroup Location::MapCircle::border
++ \qmlproperty int MapCircle::border.width
++ \qmlproperty color MapCircle::border.color
++
++ This property is part of the border grouped property.
++ The border property holds the width and color used to draw the border of the circle.
++ The width is in pixels and is independent of the zoom level of the map.
++
++ The default values correspond to a black border with a width of 1 pixel.
++ For no line, use a width of 0 or a transparent color.
++ */
++ QDeclarativeMapLineProperties *QDeclarativeCircleMapItem::border()
++ {
++ return &m_border;
++ }
++
++ void QDeclarativeCircleMapItem::markSourceDirtyAndUpdate()
++ {
++ m_d->markSourceDirtyAndUpdate();
++ }
++
++ void QDeclarativeCircleMapItem::onLinePropertiesChanged()
++ {
++ m_d->onLinePropertiesChanged();
++ }
++
++ void QDeclarativeCircleMapItem::setMap(QDeclarativeGeoMap *quickMap, QGeoMap *map)
++ {
++ QDeclarativeGeoMapItemBase::setMap(quickMap,map);
++ if (map)
++ m_d->onMapSet();
++ }
++
++ /*!
++ \qmlproperty coordinate MapCircle::center
++
++ This property holds the central point about which the circle is defined.
++
++ \sa radius
++ */
++ void QDeclarativeCircleMapItem::setCenter(const QGeoCoordinate &center)
++ {
++ if (m_circle.center() == center)
++ return;
++
++ m_circle.setCenter(center);
++ m_d->onGeoGeometryChanged();
++ emit centerChanged(center);
++ }
++
++ QGeoCoordinate QDeclarativeCircleMapItem::center()
++ {
++ return m_circle.center();
++ }
++
++ /*!
++ \qmlproperty color MapCircle::color
++
++ This property holds the fill color of the circle when drawn. For no fill,
++ use a transparent color.
++ */
++ void QDeclarativeCircleMapItem::setColor(const QColor &color)
++ {
++ if (m_color == color)
++ return;
++
++ m_color = color;
++ polishAndUpdate(); // in case color was transparent and now is not or vice versa
++ emit colorChanged(m_color);
++ }
++
++ QColor QDeclarativeCircleMapItem::color() const
++ {
++ return m_color;
++ }
++
++ /*!
++ \qmlproperty real MapCircle::radius
++
++ This property holds the radius of the circle, in meters on the ground.
++
++ \sa center
++ */
++ void QDeclarativeCircleMapItem::setRadius(qreal radius)
++ {
++ if (m_circle.radius() == radius)
++ return;
++
++ m_circle.setRadius(radius);
++ m_d->onGeoGeometryChanged();
++ emit radiusChanged(radius);
++ }
++
++ qreal QDeclarativeCircleMapItem::radius() const
++ {
++ return m_circle.radius();
++ }
++
++ /*!
++ \qmlproperty real MapCircle::opacity
++
++ This property holds the opacity of the item. Opacity is specified as a
++ number between 0 (fully transparent) and 1 (fully opaque). The default is 1.
++
++ An item with 0 opacity will still receive mouse events. To stop mouse events, set the
++ visible property of the item to false.
++ */
++
++ /*!
++ \internal
++ */
++ QSGNode *QDeclarativeCircleMapItem::updateMapItemPaintNode(QSGNode *oldNode, UpdatePaintNodeData *data)
++ {
++ return m_d->updateMapItemPaintNode(oldNode, data);
++ }
++
++ /*!
++ \internal
++ */
++ void QDeclarativeCircleMapItem::updatePolish()
++ {
++ if (!map() || map()->geoProjection().projectionType() != QGeoProjection::ProjectionWebMercator)
++ return;
++ m_d->updatePolish();
++ }
++
++ /*!
++ \internal
++ */
++ void QDeclarativeCircleMapItem::afterViewportChanged(const QGeoMapViewportChangeEvent &event)
++ {
++ if (event.mapSize.isEmpty())
++ return;
++
++ m_d->afterViewportChanged();
++ }
++
++ /*!
++ \internal
++ */
++ bool QDeclarativeCircleMapItem::contains(const QPointF &point) const
++ {
++ return m_d->contains(point);
++ }
++
++ const QGeoShape &QDeclarativeCircleMapItem::geoShape() const
++ {
++ return m_circle;
++ }
++
++ void QDeclarativeCircleMapItem::setGeoShape(const QGeoShape &shape)
++ {
++ if (shape == m_circle)
++ return;
++
++ const QGeoCircle circle(shape); // if shape isn't a circle, circle will be created as a default-constructed circle
++ const bool centerHasChanged = circle.center() != m_circle.center();
++ const bool radiusHasChanged = circle.radius() != m_circle.radius();
++ m_circle = circle;
++
++ m_d->onGeoGeometryChanged();
++ if (centerHasChanged)
++ emit centerChanged(m_circle.center());
++ if (radiusHasChanged)
++ emit radiusChanged(m_circle.radius());
++ }
++
++ /*!
++ \internal
++ */
++ void QDeclarativeCircleMapItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
++ {
++ if (!map() || !m_circle.isValid() || m_updatingGeometry || newGeometry == oldGeometry) {
++ QDeclarativeGeoMapItemBase::geometryChange(newGeometry, oldGeometry);
++ return;
++ }
++
++ QDoubleVector2D newPoint = QDoubleVector2D(x(),y()) + QDoubleVector2D(width(), height()) * 0.5;
++ QGeoCoordinate newCoordinate = map()->geoProjection().itemPositionToCoordinate(newPoint, false);
++ if (newCoordinate.isValid())
++ setCenter(newCoordinate); // ToDo: this is incorrect. setting such center might yield to another geometry changed.
++
++ // Not calling QDeclarativeGeoMapItemBase::geometryChange() as it will be called from a nested
++ // call to this function.
++ }
++
++ QDeclarativeCircleMapItemPrivate::~QDeclarativeCircleMapItemPrivate()
++ {
++ }
++
++ QDeclarativeCircleMapItemPrivateCPU::QDeclarativeCircleMapItemPrivateCPU(QDeclarativeCircleMapItem &circle)
++ : QDeclarativeCircleMapItemPrivate(circle)
++ {
++ m_shape = new QQuickShape(&m_circle);
++ m_shape->setObjectName("_qt_map_item_shape");
++ m_shape->setZ(-1);
++ m_shape->setContainsMode(QQuickShape::FillContains);
++
++ m_shapePath = new QQuickShapePath(m_shape);
++ m_painterPath = new QDeclarativeGeoMapPainterPath(m_shapePath);
++
++ auto pathElements = m_shapePath->pathElements();
++ pathElements.append(&pathElements, m_painterPath);
++
++ auto shapePaths = m_shape->data();
++ shapePaths.append(&shapePaths, m_shapePath);
++ }
++
++ QDeclarativeCircleMapItemPrivateCPU::~QDeclarativeCircleMapItemPrivateCPU()
++ {
++ delete m_shape;
++ }
++
++ /*
++ * A workaround for circle path to be drawn correctly using a polygon geometry
++ * This method generates a polygon like
++ * ______________
++ * | ____ |
++ * \__/ \__/
++ */
++ void QDeclarativeCircleMapItemPrivate::includeOnePoleInPath(QList<QDoubleVector2D> &path,
++ const QGeoCoordinate &center,
++ qreal distance, const QGeoProjectionWebMercator &p)
++ {
++ const qreal poleLat = 90;
++ const qreal distanceToNorthPole = center.distanceTo(QGeoCoordinate(poleLat, 0));
++ const qreal distanceToSouthPole = center.distanceTo(QGeoCoordinate(-poleLat, 0));
++ const bool crossNorthPole = distanceToNorthPole < distance;
++ const bool crossSouthPole = distanceToSouthPole < distance;
++
++ if (!crossNorthPole && !crossSouthPole)
++ return;
++
++ if (crossNorthPole && crossSouthPole)
++ return;
++
++ const QRectF cameraRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(p.visibleGeometry());
++ const qreal xAtBorder = cameraRect.left();
++
++ // The strategy is to order the points from left to right as they appear on the screen.
++ // Then add the 3 missing sides that form the box for painting at the front and at the end of the list.
++ // We ensure that the box aligns with the cameraRect in order to avoid rendering it twice (wrap around).
++ // Notably, this leads to outlines at the right side of the map.
++ // Set xAtBorder to 0.0 to avoid this, however, for an increased rendering cost.
++ for (auto &c : path) {
++ c.setX(c.x());
++ while (c.x() - xAtBorder > 1.0)
++ c.setX(c.x() - 1.0);
++ while (c.x() - xAtBorder < 0.0)
++ c.setX(c.x() + 1.0);
++ }
++
++ std::sort(path.begin(), path.end(),
++ [](const QDoubleVector2D &a, const QDoubleVector2D &b) -> bool
++ {return a.x() < b.x();});
++
++ const qreal newPoleLat = crossNorthPole ? -0.1 : 1.1;
++ const QDoubleVector2D P1 = path.first() + QDoubleVector2D(1.0, 0.0);
++ const QDoubleVector2D P2 = path.last() - QDoubleVector2D(1.0, 0.0);
++ path.push_front(P2);
++ path.push_front(QDoubleVector2D(P2.x(), newPoleLat));
++ path.append(P1);
++ path.append(QDoubleVector2D(P1.x(), newPoleLat));
++ }
++
++ int QDeclarativeCircleMapItemPrivate::crossEarthPole(const QGeoCoordinate &center, qreal distance)
++ {
++ qreal poleLat = 90;
++ QGeoCoordinate northPole = QGeoCoordinate(poleLat, center.longitude());
++ QGeoCoordinate southPole = QGeoCoordinate(-poleLat, center.longitude());
++ // approximate using great circle distance
++ qreal distanceToNorthPole = center.distanceTo(northPole);
++ qreal distanceToSouthPole = center.distanceTo(southPole);
++ return (distanceToNorthPole < distance? 1 : 0) +
++ (distanceToSouthPole < distance? 1 : 0);
++ }
++
++ void QDeclarativeCircleMapItemPrivate::calculatePeripheralPointsSimple(QList<QDoubleVector2D> &path,
++ const QGeoCoordinate &center,
++ qreal distance,
++ const QGeoProjectionWebMercator &p,
++ int steps)
++ {
++ const double lambda = 0.0001;
++ const QDoubleVector2D c = p.geoToMapProjection(center);
++ const double lambda_geo = center.distanceTo(p.mapProjectionToGeo(c + QDoubleVector2D(lambda, 0)));
++ const qreal mapDistance = distance * lambda / lambda_geo;
++
++ for (int i = 0; i < steps; ++i) {
++ const qreal rad = 2 * M_PI * i / steps;
++ path << c + QDoubleVector2D(cos(rad), sin(rad)) * mapDistance;
++ }
++ }
++
++ void QDeclarativeCircleMapItemPrivate::calculatePeripheralPointsGreatCircle(QList<QDoubleVector2D> &path,
++ const QGeoCoordinate &center,
++ qreal distance,
++ const QGeoProjectionWebMercator &p,
++ int steps)
++ {
++ // Calculate points based on great-circle distance
++ // Calculation is the same as GeoCoordinate's atDistanceAndAzimuth function
++ // but tweaked here for computing multiple points
++
++ // pre-calculations
++ steps = qMax(steps, 3);
++ qreal centerLon = center.longitude();
++ qreal latRad = QLocationUtils::radians(center.latitude());
++ qreal lonRad = QLocationUtils::radians(centerLon);
++ qreal cosLatRad = std::cos(latRad);
++ qreal sinLatRad = std::sin(latRad);
++ qreal ratio = (distance / QLocationUtils::earthMeanRadius());
++ qreal cosRatio = std::cos(ratio);
++ qreal sinRatio = std::sin(ratio);
++ qreal sinLatRad_x_cosRatio = sinLatRad * cosRatio;
++ qreal cosLatRad_x_sinRatio = cosLatRad * sinRatio;
++ for (int i = 0; i < steps; ++i) {
++ const qreal azimuthRad = 2 * M_PI * i / steps;
++ const qreal resultLatRad = std::asin(sinLatRad_x_cosRatio
++ + cosLatRad_x_sinRatio * std::cos(azimuthRad));
++ const qreal resultLonRad = lonRad + std::atan2(std::sin(azimuthRad) * cosLatRad_x_sinRatio,
++ cosRatio - sinLatRad * std::sin(resultLatRad));
++ const qreal lat2 = QLocationUtils::degrees(resultLatRad);
++ qreal lon2 = QLocationUtils::degrees(resultLonRad);
++
++ //Workaround as QGeoCoordinate does not take Longitudes outside [-180,180]
++ qreal offset = 0.0;
++ while (lon2 > 180.0) {
++ offset += 1.0;
++ lon2 -= 360.0;
++ }
++ while (lon2 < -180.0) {
++ offset -= 1.0;
++ lon2 += 360.0;
++ }
++ path << p.geoToMapProjection(QGeoCoordinate(lat2, lon2, center.altitude())) + QDoubleVector2D(offset, 0.0);
++ }
++ }
++
++ //////////////////////////////////////////////////////////////////////
++
++ void QDeclarativeCircleMapItemPrivateCPU::updatePolish()
++ {
++ if (!m_circle.m_circle.isValid()) {
++ m_geometry.clear();
++ m_circle.setWidth(0);
++ m_circle.setHeight(0);
++ m_shape->setVisible(false);
++ return;
++ }
++
++ const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(m_circle.map()->geoProjection());
++ QScopedValueRollback<bool> rollback(m_circle.m_updatingGeometry);
++ m_circle.m_updatingGeometry = true;
++
++ QList<QDoubleVector2D> circlePath = m_circlePath;
++
++ const QGeoCoordinate &center = m_circle.m_circle.center();
++ const qreal &radius = m_circle.m_circle.radius();
++
++ // if circle crosses north/south pole, then don't preserve circular shape,
++ int crossingPoles = m_circle.referenceSurface() == QLocation::ReferenceSurface::Globe ? crossEarthPole(center, radius) : 0;
++ if (crossingPoles == 1) { // If the circle crosses both poles, we will remove it from a rectangle
++ includeOnePoleInPath(circlePath, center, radius, p);
++ m_geometry.updateSourcePoints(*m_circle.map(), QList<QList<QDoubleVector2D>>{circlePath}, QGeoMapPolygonGeometry::DrawOnce);
++ }
++ else if (crossingPoles == 2) { // If the circle crosses both poles, we will remove it from a rectangle
++ // The circle covers both poles. This appears on the map as a total fill with a hole on the opposite side of the planet
++ // This can be represented by a rectangle that spans the entire planet with a hole defined by the calculated points.
++ // The points on one side have to be wraped around the globe
++ const qreal centerX = p.geoToMapProjection(center).x();
++ for (int i = 0; i < circlePath.count(); i++) {
++ if (circlePath.at(i).x() > centerX)
++ circlePath[i].setX(circlePath.at(i).x() - 1.0);
++ }
++ QRectF cameraRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(p.visibleGeometry());
++ const QRectF circleRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(circlePath);
++ QGeoMapPolygonGeometry::MapBorderBehaviour wrappingMode = QGeoMapPolygonGeometry::DrawOnce;
++ QList<QDoubleVector2D> surroundingRect;
++ if (cameraRect.contains(circleRect)){
++ cameraRect = cameraRect.adjusted(-0.1, -0.1, 0.2, 0.2);
++ surroundingRect = {{cameraRect.left(), cameraRect.top()}, {cameraRect.right(), cameraRect.top()},
++ {cameraRect.right(), cameraRect.bottom()}, {cameraRect.left() , cameraRect.bottom()}};
++ } else {
++ const qreal anchorRect = centerX;
++
++ surroundingRect = {{anchorRect, -0.1}, {anchorRect + 1.0, -0.1},
++ {anchorRect + 1.0, 1.1}, {anchorRect, 1.1}};
++ wrappingMode = QGeoMapPolygonGeometry::Duplicate;
++ }
++ m_geometry.updateSourcePoints(*m_circle.map(), {surroundingRect, circlePath}, wrappingMode);
++ } else {
++ m_geometry.updateSourcePoints(*m_circle.map(), QList<QList<QDoubleVector2D>>{circlePath});
++ }
++
++ m_circle.setShapeTriangulationScale(m_shape, m_geometry.maxCoord());
++
++ const bool hasBorder = m_circle.m_border.color().alpha() != 0 && m_circle.m_border.width() > 0;
++ const float borderWidth = hasBorder ? m_circle.m_border.width() : 0.0f;
++ m_shapePath->setStrokeColor(hasBorder ? m_circle.m_border.color() : Qt::transparent);
++ m_shapePath->setStrokeWidth(hasBorder ? borderWidth : -1.0f);
++ m_shapePath->setFillColor(m_circle.color());
++
++ const QRectF bb = m_geometry.sourceBoundingBox();
++ QPainterPath path = m_geometry.srcPath();
++ path.translate(-bb.left() + borderWidth, -bb.top() + borderWidth);
++ path.closeSubpath();
++ m_painterPath->setPath(path);
++
++ m_circle.setSize(bb.size());
++ m_shape->setSize(m_circle.size());
++ m_shape->setOpacity(m_circle.zoomLevelOpacity());
++ m_shape->setVisible(true);
++
++ m_circle.setPositionOnMap(m_geometry.origin(), -1 * bb.topLeft() + QPointF(borderWidth, borderWidth));
++ }
++
++ QSGNode *QDeclarativeCircleMapItemPrivateCPU::updateMapItemPaintNode(QSGNode *oldNode,
++ QQuickItem::UpdatePaintNodeData *data)
++ {
++ Q_UNUSED(data);
++ delete oldNode;
++ if (m_geometry.isScreenDirty()) {
++ m_geometry.markClean();
++ }
++ return nullptr;
++ }
++
++ bool QDeclarativeCircleMapItemPrivateCPU::contains(const QPointF &point) const
++ {
++ return m_shape->contains(m_circle.mapToItem(m_shape, point));
++ }
++
++ QT_END_NAMESPACE