diff options
Diffstat (limited to 'src/location/quickmapitems/qdeclarativepolylinemapitem.cpp')
-rw-r--r-- | src/location/quickmapitems/qdeclarativepolylinemapitem.cpp | 490 |
1 files changed, 87 insertions, 403 deletions
diff --git a/src/location/quickmapitems/qdeclarativepolylinemapitem.cpp b/src/location/quickmapitems/qdeclarativepolylinemapitem.cpp index 2477a4d8..e80afbee 100644 --- a/src/location/quickmapitems/qdeclarativepolylinemapitem.cpp +++ b/src/location/quickmapitems/qdeclarativepolylinemapitem.cpp @@ -3,7 +3,6 @@ #include "qdeclarativepolylinemapitem_p.h" #include "qdeclarativepolylinemapitem_p_p.h" -#include "qdeclarativegeomapitemutils_p.h" #include <QtCore/QScopedValueRollback> #include <qnumeric.h> @@ -296,421 +295,97 @@ void QDeclarativeMapLineProperties::setWidth(qreal width) emit widthChanged(width_); } -QGeoMapPolylineGeometry::QGeoMapPolylineGeometry() -{ -} - -QList<QList<QDoubleVector2D> > QGeoMapPolylineGeometry::clipPath(const QGeoMap &map, - const QList<QDoubleVector2D> &path, - QDoubleVector2D &leftBoundWrapped) -{ - /* - * Approach: - * 1) project coordinates to wrapped web mercator, and do unwrapBelowX - * 2) if the scene is tilted, clip the geometry against the visible region (this may generate multiple polygons) - * 2.1) recalculate the origin and geoLeftBound to prevent these parameters from ending in unprojectable areas - * 2.2) ensure the left bound does not wrap around due to QGeoCoordinate <-> clipper conversions - */ - const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(map.geoProjection()); - srcOrigin_ = geoLeftBound_; - - double unwrapBelowX = 0; - leftBoundWrapped = p.wrapMapProjection(p.geoToMapProjection(geoLeftBound_)); - if (preserveGeometry_) - unwrapBelowX = leftBoundWrapped.x(); - - QList<QDoubleVector2D> wrappedPath; - wrappedPath.reserve(path.size()); - QDoubleVector2D wrappedLeftBound(qInf(), qInf()); - // 1) - for (const auto &coord : path) { - QDoubleVector2D wrappedProjection = p.wrapMapProjection(coord); - - // We can get NaN if the map isn't set up correctly, or the projection - // is faulty -- probably best thing to do is abort - if (!qIsFinite(wrappedProjection.x()) || !qIsFinite(wrappedProjection.y())) - return QList<QList<QDoubleVector2D> >(); - - const bool isPointLessThanUnwrapBelowX = (wrappedProjection.x() < leftBoundWrapped.x()); - // unwrap x to preserve geometry if moved to border of map - if (preserveGeometry_ && isPointLessThanUnwrapBelowX) { - double distance = wrappedProjection.x() - unwrapBelowX; - if (distance < 0.0) - distance += 1.0; - wrappedProjection.setX(unwrapBelowX + distance); - } - if (wrappedProjection.x() < wrappedLeftBound.x() || (wrappedProjection.x() == wrappedLeftBound.x() && wrappedProjection.y() < wrappedLeftBound.y())) { - wrappedLeftBound = wrappedProjection; - } - wrappedPath.append(wrappedProjection); - } - -#ifdef QT_LOCATION_DEBUG - m_wrappedPath = wrappedPath; -#endif - - // 2) - QList<QList<QDoubleVector2D> > clippedPaths; - const QList<QDoubleVector2D> &visibleRegion = p.projectableGeometry(); - if (visibleRegion.size()) { - clippedPaths = clipLine(wrappedPath, visibleRegion); - - // 2.1) update srcOrigin_ and leftBoundWrapped with the point with minimum X - QDoubleVector2D lb(qInf(), qInf()); - for (const QList<QDoubleVector2D> &path: clippedPaths) { - for (const QDoubleVector2D &p: path) { - if (p == leftBoundWrapped) { - lb = p; - break; - } else if (p.x() < lb.x() || (p.x() == lb.x() && p.y() < lb.y())) { - // y-minimization needed to find the same point on polygon and border - lb = p; - } - } - } - if (qIsInf(lb.x())) - return QList<QList<QDoubleVector2D> >(); - - // 2.2) Prevent the conversion to and from clipper from introducing negative offsets which - // in turn will make the geometry wrap around. - lb.setX(qMax(wrappedLeftBound.x(), lb.x())); - leftBoundWrapped = lb; - } else { - clippedPaths.append(wrappedPath); - } - -#ifdef QT_LOCATION_DEBUG - m_clippedPaths = clippedPaths; -#endif - - return clippedPaths; -} - -void QGeoMapPolylineGeometry::pathToScreen(const QGeoMap &map, - const QList<QList<QDoubleVector2D> > &clippedPaths, - const QDoubleVector2D &leftBoundWrapped) -{ - const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(map.geoProjection()); - // 3) project the resulting geometry to screen position and calculate screen bounds - double minX = qInf(); - double minY = qInf(); - double maxX = -qInf(); - double maxY = -qInf(); - srcOrigin_ = p.mapProjectionToGeo(p.unwrapMapProjection(leftBoundWrapped)); - QDoubleVector2D origin = p.wrappedMapProjectionToItemPosition(leftBoundWrapped); - for (const QList<QDoubleVector2D> &path: clippedPaths) { - QDoubleVector2D lastAddedPoint; - for (qsizetype i = 0; i < path.size(); ++i) { - QDoubleVector2D point = p.wrappedMapProjectionToItemPosition(path.at(i)); - point = point - origin; // (0,0) if point == geoLeftBound_ - - minX = qMin(point.x(), minX); - minY = qMin(point.y(), minY); - maxX = qMax(point.x(), maxX); - maxY = qMax(point.y(), maxY); - - if (i == 0) { - srcPoints_ << point.x() << point.y(); - srcPointTypes_ << QPainterPath::MoveToElement; - lastAddedPoint = point; - } else { - if ((point - lastAddedPoint).manhattanLength() > 3 || - i == path.size() - 1) { - srcPoints_ << point.x() << point.y(); - srcPointTypes_ << QPainterPath::LineToElement; - lastAddedPoint = point; - } - } - } - } - - sourceBounds_ = QRectF(QPointF(minX, minY), QPointF(maxX, maxY)); -} - /*! \internal */ void QGeoMapPolylineGeometry::updateSourcePoints(const QGeoMap &map, - const QList<QDoubleVector2D> &path, - const QGeoCoordinate geoLeftBound) + const QList<QDoubleVector2D> &basePath) { + // A polygon consists of mutliple paths. This is usually a perimeter and multiple holes + // We move all paths into a single QPainterPath. The filling rule EvenOdd will then ensure that the paths are shown correctly if (!sourceDirty_) return; - - geoLeftBound_ = geoLeftBound; - - // clear the old data and reserve enough memory - srcPoints_.clear(); - srcPoints_.reserve(path.size() * 2); - srcPointTypes_.clear(); - srcPointTypes_.reserve(path.size()); - - /* - * Approach: - * 1) project coordinates to wrapped web mercator, and do unwrapBelowX - * 2) if the scene is tilted, clip the geometry against the visible region (this may generate multiple polygons) - * 3) project the resulting geometry to screen position and calculate screen bounds - */ - - QDoubleVector2D leftBoundWrapped; - // 1, 2) - const QList<QList<QDoubleVector2D> > &clippedPaths = clipPath(map, path, leftBoundWrapped); - - // 3) - pathToScreen(map, clippedPaths, leftBoundWrapped); - + const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(map.geoProjection()); srcPath_ = QPainterPath(); - maxCoord_ = 0.0; - const int elemCount = srcPointTypes_.count(); - for (int i = 0; i < elemCount; ++i) { - switch (srcPointTypes_[i]) { - case QPainterPath::MoveToElement: - { - const qreal x = srcPoints_[2 * i]; - const qreal y = srcPoints_[2 * i + 1]; - if (qMax(x, y) > maxCoord_) - maxCoord_ = qMax(x, y); - srcPath_.moveTo(x, y); - } - break; - case QPainterPath::LineToElement: - { - const qreal x = srcPoints_[2 * i]; - const qreal y = srcPoints_[2 * i + 1]; - if (qMax(x, y) > maxCoord_) - maxCoord_ = qMax(x, y); - srcPath_.lineTo(x, y); - } - break; - default: - break; - } + srcOrigin_ = p.mapProjectionToGeo(QDoubleVector2D(0, 0)); //avoid warning of NaN values if function is returned early + + //0 Wrap the points around the globe if the path makes more sense that way. + // Ultimately, this is done if it is closer to walk around the day-border than the other direction + QVarLengthArray<QList<QDoubleVector2D>, 3> wrappedPaths; + wrappedPaths << QList<QDoubleVector2D>({basePath[0]}); + wrappedPaths.last().reserve(basePath.size()); + for (int i = 1; i < basePath.size(); i++) { + if (basePath[i].x() > wrappedPaths.last().last().x() + 0.5) + wrappedPaths.last() << basePath[i] - QDoubleVector2D(1.0, 0.0); + else if (basePath[i].x() < wrappedPaths.last().last().x() - 0.5) + wrappedPaths.last() << basePath[i] + QDoubleVector2D(1.0, 0.0); + else + wrappedPaths.last() << basePath[i]; } -} - -// *** SCREEN CLIPPING *** // - -enum ClipPointType { - InsidePoint = 0x00, - LeftPoint = 0x01, - RightPoint = 0x02, - BottomPoint = 0x04, - TopPoint = 0x08 -}; -static inline int clipPointType(qreal x, qreal y, const QRectF &rect) -{ - int type = InsidePoint; - if (x < rect.left()) - type |= LeftPoint; - else if (x > rect.right()) - type |= RightPoint; - if (y < rect.top()) - type |= TopPoint; - else if (y > rect.bottom()) - type |= BottomPoint; - return type; -} - -static void clipSegmentToRect(qreal x0, qreal y0, qreal x1, qreal y1, const QRectF &clipRect, - QList<qreal> &outPoints, QList<QPainterPath::ElementType> &outTypes) -{ - int type0 = clipPointType(x0, y0, clipRect); - int type1 = clipPointType(x1, y1, clipRect); - bool accept = false; - - while (true) { - if (!(type0 | type1)) { - accept = true; - break; - } else if (type0 & type1) { - break; - } else { - qreal x = 0.0; - qreal y = 0.0; - int outsideType = type0 ? type0 : type1; - - if (outsideType & BottomPoint) { - x = x0 + (x1 - x0) * (clipRect.bottom() - y0) / (y1 - y0); - y = clipRect.bottom() - 0.1; - } else if (outsideType & TopPoint) { - x = x0 + (x1 - x0) * (clipRect.top() - y0) / (y1 - y0); - y = clipRect.top() + 0.1; - } else if (outsideType & RightPoint) { - y = y0 + (y1 - y0) * (clipRect.right() - x0) / (x1 - x0); - x = clipRect.right() - 0.1; - } else if (outsideType & LeftPoint) { - y = y0 + (y1 - y0) * (clipRect.left() - x0) / (x1 - x0); - x = clipRect.left() + 0.1; - } - - if (outsideType == type0) { - x0 = x; - y0 = y; - type0 = clipPointType(x0, y0, clipRect); - } else { - x1 = x; - y1 = y; - type1 = clipPointType(x1, y1, clipRect); - } - } + //1 The bounding rectangle of the polygon and camera view are compared to determine if the polygon is visible + // The viewport is periodic in x-direction in the interval [-1; 1]. + // The polygon (maybe) has to be ploted periodically too by shifting it by -1 or +1; + const QRectF cameraRect = QDeclarativeGeoMapItemUtils::boundingRectangleFromList(p.visibleGeometry()); + QRectF itemRect; + for (const auto &path : wrappedPaths) + itemRect |= QDeclarativeGeoMapItemUtils::boundingRectangleFromList(path).adjusted(-1e-6, -1e-6, 2e-6, 2e-6); //TODO: Maybe use linewidth? + for (double xoffset : {-1.0, 1.0}) { + if (!cameraRect.intersects(itemRect.translated(QPointF(xoffset,0)))) + continue; + wrappedPaths.append(QList<QDoubleVector2D>()); + QList<QDoubleVector2D> &wP = wrappedPaths.last(); + wP.reserve(wrappedPaths.first().size()); + for (const QDoubleVector2D &coord : wrappedPaths.first()) + wP.append(coord + QDoubleVector2D(xoffset, 0.0)); } + if (wrappedPaths.isEmpty()) // the polygon boundary rectangle does not overlap with the viewport rectangle + return; - if (accept) { - if (outPoints.size() >= 2) { - qreal lastX, lastY; - lastY = outPoints.at(outPoints.size() - 1); - lastX = outPoints.at(outPoints.size() - 2); - - if (!qFuzzyCompare(lastY, y0) || !qFuzzyCompare(lastX, x0)) { - outTypes << QPainterPath::MoveToElement; - outPoints << x0 << y0; - } + //2 The polygons that are at least partially in the viewport are cliped to reduce their size + QList<QList<QDoubleVector2D>> clippedPaths; + const QList<QDoubleVector2D> &visibleRegion = p.visibleGeometryExpanded(); + for (const auto &path : wrappedPaths) { + if (visibleRegion.size()) { + clippedPaths << clipLine(path, visibleRegion); + //TODO: Replace clipping with Clipper lib, similar to QPolygonMapItem } else { - outTypes << QPainterPath::MoveToElement; - outPoints << x0 << y0; - } - - outTypes << QPainterPath::LineToElement; - outPoints << x1 << y1; - } -} - -static void clipPathToRect(const QList<qreal> &points, - const QList<QPainterPath::ElementType> &types, const QRectF &clipRect, - QList<qreal> &outPoints, QList<QPainterPath::ElementType> &outTypes) -{ - outPoints.clear(); - outPoints.reserve(points.size()); - outTypes.clear(); - outTypes.reserve(types.size()); - - qreal lastX = 0; - qreal lastY = 0; // or else used uninitialized - for (qsizetype i = 0; i < types.size(); ++i) { - if (i > 0 && types[i] != QPainterPath::MoveToElement) { - qreal x = points[i * 2], y = points[i * 2 + 1]; - clipSegmentToRect(lastX, lastY, x, y, clipRect, outPoints, outTypes); + clippedPaths.append(path); //Do we really need this if there are no visible regions?? } - - lastX = points[i * 2]; - lastY = points[i * 2 + 1]; - } -} - -//////////////////////////////////////////////////////////////////////////// - -/*! - \internal -*/ -void QGeoMapPolylineGeometry::updateScreenPoints(const QGeoMap &map, - qreal strokeWidth, - bool adjustTranslation) -{ - if (!screenDirty_) - return; - - QPointF origin = map.geoProjection().coordinateToItemPosition(srcOrigin_, false).toPointF(); - - if (!qIsFinite(origin.x()) || !qIsFinite(origin.y()) || srcPointTypes_.size() < 2) { // the line might have been clipped away. - clear(); - return; - } - - // Create the viewport rect in the same coordinate system - // as the actual points - QRectF viewport(0, 0, map.viewportWidth(), map.viewportHeight()); - viewport.adjust(-strokeWidth, -strokeWidth, strokeWidth * 2, strokeWidth * 2); - viewport.translate(-1 * origin); - - QList<qreal> points; - QList<QPainterPath::ElementType> types; - - if (clipToViewport_) { - // Although the geometry has already been clipped against the visible region in wrapped mercator space. - // This is currently still needed to prevent a number of artifacts deriving from QTriangulatingStroker processing - // very large lines (that is, polylines that span many pixels in screen space) - clipPathToRect(srcPoints_, srcPointTypes_, viewport, points, types); - } else { - points = srcPoints_; - types = srcPointTypes_; } - - QVectorPath vp(points.data(), types.size(), types.data()); - QTriangulatingStroker ts; - // As of Qt5.11, the clip argument is not actually used, in the call below. - ts.process(vp, QPen(QBrush(Qt::black), strokeWidth), QRectF(), QPainter::Antialiasing); - - clear(); - - // Nothing is on the screen - if (ts.vertexCount() == 0) + if (clippedPaths.isEmpty()) //the polygon is entirely outside visibleRegion return; - // QTriangulatingStroker#vertexCount is actually the length of the array, - // not the number of vertices - screenVertices_.reserve(ts.vertexCount()); - QRectF bb; + for (const auto &path: clippedPaths) + bb |= QDeclarativeGeoMapItemUtils::boundingRectangleFromList(path); + //Offset by origin, find the maximum coordinate + maxCoord_ = 0.0; + srcOrigin_ = p.mapProjectionToGeo(QDoubleVector2D(bb.left(), bb.top())); + QDoubleVector2D origin = p.wrappedMapProjectionToItemPosition(p.geoToWrappedMapProjection(srcOrigin_)); //save way: redo all projections + for (const auto &path: clippedPaths) { + QDoubleVector2D lastAddedPoint; + for (qsizetype i = 0; i < path.size(); ++i) { + QDoubleVector2D point = p.wrappedMapProjectionToItemPosition(path.at(i)); + point = point - origin; // (0,0) if point == origin - QPointF pt; - const float *vs = ts.vertices(); - for (int i = 0; i < (ts.vertexCount()/2*2); i += 2) { - pt = QPointF(vs[i], vs[i + 1]); - screenVertices_ << pt; - - if (!qIsFinite(pt.x()) || !qIsFinite(pt.y())) - break; - - if (!bb.contains(pt)) { - if (pt.x() < bb.left()) - bb.setLeft(pt.x()); - - if (pt.x() > bb.right()) - bb.setRight(pt.x()); - - if (pt.y() < bb.top()) - bb.setTop(pt.y()); - - if (pt.y() > bb.bottom()) - bb.setBottom(pt.y()); - } - } - - screenBounds_ = bb; - - const QPointF strokeOffset = (adjustTranslation) ? QPointF(strokeWidth, strokeWidth) * 0.5: QPointF(); - const QPointF offset = -1 * sourceBounds_.topLeft() + strokeOffset; - for (qsizetype i = 0; i < screenVertices_.size(); ++i) - screenVertices_[i] += offset; - - firstPointOffset_ += offset; - screenOutline_.translate(offset); - screenBounds_.translate(offset); -} - -void QGeoMapPolylineGeometry::clearSource() -{ - srcPoints_.clear(); - srcPointTypes_.clear(); -} + if (qMax(point.x(), point.y()) > maxCoord_) + maxCoord_ = qMax(point.x(), point.y()); -bool QGeoMapPolylineGeometry::contains(const QPointF &point) const -{ - // screenOutline_.contains(screenPoint) doesn't work, as, it appears, that - // screenOutline_ for QGeoMapPolylineGeometry is empty (QRectF(0,0 0x0)) - const QList<QPointF> &verts = vertices(); - QPolygonF tri; - for (const auto &v : verts) { - tri << v; - if (tri.size() == 3) { - if (tri.containsPoint(point, Qt::OddEvenFill)) - return true; - tri.remove(0); + if (i == 0) { + srcPath_.moveTo(point.toPointF()); + lastAddedPoint = point; + } else { + if ((point - lastAddedPoint).manhattanLength() > 3 || + i == path.size() - 1) { + srcPath_.lineTo(point.toPointF()); + lastAddedPoint = point; + } + } } } - return false; + sourceBounds_ = srcPath_.boundingRect(); } /* @@ -750,7 +425,7 @@ void QDeclarativePolylineMapItemPrivateCPU::regenerateCache() return; const QGeoProjectionWebMercator &p = static_cast<const QGeoProjectionWebMercator&>(m_poly.map()->geoProjection()); m_geopathProjected.clear(); - m_geopathProjected.reserve(m_poly.m_geopath.size()); + m_geopathProjected.reserve(m_poly.m_geopath.path().size()); for (const QGeoCoordinate &c : m_poly.m_geopath.path()) m_geopathProjected << p.geoToMapProjection(c); } @@ -778,16 +453,14 @@ void QDeclarativePolylineMapItemPrivateCPU::updatePolish() const QGeoMap *map = m_poly.map(); const qreal borderWidth = m_poly.m_line.width(); - m_geometry.updateSourcePoints(*map, m_geopathProjected, m_poly.m_geopath.boundingGeoRectangle().topLeft()); - - // still needed even with Shapes, due to contains() - m_geometry.updateScreenPoints(*map, borderWidth); + m_geometry.updateSourcePoints(*map, m_geopathProjected); const QRectF bb = m_geometry.sourceBoundingBox(); m_poly.setSize(bb.size() + QSizeF(borderWidth, borderWidth)); // it has to be shifted so that the center of the line is on the correct geocoord m_poly.setPositionOnMap(m_geometry.origin(), -1 * bb.topLeft() + QPointF(borderWidth, borderWidth) * 0.5); + m_poly.setShapeTriangulationScale(m_shape, m_geometry.maxCoord_); m_shapePath->setStrokeColor(m_poly.m_line.color()); @@ -808,11 +481,8 @@ QSGNode *QDeclarativePolylineMapItemPrivateCPU::updateMapItemPaintNode(QSGNode * { delete oldNode; - //TODO: update only material - if (m_geometry.isScreenDirty() || m_poly.m_dirtyMaterial || !oldNode) { - m_geometry.setPreserveGeometry(false); + if (m_geometry.isScreenDirty() || !oldNode) { m_geometry.markClean(); - m_poly.m_dirtyMaterial = false; } return nullptr; } @@ -822,7 +492,21 @@ bool QDeclarativePolylineMapItemPrivateCPU::contains(const QPointF &point) const // With Shapes, do not just call // m_shape->contains(m_poly.mapToItem(m_shape, point)) because that can // only do FillContains at best, whereas the polyline relies on stroking. - return m_geometry.contains(point); + + const QPainterPath &path = m_geometry.srcPath_; + const double &lineWidth = m_poly.m_line.width(); + const QPointF p = m_poly.mapToItem(m_shape, point) - QPointF(lineWidth, lineWidth) * 0.5; + + for (int i = 1; i < path.elementCount(); i++) { + if (path.elementAt(i).type == QPainterPath::MoveToElement) + continue; + const double dsqr = QDeclarativeGeoMapItemUtils::distanceSqrPointLine(p.x(), p.y(), + path.elementAt(i - 1).x, path.elementAt(i - 1).y, + path.elementAt(i).x, path.elementAt(i).y); + if (dsqr < 0.25 * lineWidth * lineWidth) + return true; + } + return false; } /* |