1/****************************************************************************
2**
3** Copyright (C) 2015 The Qt Company Ltd.
4** Contact: http://www.qt.io/licensing/
5**
6** This file is part of the QtLocation module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL3$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see http://www.qt.io/terms-conditions. For further
15** information use the contact form at http://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPLv3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or later as published by the Free
28** Software Foundation and appearing in the file LICENSE.GPL included in
29** the packaging of this file. Please review the following information to
30** ensure the GNU General Public License version 2.0 requirements will be
31** met: http://www.gnu.org/licenses/gpl-2.0.html.
32**
33** $QT_END_LICENSE$
34**
35****************************************************************************/
36
37#include "qdeclarativegeomapquickitem_p.h"
38
39#include <QtCore/QScopedValueRollback>
40#include <QtQml/qqmlinfo.h>
41#include <QtQuick/QSGOpacityNode>
42#include <QtPositioning/private/qdoublevector2d_p.h>
43#include <QtQuick/private/qquickmousearea_p.h>
44#include <QtLocation/private/qgeomap_p.h>
45
46#include <QDebug>
47#include <cmath>
48
49QT_BEGIN_NAMESPACE
50
51/*!
52 \qmltype MapQuickItem
53 \instantiates QDeclarativeGeoMapQuickItem
54 \inqmlmodule QtLocation
55 \ingroup qml-QtLocation5-maps
56 \since QtLocation 5.5
57
58 \brief The MapQuickItem type displays an arbitrary Qt Quick object
59 on a Map.
60
61 The MapQuickItem type is used to place an arbitrary Qt Quick object
62 on a Map at a specified location and size. Compared to floating an item
63 above the Map, a MapQuickItem will follow the panning (and optionally, the
64 zooming) of the Map as if it is on the Map surface.
65
66 The \l{sourceItem} property contains the Qt Quick item to be drawn, which
67 can be any kind of visible type.
68
69 \section2 Positioning and Sizing
70
71 The positioning of the MapQuickItem on the Map is controlled by two
72 properties: \l coordinate and \l anchorPoint. If only \l coordinate is set,
73 it specifies a longitude/latitude coordinate for the item to be placed at.
74 The set coordinate will line up with the top-left corner of the contained
75 item when shown on the screen.
76
77 The \l anchorPoint property provides a way to line up the coordinate with
78 other parts of the item than just the top-left corner, by setting a number
79 of pixels the item will be offset by. A simple way to think about it is
80 to note that the point given by \l anchorPoint on the item itself is the
81 point that will line up with the given \l coordinate when displayed.
82
83 In addition to being anchored to the map, the MapQuickItem can optionally
84 follow the scale of the map, and change size when the Map is zoomed in or
85 zoomed out. This behaviour is controlled by the \l zoomLevel property. The
86 default behaviour if \l zoomLevel is not set is for the item to be drawn
87 "on the screen" rather than "on the map", so that its size remains the same
88 regardless of the zoom level of the Map.
89
90 \section2 Performance
91
92 Performance of a MapQuickItem is normally in the same ballpark as the
93 contained Qt Quick item alone. Overheads added amount to a translation
94 and (possibly) scaling of the original item, as well as a transformation
95 from longitude and latitude to screen position.
96
97 \section2 Limitations
98
99 \note Due to an implementation detail, items placed inside a
100 MapQuickItem will have a \c{parent} item which is not the MapQuickItem.
101 Refer to the MapQuickItem by its \c{id}, and avoid the use of \c{anchor}
102 in the \c{sourceItem}.
103
104 \section2 Example Usage
105
106 The following snippet shows a MapQuickItem containing an Image object,
107 to display a Marker on the Map. This strategy is used to show the map
108 markers in the MapViewer example.
109
110 \snippet mapviewer/map/Marker.qml mqi-top
111 \snippet mapviewer/map/Marker.qml mqi-anchor
112 \snippet mapviewer/map/Marker.qml mqi-closeimage
113 \snippet mapviewer/map/Marker.qml mqi-close
114
115 \image api-mapquickitem.png
116*/
117
118/*!
119 \qmlproperty bool QtLocation::MapQuickItem::autoFadeIn
120
121 This property holds whether the item automatically fades in when zooming into the map
122 starting from very low zoom levels. By default this is \c true.
123 Setting this property to \c false causes the map item to always have the opacity specified
124 with the \l QtQuick::Item::opacity property, which is 1.0 by default.
125
126 \since 5.14
127*/
128
129QMapQuickItemMatrix4x4::QMapQuickItemMatrix4x4(QObject *parent) : QQuickTransform(parent) { }
130
131void QMapQuickItemMatrix4x4::setMatrix(const QMatrix4x4 &matrix)
132{
133 if (m_matrix == matrix)
134 return;
135 m_matrix = matrix;
136 update();
137}
138
139void QMapQuickItemMatrix4x4::applyTo(QMatrix4x4 *matrix) const
140{
141 *matrix *= m_matrix;
142}
143
144
145QDeclarativeGeoMapQuickItem::QDeclarativeGeoMapQuickItem(QQuickItem *parent)
146: QDeclarativeGeoMapItemBase(parent), zoomLevel_(0.0),
147 mapAndSourceItemSet_(false), updatingGeometry_(false), matrix_(nullptr)
148{
149 m_itemType = QGeoMap::MapQuickItem;
150 setFlag(flag: ItemHasContents, enabled: true);
151 opacityContainer_ = new QQuickItem(this);
152 opacityContainer_->setParentItem(this);
153 opacityContainer_->setFlag(flag: ItemHasContents, enabled: true);
154 setFiltersChildMouseEvents(true);
155}
156
157QDeclarativeGeoMapQuickItem::~QDeclarativeGeoMapQuickItem() {}
158
159/*!
160 \qmlproperty coordinate MapQuickItem::coordinate
161
162 This property holds the anchor coordinate of the MapQuickItem. The point
163 on the sourceItem that is specified by anchorPoint is kept aligned with
164 this coordinate when drawn on the map.
165
166 In the image below, there are 3 MapQuickItems that are identical except
167 for the value of their anchorPoint properties. The values of anchorPoint
168 for each are written on top of the item.
169
170 \image api-mapquickitem-anchor.png
171*/
172void QDeclarativeGeoMapQuickItem::setCoordinate(const QGeoCoordinate &coordinate)
173{
174 if (coordinate_ == coordinate)
175 return;
176
177 coordinate_ = coordinate;
178 geoshape_.setTopLeft(coordinate_);
179 geoshape_.setBottomRight(coordinate_);
180 // TODO: Handle zoomLevel != 0.0
181 polishAndUpdate();
182 emit coordinateChanged();
183}
184
185/*!
186 \internal
187*/
188void QDeclarativeGeoMapQuickItem::setMap(QDeclarativeGeoMap *quickMap, QGeoMap *map)
189{
190 QDeclarativeGeoMapItemBase::setMap(quickMap,map);
191 if (map && quickMap) {
192 connect(sender: map, SIGNAL(cameraDataChanged(QGeoCameraData)),
193 receiver: this, SLOT(polishAndUpdate()));
194 polishAndUpdate();
195 }
196}
197// See QQuickMultiPointTouchArea::childMouseEventFilter for reference
198bool QDeclarativeGeoMapQuickItem::childMouseEventFilter(QQuickItem *receiver, QEvent *event)
199{
200 if (isEnabled() && isVisible()) {
201 switch (event->type()) {
202 case QEvent::MouseButtonPress:
203 case QEvent::TouchBegin:
204 dragStartCoordinate_ = coordinate_;
205 default:
206 break;
207 }
208 }
209 return QQuickItem::childMouseEventFilter(receiver, event);
210}
211
212/*!
213 \internal
214*/
215void QDeclarativeGeoMapQuickItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
216{
217 if (!mapAndSourceItemSet_ || updatingGeometry_ ||
218 newGeometry.topLeft() == oldGeometry.topLeft()) {
219 QDeclarativeGeoMapItemBase::geometryChanged(newGeometry, oldGeometry);
220 return;
221 }
222
223 QGeoCoordinate newCoordinate;
224 // with zoomLevel set the anchorPoint has to be factored into the transformation to properly transform around it.
225 if (zoomLevel_ != 0.0
226 && map()->geoProjection().projectionType() == QGeoProjection::ProjectionWebMercator) {
227 const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(map()->geoProjection());
228
229 // When dragStartCoordinate_ can't be projected to screen, dragging must be disabled.
230 if (!p.isProjectable(wrappedProjection: p.geoToWrappedMapProjection(coordinate: dragStartCoordinate_)))
231 return;
232
233 QDoubleVector2D pos = map()->geoProjection().coordinateToItemPosition(coordinate: dragStartCoordinate_, clipToViewport: false);
234 // oldGeometry.topLeft() is always intended to be (0,0), even when for some reason it's not.
235 pos.setX(pos.x() + newGeometry.topLeft().x());
236 pos.setY(pos.y() + newGeometry.topLeft().y());
237 newCoordinate = map()->geoProjection().itemPositionToCoordinate(pos, clipToViewport: false);
238 } else {
239 newCoordinate = map()->geoProjection().itemPositionToCoordinate(pos: QDoubleVector2D(x(), y()) + QDoubleVector2D(anchorPoint_), clipToViewport: false);
240 }
241
242 if (newCoordinate.isValid())
243 setCoordinate(newCoordinate);
244
245 // Not calling QDeclarativeGeoMapItemBase::geometryChanged() as it will be called from a nested
246 // call to this function.
247}
248
249/*!
250 \internal
251*/
252QGeoCoordinate QDeclarativeGeoMapQuickItem::coordinate()
253{
254 return coordinate_;
255}
256
257/*!
258 \qmlproperty object MapQuickItem::sourceItem
259
260 This property holds the source item that will be drawn on the map.
261*/
262void QDeclarativeGeoMapQuickItem::setSourceItem(QQuickItem *sourceItem)
263{
264 QQuickItem *item = qobject_cast<QQuickItem *>(object: sourceItem); // Workaround for QTBUG-72930
265 if (sourceItem_.data() == item)
266 return;
267 sourceItem_ = item;
268 polishAndUpdate();
269 emit sourceItemChanged();
270}
271
272QQuickItem *QDeclarativeGeoMapQuickItem::sourceItem()
273{
274 return sourceItem_.data();
275}
276
277/*!
278 \internal
279*/
280void QDeclarativeGeoMapQuickItem::afterChildrenChanged()
281{
282 QList<QQuickItem *> kids = childItems();
283 if (kids.size() > 0) {
284 bool printedWarning = false;
285 foreach (QQuickItem *i, kids) {
286 if (i->flags() & QQuickItem::ItemHasContents
287 && !qobject_cast<QQuickMouseArea *>(object: i)
288 && sourceItem_.data() != i
289 && opacityContainer_ != i) {
290 if (!printedWarning) {
291 qmlWarning(me: this) << "Use the sourceItem property for the contained item, direct children are not supported";
292 printedWarning = true;
293 }
294
295 qmlWarning(me: i) << "deleting this child";
296 i->deleteLater();
297 }
298 }
299 }
300}
301
302/*!
303 \qmlproperty QPointF MapQuickItem::anchorPoint
304
305 This property determines which point on the sourceItem that will be lined
306 up with the coordinate on the map.
307*/
308void QDeclarativeGeoMapQuickItem::setAnchorPoint(const QPointF &anchorPoint)
309{
310 if (anchorPoint == anchorPoint_)
311 return;
312 anchorPoint_ = anchorPoint;
313 polishAndUpdate();
314 emit anchorPointChanged();
315}
316
317QPointF QDeclarativeGeoMapQuickItem::anchorPoint() const
318{
319 return anchorPoint_;
320}
321
322/*!
323 \qmlproperty real MapQuickItem::zoomLevel
324
325 This property controls the scaling behaviour of the contents of the
326 MapQuickItem. In particular, by setting this property it is possible
327 to choose between objects that are drawn on the screen (and sized in
328 screen pixels), and those drawn on the map surface (which change size
329 with the zoom level of the map).
330
331 The default value for this property is 0.0, which corresponds to drawing
332 the object on the screen surface. If set to another value, the object will
333 be drawn on the map surface instead. The value (if not zero) specifies the
334 zoomLevel at which the object will be visible at a scale of 1:1 (ie, where
335 object pixels and screen pixels are the same). At zoom levels lower than
336 this, the object will appear smaller, and at higher zoom levels, appear
337 larger. This is in contrast to when this property is set to zero, where
338 the object remains the same size on the screen at all zoom levels.
339*/
340void QDeclarativeGeoMapQuickItem::setZoomLevel(qreal zoomLevel)
341{
342 if (zoomLevel == zoomLevel_)
343 return;
344 zoomLevel_ = zoomLevel;
345 // TODO: update geoshape_!
346 polishAndUpdate();
347 emit zoomLevelChanged();
348}
349
350qreal QDeclarativeGeoMapQuickItem::zoomLevel() const
351{
352 return zoomLevel_;
353}
354
355const QGeoShape &QDeclarativeGeoMapQuickItem::geoShape() const
356{
357 // TODO: return a QGeoRectangle representing the bounding geo rectangle of the quick item
358 // when zoomLevel_ is != 0.0
359 return geoshape_;
360}
361
362void QDeclarativeGeoMapQuickItem::setGeoShape(const QGeoShape &shape)
363{
364 if (shape == geoshape_)
365 return;
366
367 const QGeoRectangle rect = shape.boundingGeoRectangle();
368 geoshape_ = rect;
369 coordinate_ = rect.center();
370
371 // TODO: Handle zoomLevel != 0.0
372 polishAndUpdate();
373 emit coordinateChanged();
374
375}
376
377/*!
378 \internal
379*/
380void QDeclarativeGeoMapQuickItem::updatePolish()
381{
382 if (!quickMap() && sourceItem_) {
383 mapAndSourceItemSet_ = false;
384 sourceItem_.data()->setParentItem(0);
385 return;
386 }
387
388 if (!quickMap() || !map() || !sourceItem_) {
389 mapAndSourceItemSet_ = false;
390 return;
391 }
392
393 if (!mapAndSourceItemSet_ && quickMap() && map() && sourceItem_) {
394 mapAndSourceItemSet_ = true;
395 sourceItem_.data()->setParentItem(opacityContainer_);
396 sourceItem_.data()->setTransformOrigin(QQuickItem::TopLeft);
397 connect(sender: sourceItem_.data(), SIGNAL(xChanged()),
398 receiver: this, SLOT(polishAndUpdate()));
399 connect(sender: sourceItem_.data(), SIGNAL(yChanged()),
400 receiver: this, SLOT(polishAndUpdate()));
401 connect(sender: sourceItem_.data(), SIGNAL(widthChanged()),
402 receiver: this, SLOT(polishAndUpdate()));
403 connect(sender: sourceItem_.data(), SIGNAL(heightChanged()),
404 receiver: this, SLOT(polishAndUpdate()));
405 }
406
407 if (!coordinate_.isValid()) {
408 opacityContainer_->setVisible(false);
409 return;
410 } else {
411 opacityContainer_->setVisible(true);
412 }
413
414 QScopedValueRollback<bool> rollback(updatingGeometry_);
415 updatingGeometry_ = true;
416
417 opacityContainer_->setOpacity(zoomLevelOpacity());
418
419 setWidth(sourceItem_.data()->width());
420 setHeight(sourceItem_.data()->height());
421 if (zoomLevel_ != 0.0 // zoom level initialized to 0.0. If it's different, it has been set explicitly.
422 && map()->geoProjection().projectionType() == QGeoProjection::ProjectionWebMercator) { // Currently unsupported on any other projection
423 const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(map()->geoProjection());
424
425 if (!matrix_) {
426 matrix_ = new QMapQuickItemMatrix4x4(this);
427 matrix_->appendToItem(opacityContainer_);
428 }
429 matrix_->setMatrix(p.quickItemTransformation(coordinate: coordinate(), anchorPoint: anchorPoint_, zoomLevel: zoomLevel_));
430 setPosition(QPointF(0,0));
431 } else {
432 if (map()->geoProjection().projectionType() == QGeoProjection::ProjectionWebMercator) {
433 const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(map()->geoProjection());
434 if (map()->cameraData().tilt() > 0.0
435 && !p.isProjectable(wrappedProjection: p.geoToWrappedMapProjection(coordinate: coordinate()))) {
436 // if the coordinate is behind the camera, we use the transformation to get the item out of the way
437 if (!matrix_) {
438 matrix_ = new QMapQuickItemMatrix4x4(this);
439 matrix_->appendToItem(opacityContainer_);
440 }
441 matrix_->setMatrix(p.quickItemTransformation(coordinate: coordinate(), anchorPoint: anchorPoint_, zoomLevel: map()->cameraData().zoomLevel()));
442 setPosition(QPointF(0,0));
443 } else { // All good, rendering screen-aligned
444 if (matrix_)
445 matrix_->setMatrix(QMatrix4x4());
446 setPositionOnMap(coordinate: coordinate(), offset: anchorPoint_);
447 }
448 } else { // On other projections we can only currently test if coordinateToItemPosition returns a valid position
449 if (map()->cameraData().tilt() > 0.0
450 && qIsNaN(d: map()->geoProjection().coordinateToItemPosition(coordinate: coordinate(), clipToViewport: false).x())) {
451 opacityContainer_->setVisible(false);
452 } else {
453 if (matrix_)
454 matrix_->setMatrix(QMatrix4x4());
455 setPositionOnMap(coordinate: coordinate(), offset: anchorPoint_);
456 }
457 }
458 }
459}
460
461/*!
462 \internal
463*/
464void QDeclarativeGeoMapQuickItem::afterViewportChanged(const QGeoMapViewportChangeEvent &event)
465{
466 Q_UNUSED(event);
467 if (event.mapSize.width() <= 0 || event.mapSize.height() <= 0)
468 return;
469
470 polishAndUpdate();
471}
472
473/*!
474 \internal
475*/
476qreal QDeclarativeGeoMapQuickItem::scaleFactor()
477{
478 qreal scale = 1.0;
479 // use 1+x to avoid fuzzy compare against zero
480 if (!qFuzzyCompare(p1: 1.0 + zoomLevel_, p2: 1.0))
481 scale = std::pow(x: 0.5, y: zoomLevel_ - map()->cameraData().zoomLevel());
482 return scale;
483}
484
485QT_END_NAMESPACE
486

source code of qtlocation/src/location/declarativemaps/qdeclarativegeomapquickitem.cpp