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 
49 QT_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 
QMapQuickItemMatrix4x4(QObject * parent)129 QMapQuickItemMatrix4x4::QMapQuickItemMatrix4x4(QObject *parent) : QQuickTransform(parent) { }
130 
setMatrix(const QMatrix4x4 & matrix)131 void QMapQuickItemMatrix4x4::setMatrix(const QMatrix4x4 &matrix)
132 {
133     if (m_matrix == matrix)
134         return;
135     m_matrix = matrix;
136     update();
137 }
138 
applyTo(QMatrix4x4 * matrix) const139 void QMapQuickItemMatrix4x4::applyTo(QMatrix4x4 *matrix) const
140 {
141     *matrix *= m_matrix;
142 }
143 
144 
QDeclarativeGeoMapQuickItem(QQuickItem * parent)145 QDeclarativeGeoMapQuickItem::QDeclarativeGeoMapQuickItem(QQuickItem *parent)
146 :   QDeclarativeGeoMapItemBase(parent), zoomLevel_(0.0),
147     mapAndSourceItemSet_(false), updatingGeometry_(false), matrix_(nullptr)
148 {
149     m_itemType = QGeoMap::MapQuickItem;
150     setFlag(ItemHasContents, true);
151     opacityContainer_ = new QQuickItem(this);
152     opacityContainer_->setParentItem(this);
153     opacityContainer_->setFlag(ItemHasContents, true);
154     setFiltersChildMouseEvents(true);
155 }
156 
~QDeclarativeGeoMapQuickItem()157 QDeclarativeGeoMapQuickItem::~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 */
setCoordinate(const QGeoCoordinate & coordinate)172 void 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 */
setMap(QDeclarativeGeoMap * quickMap,QGeoMap * map)188 void QDeclarativeGeoMapQuickItem::setMap(QDeclarativeGeoMap *quickMap, QGeoMap *map)
189 {
190     QDeclarativeGeoMapItemBase::setMap(quickMap,map);
191     if (map && quickMap) {
192         connect(map, SIGNAL(cameraDataChanged(QGeoCameraData)),
193                 this, SLOT(polishAndUpdate()));
194         polishAndUpdate();
195     }
196 }
197 // See QQuickMultiPointTouchArea::childMouseEventFilter for reference
childMouseEventFilter(QQuickItem * receiver,QEvent * event)198 bool 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 */
geometryChanged(const QRectF & newGeometry,const QRectF & oldGeometry)215 void 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(p.geoToWrappedMapProjection(dragStartCoordinate_)))
231             return;
232 
233         QDoubleVector2D pos = map()->geoProjection().coordinateToItemPosition(dragStartCoordinate_, 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, false);
238     } else {
239         newCoordinate = map()->geoProjection().itemPositionToCoordinate(QDoubleVector2D(x(), y()) + QDoubleVector2D(anchorPoint_), 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 */
coordinate()252 QGeoCoordinate 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 */
setSourceItem(QQuickItem * sourceItem)262 void QDeclarativeGeoMapQuickItem::setSourceItem(QQuickItem *sourceItem)
263 {
264     QQuickItem *item = qobject_cast<QQuickItem *>(sourceItem); // Workaround for QTBUG-72930
265     if (sourceItem_.data() == item)
266         return;
267     sourceItem_ = item;
268     polishAndUpdate();
269     emit sourceItemChanged();
270 }
271 
sourceItem()272 QQuickItem *QDeclarativeGeoMapQuickItem::sourceItem()
273 {
274     return sourceItem_.data();
275 }
276 
277 /*!
278     \internal
279 */
afterChildrenChanged()280 void 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 *>(i)
288                     && sourceItem_.data() != i
289                     && opacityContainer_ != i) {
290                 if (!printedWarning) {
291                     qmlWarning(this) << "Use the sourceItem property for the contained item, direct children are not supported";
292                     printedWarning = true;
293                 }
294 
295                 qmlWarning(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 */
setAnchorPoint(const QPointF & anchorPoint)308 void QDeclarativeGeoMapQuickItem::setAnchorPoint(const QPointF &anchorPoint)
309 {
310     if (anchorPoint == anchorPoint_)
311         return;
312     anchorPoint_ = anchorPoint;
313     polishAndUpdate();
314     emit anchorPointChanged();
315 }
316 
anchorPoint() const317 QPointF 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 */
setZoomLevel(qreal zoomLevel)340 void 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 
zoomLevel() const350 qreal QDeclarativeGeoMapQuickItem::zoomLevel() const
351 {
352     return zoomLevel_;
353 }
354 
geoShape() const355 const 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 
setGeoShape(const QGeoShape & shape)362 void 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 */
updatePolish()380 void 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(sourceItem_.data(), SIGNAL(xChanged()),
398                 this, SLOT(polishAndUpdate()));
399         connect(sourceItem_.data(), SIGNAL(yChanged()),
400                 this, SLOT(polishAndUpdate()));
401         connect(sourceItem_.data(), SIGNAL(widthChanged()),
402                 this, SLOT(polishAndUpdate()));
403         connect(sourceItem_.data(), SIGNAL(heightChanged()),
404                 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(), anchorPoint_, 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(p.geoToWrappedMapProjection(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(), anchorPoint_, 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(), 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(map()->geoProjection().coordinateToItemPosition(coordinate(), false).x())) {
451                 opacityContainer_->setVisible(false);
452             } else {
453                 if (matrix_)
454                     matrix_->setMatrix(QMatrix4x4());
455                 setPositionOnMap(coordinate(), anchorPoint_);
456             }
457         }
458     }
459 }
460 
461 /*!
462     \internal
463 */
afterViewportChanged(const QGeoMapViewportChangeEvent & event)464 void 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 */
scaleFactor()476 qreal QDeclarativeGeoMapQuickItem::scaleFactor()
477 {
478     qreal scale = 1.0;
479     // use 1+x to avoid fuzzy compare against zero
480     if (!qFuzzyCompare(1.0 + zoomLevel_, 1.0))
481         scale = std::pow(0.5, zoomLevel_ - map()->cameraData().zoomLevel());
482     return scale;
483 }
484 
485 QT_END_NAMESPACE
486