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