Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qdeclarativecirclemapitem.cpp
Go to the documentation of this file.
1// Copyright (C) 2015 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
7
8#include <QtCore/QScopedValueRollback>
9#include <QPen>
10#include <qgeocircle.h>
11
12#include <QtGui/private/qtriangulator_p.h>
13#include <QtLocation/private/qgeomap_p.h>
14#include <QtPositioning/private/qlocationutils_p.h>
15
16#include <qmath.h>
17#include <algorithm>
18
19#include <QtQuick/private/qquickitem_p.h>
20
21QT_BEGIN_NAMESPACE
22
23/*!
24 \qmltype MapCircle
25 \nativetype QDeclarativeCircleMapItem
26 \inqmlmodule QtLocation
27 \ingroup qml-QtLocation5-maps
28 \since QtLocation 5.5
29
30 \brief The MapCircle type displays a geographic circle on a Map.
31
32 The MapCircle type displays a geographic circle on a Map, which
33 consists of all points that are within a set distance from one
34 central point. Depending on map projection, a geographic circle
35 may not always be a perfect circle on the screen: for instance, in
36 the Mercator projection, circles become ovoid in shape as they near
37 the poles. To display a perfect screen circle around a point, use a
38 MapQuickItem containing a relevant Qt Quick type instead.
39
40 By default, the circle is displayed as a 1 pixel black border with
41 no fill. To change its appearance, use the \l {color}, \l {border.color}
42 and \l {border.width} properties.
43
44 Internally, a MapCircle is implemented as a many-sided polygon. To
45 calculate the radius points it uses a spherical model of the Earth,
46 similar to the atDistanceAndAzimuth method of
47 \l [QtPositioning]{geoCoordinate}. These two things can occasionally
48 have implications for the accuracy of the circle's shape, depending
49 on position and map projection.
50
51 \note Dragging a MapCircle (through the use of \l {MouseArea} or \l {PointHandler})
52 causes new points to be generated at the same distance (in meters)
53 from the center. This is in contrast to other map items which store
54 their dimensions in terms of latitude and longitude differences between
55 vertices.
56
57 \section2 Example Usage
58
59 The following snippet shows a map containing a MapCircle, centered at
60 the coordinate (-27, 153) with a radius of 5km. The circle is
61 filled in green, with a 3 pixel black border.
62
63 \code
64 Map {
65 MapCircle {
66 center {
67 latitude: -27.5
68 longitude: 153.0
69 }
70 radius: 5000.0
71 color: 'green'
72 border.width: 3
73 }
74 }
75 \endcode
76
77 \image api-mapcircle.png
78*/
79
80/*!
81 \qmlproperty bool QtLocation::MapCircle::autoFadeIn
82
83 This property holds whether the item automatically fades in when zooming into the map
84 starting from very low zoom levels. By default this is \c true.
85 Setting this property to \c false causes the map item to always have the opacity specified
86 with the \l QtQuick::Item::opacity property, which is 1.0 by default.
87
88 \since 5.14
89*/
90
91/*!
92 \qmlproperty enum QtLocation::MapCircle::referenceSurface
93
94 This property determines the reference surface of the circle. If it is set to
95 \l QLocation::ReferenceSurface::Map the circle is drawn as a circe on the map with
96 \l radius approximated to match the map scale at the center of the circle.
97 If it is set to \l QLocation::ReferenceSurface::Globe the circle is mapped onto
98 a sphere and the great circle path is used to determine the coverage of the circle.
99 Default value is \l QLocation::ReferenceSurface::Map.
100
101 \since 6.5
102*/
103
104struct Vertex
105{
106 QVector2D position;
107};
108
109QGeoMapCircleGeometry::QGeoMapCircleGeometry()
110{
111}
112
113QDeclarativeCircleMapItem::QDeclarativeCircleMapItem(QQuickItem *parent)
114: QDeclarativeGeoMapItemBase(parent), m_border(this), m_color(Qt::transparent),
115 m_updatingGeometry(false)
116 , m_d(new QDeclarativeCircleMapItemPrivateCPU(*this))
117{
118 // ToDo: handle envvar, and switch implementation.
119 m_itemType = QGeoMap::MapCircle;
120 setFlag(ItemHasContents, true);
121 QObject::connect(&m_border, &QDeclarativeMapLineProperties::colorChanged,
122 this, &QDeclarativeCircleMapItem::onLinePropertiesChanged);
123 QObject::connect(&m_border, &QDeclarativeMapLineProperties::widthChanged,
124 this, &QDeclarativeCircleMapItem::onLinePropertiesChanged);
125 QObject::connect(this, &QDeclarativeCircleMapItem::referenceSurfaceChanged, this,
126 [this]() {m_d->onGeoGeometryChanged();});
127}
128
129QDeclarativeCircleMapItem::~QDeclarativeCircleMapItem()
130{
131}
132
133/*!
134 \qmlpropertygroup Location::MapCircle::border
135 \qmlproperty int MapCircle::border.width
136 \qmlproperty color MapCircle::border.color
137
138 This property is part of the border group property.
139 The border property holds the width and color used to draw the border of the circle.
140 The width is in pixels and is independent of the zoom level of the map.
141
142 The default values correspond to a black border with a width of 1 pixel.
143 For no line, use a width of 0 or a transparent color.
144*/
145QDeclarativeMapLineProperties *QDeclarativeCircleMapItem::border()
146{
147 return &m_border;
148}
149
150void QDeclarativeCircleMapItem::markSourceDirtyAndUpdate()
151{
152 m_d->markSourceDirtyAndUpdate();
153}
154
155void QDeclarativeCircleMapItem::onLinePropertiesChanged()
156{
157 m_d->onLinePropertiesChanged();
158}
159
160void QDeclarativeCircleMapItem::setMap(QDeclarativeGeoMap *quickMap, QGeoMap *map)
161{
162 QDeclarativeGeoMapItemBase::setMap(quickMap,map);
163 if (map)
164 m_d->onMapSet();
165}
166
167/*!
168 \qmlproperty coordinate MapCircle::center
169
170 This property holds the central point about which the circle is defined.
171
172 \sa radius
173*/
174void QDeclarativeCircleMapItem::setCenter(const QGeoCoordinate &center)
175{
176 if (m_circle.center() == center)
177 return;
178
179 m_circle.setCenter(center);
180 m_d->onGeoGeometryChanged();
181 emit centerChanged(center);
182}
183
184QGeoCoordinate QDeclarativeCircleMapItem::center()
185{
186 return m_circle.center();
187}
188
189/*!
190 \qmlproperty color MapCircle::color
191
192 This property holds the fill color of the circle when drawn. For no fill,
193 use a transparent color.
194*/
195void QDeclarativeCircleMapItem::setColor(const QColor &color)
196{
197 if (m_color == color)
198 return;
199
200 m_color = color;
201 polishAndUpdate(); // in case color was transparent and now is not or vice versa
202 emit colorChanged(m_color);
203}
204
205QColor QDeclarativeCircleMapItem::color() const
206{
207 return m_color;
208}
209
210/*!
211 \qmlproperty real MapCircle::radius
212
213 This property holds the radius of the circle, in meters on the ground.
214
215 \sa center
216*/
217void QDeclarativeCircleMapItem::setRadius(qreal radius)
218{
219 if (m_circle.radius() == radius)
220 return;
221
222 m_circle.setRadius(radius);
223 m_d->onGeoGeometryChanged();
224 emit radiusChanged(radius);
225}
226
227qreal QDeclarativeCircleMapItem::radius() const
228{
229 return m_circle.radius();
230}
231
232/*!
233 \qmlproperty real MapCircle::opacity
234
235 This property holds the opacity of the item. Opacity is specified as a
236 number between 0 (fully transparent) and 1 (fully opaque). The default is 1.
237
238 An item with 0 opacity will still receive mouse events. To stop mouse events, set the
239 visible property of the item to false.
240*/
241
242/*!
243 \internal
244*/
245QSGNode *QDeclarativeCircleMapItem::updateMapItemPaintNode(QSGNode *oldNode, UpdatePaintNodeData *data)
246{
247 return m_d->updateMapItemPaintNode(oldNode, data);
248}
249
250/*!
251 \internal
252*/
253void QDeclarativeCircleMapItem::updatePolish()
254{
255 if (!map() || map()->geoProjection().projectionType() != QGeoProjection::ProjectionWebMercator)
256 return;
257 m_d->updatePolish();
258}
259
260/*!
261 \internal
262*/
263void QDeclarativeCircleMapItem::afterViewportChanged(const QGeoMapViewportChangeEvent &event)
264{
265 if (event.mapSize.isEmpty())
266 return;
267
268 m_d->afterViewportChanged();
269}
270
271/*!
272 \internal
273*/
274bool QDeclarativeCircleMapItem::contains(const QPointF &point) const
275{
276 return m_d->contains(point);
277}
278
279const QGeoShape &QDeclarativeCircleMapItem::geoShape() const
280{
281 return m_circle;
282}
283
284void QDeclarativeCircleMapItem::setGeoShape(const QGeoShape &shape)
285{
286 if (shape == m_circle)
287 return;
288
289 const QGeoCircle circle(shape); // if shape isn't a circle, circle will be created as a default-constructed circle
290 const bool centerHasChanged = circle.center() != m_circle.center();
291 const bool radiusHasChanged = circle.radius() != m_circle.radius();
292 m_circle = circle;
293
294 m_d->onGeoGeometryChanged();
295 if (centerHasChanged)
296 emit centerChanged(m_circle.center());
297 if (radiusHasChanged)
298 emit radiusChanged(m_circle.radius());
299}
300
301/*!
302 \internal
303*/
304void QDeclarativeCircleMapItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
305{
306 if (!map() || !m_circle.isValid() || m_updatingGeometry || newGeometry == oldGeometry) {
307 QDeclarativeGeoMapItemBase::geometryChange(newGeometry, oldGeometry);
308 return;
309 }
310
311 QDoubleVector2D newPoint = QDoubleVector2D(x(),y()) + QDoubleVector2D(width(), height()) * 0.5;
312 QGeoCoordinate newCoordinate = map()->geoProjection().itemPositionToCoordinate(newPoint, false);
313 if (newCoordinate.isValid())
314 setCenter(newCoordinate); // ToDo: this is incorrect. setting such center might yield to another geometry changed.
315
316 // Not calling QDeclarativeGeoMapItemBase::geometryChange() as it will be called from a nested
317 // call to this function.
318}
319
320QDeclarativeCircleMapItemPrivate::~QDeclarativeCircleMapItemPrivate()
321{
322}
323
324QDeclarativeCircleMapItemPrivateCPU::QDeclarativeCircleMapItemPrivateCPU(QDeclarativeCircleMapItem &circle)
325 : QDeclarativeCircleMapItemPrivate(circle)
326{
327 m_shape = new QQuickShape(&m_circle);
328 m_shape->setObjectName("_qt_map_item_shape");
329 m_shape->setZ(-1);
330 m_shape->setContainsMode(QQuickShape::FillContains);
331
332 m_shapePath = new QQuickShapePath(m_shape);
333 m_painterPath = new QDeclarativeGeoMapPainterPath(m_shapePath);
334
335 auto pathElements = m_shapePath->pathElements();
336 pathElements.append(&pathElements, m_painterPath);
337
338 auto shapePaths = m_shape->data();
339 shapePaths.append(&shapePaths, m_shapePath);
340}
341
342QDeclarativeCircleMapItemPrivateCPU::~QDeclarativeCircleMapItemPrivateCPU()
343{
344 delete m_shape;
345}
346
347/*
348 * A workaround for circle path to be drawn correctly using a polygon geometry
349 * This method generates a polygon like
350 * ______________
351 * | ____ |
352 * \__/ \__/
353 */
354void QDeclarativeCircleMapItemPrivate::includeOnePoleInPath(QList<QDoubleVector2D> &path,
355 const QGeoCoordinate &center,
356 qreal distance, const QGeoProjectionWebMercator &p)
357{
358 const qreal poleLat = 90;
359 const qreal distanceToNorthPole = center.distanceTo(QGeoCoordinate(poleLat, 0));
360 const qreal distanceToSouthPole = center.distanceTo(QGeoCoordinate(-poleLat, 0));
361 const bool crossNorthPole = distanceToNorthPole < distance;
362 const bool crossSouthPole = distanceToSouthPole < distance;
363
364 if (!crossNorthPole && !crossSouthPole)
365 return;
366
367 if (crossNorthPole && crossSouthPole)
368 return;
369
370 const QRectF cameraRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(p.visibleGeometry());
371 const qreal xAtBorder = cameraRect.left();
372
373 // The strategy is to order the points from left to right as they appear on the screen.
374 // Then add the 3 missing sides that form the box for painting at the front and at the end of the list.
375 // We ensure that the box aligns with the cameraRect in order to avoid rendering it twice (wrap around).
376 // Notably, this leads to outlines at the right side of the map.
377 // Set xAtBorder to 0.0 to avoid this, however, for an increased rendering cost.
378 for (auto &c : path) {
379 c.setX(c.x());
380 while (c.x() - xAtBorder > 1.0)
381 c.setX(c.x() - 1.0);
382 while (c.x() - xAtBorder < 0.0)
383 c.setX(c.x() + 1.0);
384 }
385
386 std::sort(path.begin(), path.end(),
387 [](const QDoubleVector2D &a, const QDoubleVector2D &b) -> bool
388 {return a.x() < b.x();});
389
390 const qreal newPoleLat = crossNorthPole ? -0.1 : 1.1;
391 const QDoubleVector2D P1 = path.first() + QDoubleVector2D(1.0, 0.0);
392 const QDoubleVector2D P2 = path.last() - QDoubleVector2D(1.0, 0.0);
393 path.push_front(P2);
394 path.push_front(QDoubleVector2D(P2.x(), newPoleLat));
395 path.append(P1);
396 path.append(QDoubleVector2D(P1.x(), newPoleLat));
397}
398
399int QDeclarativeCircleMapItemPrivate::crossEarthPole(const QGeoCoordinate &center, qreal distance)
400{
401 qreal poleLat = 90;
402 QGeoCoordinate northPole = QGeoCoordinate(poleLat, center.longitude());
403 QGeoCoordinate southPole = QGeoCoordinate(-poleLat, center.longitude());
404 // approximate using great circle distance
405 qreal distanceToNorthPole = center.distanceTo(northPole);
406 qreal distanceToSouthPole = center.distanceTo(southPole);
407 return (distanceToNorthPole < distance? 1 : 0) +
408 (distanceToSouthPole < distance? 1 : 0);
409}
410
411void QDeclarativeCircleMapItemPrivate::calculatePeripheralPointsSimple(QList<QDoubleVector2D> &path,
412 const QGeoCoordinate &center,
413 qreal distance,
414 const QGeoProjectionWebMercator &p,
415 int steps)
416{
417 const double lambda = 0.0001;
418 const QDoubleVector2D c = p.geoToMapProjection(center);
419 const double lambda_geo = center.distanceTo(p.mapProjectionToGeo(c + QDoubleVector2D(lambda, 0)));
420 const qreal mapDistance = distance * lambda / lambda_geo;
421
422 for (int i = 0; i < steps; ++i) {
423 const qreal rad = 2 * M_PI * i / steps;
424 path << c + QDoubleVector2D(cos(rad), sin(rad)) * mapDistance;
425 }
426}
427
428void QDeclarativeCircleMapItemPrivate::calculatePeripheralPointsGreatCircle(QList<QDoubleVector2D> &path,
429 const QGeoCoordinate &center,
430 qreal distance,
431 const QGeoProjectionWebMercator &p,
432 int steps)
433{
434 // Calculate points based on great-circle distance
435 // Calculation is the same as GeoCoordinate's atDistanceAndAzimuth function
436 // but tweaked here for computing multiple points
437
438 // pre-calculations
439 steps = qMax(steps, 3);
440 qreal centerLon = center.longitude();
441 qreal latRad = QLocationUtils::radians(center.latitude());
442 qreal lonRad = QLocationUtils::radians(centerLon);
443 qreal cosLatRad = std::cos(latRad);
444 qreal sinLatRad = std::sin(latRad);
445 qreal ratio = (distance / QLocationUtils::earthMeanRadius());
446 qreal cosRatio = std::cos(ratio);
447 qreal sinRatio = std::sin(ratio);
448 qreal sinLatRad_x_cosRatio = sinLatRad * cosRatio;
449 qreal cosLatRad_x_sinRatio = cosLatRad * sinRatio;
450 for (int i = 0; i < steps; ++i) {
451 const qreal azimuthRad = 2 * M_PI * i / steps;
452 const qreal resultLatRad = std::asin(sinLatRad_x_cosRatio
453 + cosLatRad_x_sinRatio * std::cos(azimuthRad));
454 const qreal resultLonRad = lonRad + std::atan2(std::sin(azimuthRad) * cosLatRad_x_sinRatio,
455 cosRatio - sinLatRad * std::sin(resultLatRad));
456 const qreal lat2 = QLocationUtils::degrees(resultLatRad);
457 qreal lon2 = QLocationUtils::degrees(resultLonRad);
458
459 //Workaround as QGeoCoordinate does not take Longitudes outside [-180,180]
460 qreal offset = 0.0;
461 while (lon2 > 180.0) {
462 offset += 1.0;
463 lon2 -= 360.0;
464 }
465 while (lon2 < -180.0) {
466 offset -= 1.0;
467 lon2 += 360.0;
468 }
469 path << p.geoToMapProjection(QGeoCoordinate(lat2, lon2, center.altitude())) + QDoubleVector2D(offset, 0.0);
470 }
471}
472
473//////////////////////////////////////////////////////////////////////
474
475void QDeclarativeCircleMapItemPrivateCPU::updatePolish()
476{
477 if (!m_circle.m_circle.isValid()) {
478 m_geometry.clear();
479 m_circle.setWidth(0);
480 m_circle.setHeight(0);
481 m_shape->setVisible(false);
482 return;
483 }
484
485 const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(m_circle.map()->geoProjection());
486 QScopedValueRollback<bool> rollback(m_circle.m_updatingGeometry);
487 m_circle.m_updatingGeometry = true;
488
489 QList<QDoubleVector2D> circlePath = m_circlePath;
490
491 const QGeoCoordinate &center = m_circle.m_circle.center();
492 const qreal &radius = m_circle.m_circle.radius();
493
494 // if circle crosses north/south pole, then don't preserve circular shape,
495 int crossingPoles = m_circle.referenceSurface() == QLocation::ReferenceSurface::Globe ? crossEarthPole(center, radius) : 0;
496 if (crossingPoles == 1) { // If the circle crosses both poles, we will remove it from a rectangle
497 includeOnePoleInPath(circlePath, center, radius, p);
498 m_geometry.updateSourcePoints(*m_circle.map(), QList<QList<QDoubleVector2D>>{circlePath}, QGeoMapPolygonGeometry::DrawOnce);
499 }
500 else if (crossingPoles == 2) { // If the circle crosses both poles, we will remove it from a rectangle
501 // The circle covers both poles. This appears on the map as a total fill with a hole on the opposite side of the planet
502 // This can be represented by a rectangle that spans the entire planet with a hole defined by the calculated points.
503 // The points on one side have to be wraped around the globe
504 const qreal centerX = p.geoToMapProjection(center).x();
505 for (int i = 0; i < circlePath.count(); i++) {
506 if (circlePath.at(i).x() > centerX)
507 circlePath[i].setX(circlePath.at(i).x() - 1.0);
508 }
509 QRectF cameraRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(p.visibleGeometry());
510 const QRectF circleRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(circlePath);
511 QGeoMapPolygonGeometry::MapBorderBehaviour wrappingMode = QGeoMapPolygonGeometry::DrawOnce;
512 QList<QDoubleVector2D> surroundingRect;
513 if (cameraRect.contains(circleRect)){
514 cameraRect = cameraRect.adjusted(-0.1, -0.1, 0.2, 0.2);
515 surroundingRect = {{cameraRect.left(), cameraRect.top()}, {cameraRect.right(), cameraRect.top()},
516 {cameraRect.right(), cameraRect.bottom()}, {cameraRect.left() , cameraRect.bottom()}};
517 } else {
518 const qreal anchorRect = centerX;
519
520 surroundingRect = {{anchorRect, -0.1}, {anchorRect + 1.0, -0.1},
521 {anchorRect + 1.0, 1.1}, {anchorRect, 1.1}};
522 wrappingMode = QGeoMapPolygonGeometry::Duplicate;
523 }
524 m_geometry.updateSourcePoints(*m_circle.map(), {surroundingRect, circlePath}, wrappingMode);
525 } else {
526 m_geometry.updateSourcePoints(*m_circle.map(), QList<QList<QDoubleVector2D>>{circlePath});
527 }
528
529 m_circle.setShapeTriangulationScale(m_shape, m_geometry.maxCoord());
530
531 const bool hasBorder = m_circle.m_border.color().alpha() != 0 && m_circle.m_border.width() > 0;
532 const float borderWidth = hasBorder ? m_circle.m_border.width() : 0.0f;
533 m_shapePath->setStrokeColor(hasBorder ? m_circle.m_border.color() : Qt::transparent);
534 m_shapePath->setStrokeWidth(hasBorder ? borderWidth : -1.0f);
535 m_shapePath->setFillColor(m_circle.color());
536
537 const QRectF bb = m_geometry.sourceBoundingBox();
538 QPainterPath path = m_geometry.srcPath();
539 path.translate(-bb.left() + borderWidth, -bb.top() + borderWidth);
540 path.closeSubpath();
541 m_painterPath->setPath(path);
542
543 m_circle.setSize(bb.size());
544 m_shape->setSize(m_circle.size());
545 m_shape->setOpacity(m_circle.zoomLevelOpacity());
546 m_shape->setVisible(true);
547
548 m_circle.setPositionOnMap(m_geometry.origin(), -1 * bb.topLeft() + QPointF(borderWidth, borderWidth));
549}
550
551QSGNode *QDeclarativeCircleMapItemPrivateCPU::updateMapItemPaintNode(QSGNode *oldNode,
552 QQuickItem::UpdatePaintNodeData *data)
553{
554 Q_UNUSED(data);
555 delete oldNode;
556 if (m_geometry.isScreenDirty()) {
557 m_geometry.markClean();
558 }
559 return nullptr;
560}
561
562bool QDeclarativeCircleMapItemPrivateCPU::contains(const QPointF &point) const
563{
564 return m_shape->contains(m_circle.mapToItem(m_shape, point));
565}
566
567QT_END_NAMESPACE
#define M_PI
Definition qmath.h:201