// 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 #include "qdeclarativecirclemapitem_p.h" #include "qdeclarativecirclemapitem_p_p.h" #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE /*! \qmltype MapCircle \instantiates 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 the \l {coordinate} type. 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 */ /*! \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 */ 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); } QDeclarativeCircleMapItem::~QDeclarativeCircleMapItem() { } /*! \qmlpropertygroup Location::MapCircle::border \qmlproperty int MapCircle::border.width \qmlproperty color MapCircle::border.color This property is part of the border group 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 ¢er) { 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 &path, const QGeoCoordinate ¢er, 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 ¢er, 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::calculatePeripheralPoints(QList &path, const QGeoCoordinate ¢er, 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(m_circle.map()->geoProjection()); QScopedValueRollback rollback(m_circle.m_updatingGeometry); m_circle.m_updatingGeometry = true; QList circlePath = m_circlePath; const QGeoCoordinate ¢er = 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 = crossEarthPole(center, radius); 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>{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 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::WrapAround; } m_geometry.updateSourcePoints(*m_circle.map(), {surroundingRect, circlePath}, wrappingMode); } else { m_geometry.updateSourcePoints(*m_circle.map(), QList>{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