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
qdeclarativepolygonmapitem.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 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
8
9#include <QtCore/QScopedValueRollback>
10#include <qnumeric.h>
11#include <QPainterPath>
12
13#include <QtLocation/private/qgeomap_p.h>
14#include <QtPositioning/private/qlocationutils_p.h>
15#include <QtPositioning/private/qdoublevector2d_p.h>
16#include <QtPositioning/private/qclipperutils_p.h>
17#include <QtPositioning/private/qgeopolygon_p.h>
18#include <QtPositioning/private/qwebmercator_p.h>
19
21
22/*!
23 \qmltype MapPolygon
24 \nativetype QDeclarativePolygonMapItem
25 \inqmlmodule QtLocation
26 \ingroup qml-QtLocation5-maps
27 \since QtLocation 5.5
28
29 \brief The MapPolygon type displays a polygon on a Map.
30
31 The MapPolygon type displays a polygon on a Map, specified in terms of an ordered list of
32 \l {QtPositioning::coordinate}{coordinates}. For best appearance and results, polygons should be
33 simple (not self-intersecting).
34
35 Coordinates can be added or removed at any time using the \l addCoordinate and
36 \l removeCoordinate methods. They can also be modified like any other list element in QML:
37
38 \code
39 mapPolygon.path[0].latitude = 5;
40 \endcode
41
42 For drawing rectangles with "straight" edges (same latitude across one
43 edge, same latitude across the other), the \l MapRectangle type provides
44 a simpler, two-point API.
45
46 By default, the polygon is displayed as a 1 pixel black border with no
47 fill. To change its appearance, use the \l color, \l border.color and
48 \l border.width properties.
49
50 \note Since MapPolygons are geographic items, dragging a MapPolygon
51 (through the use of \l MouseArea or \l PointHandler) causes its vertices to be
52 recalculated in the geographic coordinate space. The edges retain the
53 same geographic lengths (latitude and longitude differences between the
54 vertices), but they remain straight. Apparent stretching of the item occurs
55 when dragged to a different latitude.
56
57 \section2 Example Usage
58
59 The following snippet shows a MapPolygon being used to display a triangle,
60 with three vertices near Brisbane, Australia. The triangle is filled in
61 green, with a 1 pixel black border.
62
63 \code
64 Map {
65 MapPolygon {
66 color: 'green'
67 path: [
68 { latitude: -27, longitude: 153.0 },
69 { latitude: -27, longitude: 154.1 },
70 { latitude: -28, longitude: 153.5 }
71 ]
72 }
73 }
74 \endcode
75
76 \image api-mappolygon.png
77*/
78
79/*!
80 \qmlproperty bool QtLocation::MapPolygon::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::MapPolygon::referenceSurface
92
93 This property determines the reference surface of the polygon. If it is set to
94 \l QLocation::ReferenceSurface::Map the polygons vertices are connected with straight
95 lines on the map. If it is set to \l QLocation::ReferenceSurface::Globe, the vertices
96 are connected following the great circle path, describing the shortest connection of
97 two points on a sphere.
98 Default value is \l QLocation::ReferenceSurface::Map.
99
100 \since 6.5
101*/
102
103QGeoMapPolygonGeometry::QGeoMapPolygonGeometry() = default;
104
105/*!
106 \internal
107*/
108void QGeoMapPolygonGeometry::updateSourcePoints(const QGeoMap &map,
109 const QList<QList <QDoubleVector2D>> &basePaths,
110 MapBorderBehaviour wrapping)
111{
112 // A polygon consists of mutliple paths. This is usually a perimeter and multiple holes
113 // We move all paths into a single QPainterPath. The filling rule EvenOdd will then ensure that the paths are shown correctly
114 if (!sourceDirty_)
115 return;
116 const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(map.geoProjection());
117 srcPath_ = QPainterPath();
118 srcOrigin_ = p.mapProjectionToGeo(QDoubleVector2D(0.0, 0.0)); //avoid warning of NaN values if function is returned early
119 const QRectF cameraRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(p.visibleGeometry());
120
121 QList<QList<QDoubleVector2D>> paths;
122
123 if (wrapping == WrapAround) {
124 // 0.1 Wrap the points around the globe if the path makes more sense that way.
125 // Ultimately, this is done if it is closer to walk around the day-border than the other direction
126 paths.reserve(basePaths.size());
127 for (qsizetype j = 0; j< basePaths.size(); j++) {
128 const QList<QDoubleVector2D> &bp = basePaths[j];
129 if (bp.isEmpty())
130 continue;
131 paths << QList<QDoubleVector2D>({bp[0]});
132 QList<QDoubleVector2D> &pp = paths[j];
133 pp.reserve(bp.size());
134 for (qsizetype i = 1; i < bp.size(); i++) {
135 if (bp[i].x() > pp.last().x() + 0.5)
136 pp << bp[i] - QDoubleVector2D(1.0, 0.0);
137 else if (bp[i].x() < pp.last().x() - 0.5)
138 pp << bp[i] + QDoubleVector2D(1.0, 0.0);
139 else
140 pp << bp[i];
141 }
142 }
143
144 // 0.2 Check and include one of the poles if necessary to make sense out of the polygon
145 for (qsizetype j = 0; j < paths.size(); j++) {
146 QList<QDoubleVector2D> &pp = paths[j];
147
148 if (pp.last().x() - pp.first().x() < -0.5) {
149 for (qsizetype i = 0; i < floor(pp.length()/2.); i++)
150 pp.swapItemsAt(i, pp.length() - i - 1);
151 }
152 if (pp.last().x() - pp.first().x() > 0.5) {
153
154 const double leftBorder = cameraRect.left();
155 const double rightBorder = cameraRect.right();
156
157 qsizetype originalPathLength = pp.length();
158
159 if (pp.last().x() < rightBorder) {
160 for (qsizetype i = 0; i < originalPathLength; i++)
161 pp.append(pp[i] + QDoubleVector2D(1.0, 0.0));
162 }
163 if (pp.first().x() > leftBorder) {
164 for (qsizetype i = 0; i < originalPathLength; i++)
165 pp.insert(i, pp[2*i] - QDoubleVector2D(1.0, 0.0));
166 }
167 const double newPoleLat = (pp.first().y() + pp.last().y() < 1.0) ? 0.0 : 1.0; //mean of y < 0.5?
168 const QDoubleVector2D P1 = pp.first();
169 const QDoubleVector2D P2 = pp.last();
170 pp.push_front(QDoubleVector2D(P1.x(), newPoleLat));
171 pp.append(QDoubleVector2D(P2.x(), newPoleLat));
172
173 wrapping = DrawOnce;
174 }
175 }
176 } else {
177 paths = basePaths;
178 }
179
180 //1 The bounding rectangle of the polygon and camera view are compared to determine if the polygon is visible
181 // The viewport is periodic in x-direction in the interval [-1; 1].
182 // The polygon (maybe) has to be ploted periodically too by shifting it by -1 or +1;
183 QList<QList<QDoubleVector2D>> wrappedPaths;
184
185 if (wrapping == Duplicate || wrapping == WrapAround) {
186 QRectF itemRect;
187 for (const auto &path : paths)
188 itemRect |= QDeclarativeGeoMapItemUtils::boundingRectangleFromList(path);
189
190 for (double xoffset : {-1.0, 0.0, 1.0}) {
191 if (!cameraRect.intersects(itemRect.translated(QPointF(xoffset, 0.0))))
192 continue;
193 for (const auto &path : paths) {
194 wrappedPaths.append(QList<QDoubleVector2D>());
195 QList<QDoubleVector2D> &wP = wrappedPaths.last();
196 wP.reserve(path.size());
197 for (const QDoubleVector2D &coord : path)
198 wP.append(coord+QDoubleVector2D(xoffset, 0.0));
199 }
200 }
201 } else {
202 wrappedPaths = paths;
203 }
204
205 if (wrappedPaths.isEmpty()) // the polygon boundary rectangle does not overlap with the viewport rectangle
206 return;
207
208
209 //2 The polygons that are at least partially in the viewport are cliped to reduce their size
210 QList<QList<QDoubleVector2D>> clippedPaths;
211 const QList<QDoubleVector2D> &visibleRegion = p.visibleGeometryExpanded();
212 for (const auto &path : wrappedPaths) {
213 if (visibleRegion.size()) {
214 QClipperUtils clipper;
215 clipper.addSubjectPath(path, true);
216 clipper.addClipPolygon(visibleRegion);
217 clippedPaths << clipper.execute(QClipperUtils::Intersection, QClipperUtils::pftEvenOdd,
218 QClipperUtils::pftEvenOdd);
219 }
220 else {
221 clippedPaths.append(path); //Do we really need this if there are no visible regions??
222 }
223 }
224 if (clippedPaths.isEmpty()) //the polygon is entirely outside visibleRegion
225 return;
226
227 QRectF bb;
228 for (const auto &path: clippedPaths)
229 bb |= QDeclarativeGeoMapItemUtils::boundingRectangleFromList(path);
230 //Offset by origin, find the maximum coordinate
231 srcOrigin_ = p.mapProjectionToGeo(QDoubleVector2D(bb.left(), bb.top()));
232 QDoubleVector2D origin = p.wrappedMapProjectionToItemPosition(p.geoToWrappedMapProjection(srcOrigin_)); //save way: redo all projections
233 maxCoord_ = 0.0;
234 for (const auto &path: clippedPaths) {
235 QDoubleVector2D prevPoint = p.wrappedMapProjectionToItemPosition(path.at(0)) - origin;
236 QDoubleVector2D nextPoint = p.wrappedMapProjectionToItemPosition(path.at(1)) - origin;
237 srcPath_.moveTo(prevPoint.toPointF());
238 maxCoord_ = qMax(maxCoord_, qMax(prevPoint.x(), prevPoint.y()));
239 qsizetype pointsAdded = 1;
240 for (qsizetype i = 1; i < path.size(); ++i) {
241 const QDoubleVector2D point = nextPoint;
242
243 if (qMax(point.x(), point.y()) > maxCoord_)
244 maxCoord_ = qMax(point.x(), point.y());
245
246 if (i == path.size() - 1) {
247 srcPath_.lineTo(point.toPointF()); //close the path
248 } else {
249 nextPoint = p.wrappedMapProjectionToItemPosition(path.at(i+1)) - origin;
250
251 bool addPoint = ( i > pointsAdded * 10 || //make sure that at least every 10th point is drawn
252 path.size() < 10 ); //draw small paths completely
253
254 const double tolerance = 0.1;
255 if (!addPoint) { //add the point to the shape if it deflects the boundary by more than the tolerance
256 const double dsqr = QDeclarativeGeoMapItemUtils::distanceSqrPointLine(
257 point.x(), point.y(),
258 nextPoint.x(), nextPoint.y(),
259 prevPoint.x(), prevPoint.y());
260 addPoint = addPoint || (dsqr > (tolerance*tolerance));
261 }
262
263 if (addPoint) {
264 srcPath_.lineTo(point.toPointF());
265 pointsAdded++;
266 prevPoint = point;
267 }
268
269 }
270 }
271 srcPath_.closeSubpath();
272 }
273
274 if (!assumeSimple_)
275 srcPath_ = srcPath_.simplified();
276
277 sourceBounds_ = srcPath_.boundingRect();
278}
279
280/*
281 * QDeclarativePolygonMapItem Private Implementations
282 */
283
284QDeclarativePolygonMapItemPrivate::~QDeclarativePolygonMapItemPrivate()
285{
286}
287
288QDeclarativePolygonMapItemPrivateCPU::QDeclarativePolygonMapItemPrivateCPU(QDeclarativePolygonMapItem &polygon)
289 : QDeclarativePolygonMapItemPrivate(polygon)
290{
291 m_shape = new QQuickShape(&m_poly);
292 m_shape->setObjectName("_qt_map_item_shape");
293 m_shape->setZ(-1);
294 m_shape->setContainsMode(QQuickShape::FillContains);
295
296 m_shapePath = new QQuickShapePath(m_shape);
297 m_painterPath = new QDeclarativeGeoMapPainterPath(m_shapePath);
298
299 auto pathElements = m_shapePath->pathElements();
300 pathElements.append(&pathElements, m_painterPath);
301
302 auto shapePaths = m_shape->data();
303 shapePaths.append(&shapePaths, m_shapePath);
304}
305
306QDeclarativePolygonMapItemPrivateCPU::~QDeclarativePolygonMapItemPrivateCPU()
307{
308 delete m_shape;
309}
310
311void QDeclarativePolygonMapItemPrivateCPU::updatePolish()
312{
313 if (m_poly.m_geopoly.perimeter().length() == 0) { // Possibly cleared
314 m_geometry.clear();
315 m_poly.setWidth(0);
316 m_poly.setHeight(0);
317 m_shape->setVisible(false);
318 return;
319 }
320 const QGeoMap *map = m_poly.map();
321 const qreal borderWidth = m_poly.m_border.width();
322 QScopedValueRollback<bool> rollback(m_poly.m_updatingGeometry);
323 m_poly.m_updatingGeometry = true;
324
325 m_geometry.updateSourcePoints(*map, m_geopathProjected,
326 m_poly.referenceSurface() == QLocation::ReferenceSurface::Globe ?
327 QGeoMapPolygonGeometry::WrapAround :
328 QGeoMapPolygonGeometry::Duplicate);
329
330 const QRectF bb = m_geometry.sourceBoundingBox();
331
332 m_poly.setShapeTriangulationScale(m_shape, m_geometry.maxCoord());
333
334 const bool hasBorder = m_poly.m_border.color().alpha() != 0 && m_poly.m_border.width() > 0;
335 m_shapePath->setStrokeColor(hasBorder ? m_poly.m_border.color() : Qt::transparent);
336 m_shapePath->setStrokeWidth(hasBorder ? borderWidth : -1.0f);
337 m_shapePath->setFillColor(m_poly.color());
338
339 QPainterPath path = m_geometry.srcPath();
340 path.translate(-bb.left() + borderWidth, -bb.top() + borderWidth);
341 path.closeSubpath();
342 m_painterPath->setPath(path);
343
344 m_poly.setSize(bb.size() + QSize(2 * borderWidth, 2 * borderWidth));
345 m_shape->setSize(m_poly.size());
346 m_shape->setOpacity(m_poly.zoomLevelOpacity());
347 m_shape->setVisible(true);
348
349 m_poly.setPositionOnMap(m_geometry.origin(), -1 * bb.topLeft() + QPointF(borderWidth, borderWidth));
350}
351
352QSGNode *QDeclarativePolygonMapItemPrivateCPU::updateMapItemPaintNode(QSGNode *oldNode,
353 QQuickItem::UpdatePaintNodeData *data)
354{
355 Q_UNUSED(data);
356 delete oldNode;
357 if (m_geometry.isScreenDirty()) {
358 m_geometry.markClean();
359 }
360 return nullptr;
361}
362
363bool QDeclarativePolygonMapItemPrivateCPU::contains(const QPointF &point) const
364{
365 return m_shape->contains(m_poly.mapToItem(m_shape, point));
366}
367
368/*
369 * QDeclarativePolygonMapItem Implementation
370 */
371
372QDeclarativePolygonMapItem::QDeclarativePolygonMapItem(QQuickItem *parent)
373: QDeclarativeGeoMapItemBase(parent), m_border(this), m_color(Qt::transparent),
374 m_updatingGeometry(false)
375 , m_d(new QDeclarativePolygonMapItemPrivateCPU(*this))
376
377{
378 // ToDo: handle envvar, and switch implementation.
379 m_itemType = QGeoMap::MapPolygon;
380 m_geopoly = QGeoPolygonEager();
381 setFlag(ItemHasContents, true);
382 // ToDo: fix this, only flag material?
383 QObject::connect(&m_border, &QDeclarativeMapLineProperties::colorChanged,
384 this, &QDeclarativePolygonMapItem::onLinePropertiesChanged);
385 QObject::connect(&m_border, &QDeclarativeMapLineProperties::widthChanged,
386 this, &QDeclarativePolygonMapItem::onLinePropertiesChanged);
387 QObject::connect(this, &QDeclarativePolygonMapItem::referenceSurfaceChanged,
388 this, [this]() { m_d->onGeoGeometryChanged(); });
389}
390
391QDeclarativePolygonMapItem::~QDeclarativePolygonMapItem()
392{
393}
394
395/*!
396 \qmlpropertygroup Location::MapPolygon::border
397 \qmlproperty int MapPolygon::border.width
398 \qmlproperty color MapPolygon::border.color
399
400 This property is part of the border property group. The border property
401 group holds the width and color used to draw the border of the polygon.
402
403 The width is in pixels and is independent of the zoom level of the map.
404
405 The default values correspond to a black border with a width of 1 pixel.
406 For no line, use a width of 0 or a transparent color.
407*/
408
409QDeclarativeMapLineProperties *QDeclarativePolygonMapItem::border()
410{
411 return &m_border;
412}
413
414/*!
415 \internal
416*/
417void QDeclarativePolygonMapItem::setMap(QDeclarativeGeoMap *quickMap, QGeoMap *map)
418{
419 QDeclarativeGeoMapItemBase::setMap(quickMap,map);
420 if (map)
421 m_d->onMapSet();
422}
423
424/*!
425 \qmlproperty list<coordinate> MapPolygon::path
426
427 This property holds the ordered list of coordinates which
428 define the polygon.
429 Having less than 3 different coordinates in the path results in undefined behavior.
430
431 \sa addCoordinate, removeCoordinate
432*/
433QList<QGeoCoordinate> QDeclarativePolygonMapItem::path() const
434{
435 return m_geopoly.perimeter();
436}
437
438void QDeclarativePolygonMapItem::setPath(const QList<QGeoCoordinate> &path)
439{
440 // Equivalent to QDeclarativePolylineMapItem::setPathFromGeoList
441 if (m_geopoly.perimeter() == path)
442 return;
443
444 m_geopoly.setPerimeter(path);
445
446 m_d->onGeoGeometryChanged();
447 emit pathChanged();
448}
449
450/*!
451 \qmlmethod void MapPolygon::addCoordinate(coordinate)
452
453 Adds the specified \a coordinate to the path.
454
455 \sa removeCoordinate, path
456*/
457
458void QDeclarativePolygonMapItem::addCoordinate(const QGeoCoordinate &coordinate)
459{
460 if (!coordinate.isValid())
461 return;
462
463 m_geopoly.addCoordinate(coordinate);
464 m_d->onGeoGeometryUpdated();
465 emit pathChanged();
466}
467
468/*!
469 \qmlmethod void MapPolygon::removeCoordinate(coordinate)
470
471 Removes \a coordinate from the path. If there are multiple instances of the
472 same coordinate, the one added last is removed.
473
474 If \a coordinate is not in the path this method does nothing.
475
476 \sa addCoordinate, path
477*/
478void QDeclarativePolygonMapItem::removeCoordinate(const QGeoCoordinate &coordinate)
479{
480 int length = m_geopoly.perimeter().length();
481 m_geopoly.removeCoordinate(coordinate);
482 if (m_geopoly.perimeter().length() == length)
483 return;
484
485 m_d->onGeoGeometryChanged();
486 emit pathChanged();
487}
488
489/*!
490 \qmlproperty color MapPolygon::color
491
492 This property holds the color used to fill the polygon.
493
494 The default value is transparent.
495*/
496
497QColor QDeclarativePolygonMapItem::color() const
498{
499 return m_color;
500}
501
502void QDeclarativePolygonMapItem::setColor(const QColor &color)
503{
504 if (m_color == color)
505 return;
506
507 m_color = color;
508 polishAndUpdate(); // in case color was transparent and now is not or vice versa
509 emit colorChanged(m_color);
510}
511
512/*!
513 \internal
514*/
515QSGNode *QDeclarativePolygonMapItem::updateMapItemPaintNode(QSGNode *oldNode, UpdatePaintNodeData *data)
516{
517 return m_d->updateMapItemPaintNode(oldNode, data);
518}
519
520/*!
521 \internal
522*/
523void QDeclarativePolygonMapItem::updatePolish()
524{
525 if (!map() || map()->geoProjection().projectionType() != QGeoProjection::ProjectionWebMercator)
526 return;
527 m_d->updatePolish();
528}
529
530void QDeclarativePolygonMapItem::markSourceDirtyAndUpdate()
531{
532 m_d->markSourceDirtyAndUpdate();
533}
534
535void QDeclarativePolygonMapItem::onLinePropertiesChanged()
536{
537 m_d->onLinePropertiesChanged();
538}
539
540/*!
541 \internal
542*/
543void QDeclarativePolygonMapItem::afterViewportChanged(const QGeoMapViewportChangeEvent &event)
544{
545 if (event.mapSize.isEmpty())
546 return;
547
548 m_d->afterViewportChanged();
549}
550
551/*!
552 \internal
553*/
554bool QDeclarativePolygonMapItem::contains(const QPointF &point) const
555{
556 return m_d->contains(point);
557}
558
559const QGeoShape &QDeclarativePolygonMapItem::geoShape() const
560{
561 return m_geopoly;
562}
563
564void QDeclarativePolygonMapItem::setGeoShape(const QGeoShape &shape)
565{
566 if (shape == m_geopoly)
567 return;
568
569 m_geopoly = QGeoPolygonEager(shape);
570 m_d->onGeoGeometryChanged();
571 emit pathChanged();
572}
573
574/*!
575 \internal
576*/
577void QDeclarativePolygonMapItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
578{
579 if (newGeometry.topLeft() == oldGeometry.topLeft() || !map() || !m_geopoly.isValid() || m_updatingGeometry) {
580 QDeclarativeGeoMapItemBase::geometryChange(newGeometry, oldGeometry);
581 return;
582 }
583 // TODO: change the algorithm to preserve the distances and size!
584 QGeoCoordinate newCenter = map()->geoProjection().itemPositionToCoordinate(QDoubleVector2D(newGeometry.center()), false);
585 QGeoCoordinate oldCenter = map()->geoProjection().itemPositionToCoordinate(QDoubleVector2D(oldGeometry.center()), false);
586 if (!newCenter.isValid() || !oldCenter.isValid())
587 return;
588 double offsetLongi = newCenter.longitude() - oldCenter.longitude();
589 double offsetLati = newCenter.latitude() - oldCenter.latitude();
590 if (offsetLati == 0.0 && offsetLongi == 0.0)
591 return;
592
593 m_geopoly.translate(offsetLati, offsetLongi);
594 m_d->onGeoGeometryChanged();
595 emit pathChanged();
596
597 // Not calling QDeclarativeGeoMapItemBase::geometryChange() as it will be called from a nested
598 // call to this function.
599}
600
601//////////////////////////////////////////////////////////////////////
602
603QT_END_NAMESPACE
Combined button and popup list for selecting options.