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