Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qgeopath.h"
#include "qgeopolygon.h"
#include "qgeopath_p.h"
#include "qgeocoordinate.h"
#include "qnumeric.h"
#include "qlocationutils_p.h"
#include "qwebmercator_p.h"
#include "qdoublevector2d_p.h"
#include <QtCore/qmutex.h>
#include <mutex>
QT_BEGIN_NAMESPACE
QT_IMPL_METATYPE_EXTERN(QGeoPath)
constexpr auto kWarningString = u"The path has more elements than fit into an int. "
"This can cause errors while querying elements from QML";
/*!
\class QGeoPath
\inmodule QtPositioning
\ingroup QtPositioning-positioning
\since 5.9
\brief The QGeoPath class defines a geographic path.
The path is defined by an ordered list of \l QGeoCoordinate objects.
Each two adjacent elements in the path are intended to be connected
together by the shortest line segment of constant bearing passing
through both elements.
This type of connection can cross the dateline in the longitudinal direction,
but never crosses the poles.
This is relevant for the calculation of the bounding box returned by
\l QGeoShape::boundingGeoRectangle() for this shape, which will have the latitude of
the top left corner set to the maximum latitude in the path point set.
Similarly, the latitude of the bottom right corner will be the minimum latitude
in the path point set.
This class is also accessible in QML as \l[QML]{geoPath}.
A QGeoPath is both invalid and empty if it contains no coordinate.
\note A default constructed QGeoPath is both invalid and empty as it does not contain any coordinates.
*/
/*!
\property QGeoPath::path
\brief This property holds the list of coordinates for the geo path.
\note The coordinates cannot be processed in place. To change the value
of this property, retrieve the complete list of coordinates, process them,
and assign the new value to the property.
*/
inline QGeoPathPrivate *QGeoPath::d_func()
{
return static_cast<QGeoPathPrivate *>(d_ptr.data());
}
inline const QGeoPathPrivate *QGeoPath::d_func() const
{
return static_cast<const QGeoPathPrivate *>(d_ptr.constData());
}
/*!
Constructs a new, empty geo path.
*/
QGeoPath::QGeoPath()
: QGeoShape(new QGeoPathPrivate())
{
}
/*!
Constructs a new geo path from a list of coordinates
(\a path and \a width).
*/
QGeoPath::QGeoPath(const QList<QGeoCoordinate> &path, const qreal &width)
: QGeoShape(new QGeoPathPrivate(path, width))
{
}
/*!
Constructs a new geo path from the contents of \a other.
*/
QGeoPath::QGeoPath(const QGeoPath &other)
: QGeoShape(other)
{
}
/*!
Constructs a new geo path from the contents of \a other.
*/
QGeoPath::QGeoPath(const QGeoShape &other)
: QGeoShape(other)
{
if (type() != QGeoShape::PathType)
d_ptr = new QGeoPathPrivate();
}
/*!
Destroys this path.
*/
QGeoPath::~QGeoPath() {}
/*!
Assigns \a other to this geo path and returns a reference to this geo path.
*/
QGeoPath &QGeoPath::operator=(const QGeoPath &other)
{
QGeoShape::operator=(other);
return *this;
}
/*!
Sets all the elements of the \a path.
*/
void QGeoPath::setPath(const QList<QGeoCoordinate> &path)
{
Q_D(QGeoPath);
return d->setPath(path);
}
/*!
Returns all the elements of the path.
*/
const QList<QGeoCoordinate> &QGeoPath::path() const
{
Q_D(const QGeoPath);
return d->path();
}
/*!
Clears the path.
\since 5.12
*/
void QGeoPath::clearPath()
{
Q_D(QGeoPath);
d->clearPath();
}
/*!
Sets all the elements of the path.
\internal
*/
void QGeoPath::setVariantPath(const QVariantList &path)
{
Q_D(QGeoPath);
QList<QGeoCoordinate> p;
for (const auto &c: path) {
if (c.canConvert<QGeoCoordinate>())
p << c.value<QGeoCoordinate>();
}
d->setPath(p);
}
/*!
Returns all the elements of the path.
\internal
*/
QVariantList QGeoPath::variantPath() const
{
Q_D(const QGeoPath);
QVariantList p;
for (const auto &c: d->path())
p << QVariant::fromValue(c);
return p;
}
/*!
\property QGeoPath::width
\brief the width of the path in meters.
*/
void QGeoPath::setWidth(const qreal &width)
{
Q_D(QGeoPath);
d->setWidth(width);
}
/*!
Returns the width of the path, in meters. This information is used in the \l contains method.
The default value is 0.
*/
qreal QGeoPath::width() const
{
Q_D(const QGeoPath);
return d->width();
}
/*!
Translates this geo path by \a degreesLatitude northwards and \a degreesLongitude eastwards.
Negative values of \a degreesLatitude and \a degreesLongitude correspond to
southward and westward translation respectively.
*/
void QGeoPath::translate(double degreesLatitude, double degreesLongitude)
{
Q_D(QGeoPath);
d->translate(degreesLatitude, degreesLongitude);
}
/*!
Returns a copy of this geo path translated by \a degreesLatitude northwards and
\a degreesLongitude eastwards.
Negative values of \a degreesLatitude and \a degreesLongitude correspond to
southward and westward translation respectively.
\sa translate()
*/
QGeoPath QGeoPath::translated(double degreesLatitude, double degreesLongitude) const
{
QGeoPath result(*this);
result.translate(degreesLatitude, degreesLongitude);
return result;
}
/*!
Returns the length of the path, in meters, from the element \a indexFrom to the element \a indexTo.
The length is intended to be the sum of the shortest distances for each pair of adjacent points.
If \a indexTo is -1 (the default value), the length will be including the distance between last coordinate
and the first (closed loop).
To retrieve the length for the path, use 0 for \a indexFrom and \l QGeoPath::size() - 1 for \a indexTo.
*/
double QGeoPath::length(qsizetype indexFrom, qsizetype indexTo) const
{
Q_D(const QGeoPath);
return d->length(indexFrom, indexTo);
}
/*!
Returns the number of elements in the path.
\since 5.10
*/
qsizetype QGeoPath::size() const
{
Q_D(const QGeoPath);
const qsizetype result = d->size();
if (result > std::numeric_limits<int>::max())
qWarning() << kWarningString;
return result;
}
/*!
Appends \a coordinate to the path.
*/
void QGeoPath::addCoordinate(const QGeoCoordinate &coordinate)
{
Q_D(QGeoPath);
d->addCoordinate(coordinate);
if (d->size() > std::numeric_limits<int>::max())
qWarning() << kWarningString;
}
/*!
Inserts \a coordinate at the specified \a index.
*/
void QGeoPath::insertCoordinate(qsizetype index, const QGeoCoordinate &coordinate)
{
Q_D(QGeoPath);
d->insertCoordinate(index, coordinate);
}
/*!
Replaces the path element at the specified \a index with \a coordinate.
*/
void QGeoPath::replaceCoordinate(qsizetype index, const QGeoCoordinate &coordinate)
{
Q_D(QGeoPath);
d->replaceCoordinate(index, coordinate);
}
/*!
Returns the coordinate at \a index .
*/
QGeoCoordinate QGeoPath::coordinateAt(qsizetype index) const
{
Q_D(const QGeoPath);
return d->coordinateAt(index);
}
/*!
Returns true if the path contains \a coordinate as one of the elements.
*/
bool QGeoPath::containsCoordinate(const QGeoCoordinate &coordinate) const
{
Q_D(const QGeoPath);
return d->containsCoordinate(coordinate);
}
/*!
Removes the last occurrence of \a coordinate from the path.
*/
void QGeoPath::removeCoordinate(const QGeoCoordinate &coordinate)
{
Q_D(QGeoPath);
d->removeCoordinate(coordinate);
}
/*!
Removes element at position \a index from the path.
*/
void QGeoPath::removeCoordinate(qsizetype index)
{
Q_D(QGeoPath);
d->removeCoordinate(index);
}
/*!
Returns the geo path properties as a string.
*/
QString QGeoPath::toString() const
{
if (type() != QGeoShape::PathType) {
qWarning("Not a path");
return QStringLiteral("QGeoPath(not a path)");
}
QString pathString;
for (const auto &p : path())
pathString += p.toString() + QLatin1Char(',');
return QStringLiteral("QGeoPath([ %1 ])").arg(pathString);
}
/*******************************************************************************
*
* QGeoPathPrivateBase & friends
*
*******************************************************************************/
Q_CONSTINIT static QBasicMutex globalPathMutex;
QGeoPathPrivateBase::QGeoPathPrivateBase()
: QGeoShapePrivate(QGeoShape::PathType)
{
}
QGeoPathPrivateBase::QGeoPathPrivateBase(const QList<QGeoCoordinate> &path)
: QGeoPathPrivateBase()
{
setPath(path);
}
QGeoPathPrivateBase::QGeoPathPrivateBase(const QGeoPathPrivateBase &other)
: QGeoShapePrivate(other),
m_path(other.m_path)
{
// Do not copy cached members: they would need mutex locked!
// m_dirty is set to true by default, so that it'll properly trigger
// re-evaluation when needed.
}
QGeoPathPrivateBase::~QGeoPathPrivateBase()
{
}
bool QGeoPathPrivateBase::isValid() const
{
return !isEmpty();
}
bool QGeoPathPrivateBase::isEmpty() const
{
return path().isEmpty(); // this should perhaps return geometric emptiness, less than 2 points for line, or empty polygon for polygons
}
QGeoCoordinate QGeoPathPrivateBase::center() const
{
return boundingGeoRectangle().center();
}
bool QGeoPathPrivateBase::operator==(const QGeoShapePrivate &other) const
{
if (!QGeoShapePrivate::operator==(other)) // checks type
return false;
const QGeoPathPrivateBase &otherBase = static_cast<const QGeoPathPrivateBase &>(other);
return m_path == otherBase.m_path;
}
QGeoRectangle QGeoPathPrivateBase::boundingGeoRectangle() const
{
ensureBoundingBoxUpdated();
return m_bbox;
}
size_t QGeoPathPrivateBase::hash(size_t seed) const
{
return qHashRange(m_path.cbegin(), m_path.cend(), seed);
}
const QList<QGeoCoordinate> &QGeoPathPrivateBase::path() const
{
return m_path;
}
double QGeoPathPrivateBase::length(qsizetype indexFrom, qsizetype indexTo) const
{
if (path().isEmpty())
return 0.0;
bool wrap = indexTo == -1;
if (indexTo < 0 || indexTo >= path().size())
indexTo = path().size() - 1;
double len = 0.0;
// TODO: consider calculating the length of the actual rhumb line segments
// instead of the shortest path from A to B.
for (qsizetype i = indexFrom; i < indexTo; i++)
len += m_path[i].distanceTo(m_path[i + 1]);
if (wrap)
len += m_path.last().distanceTo(m_path.first());
return len;
}
qsizetype QGeoPathPrivateBase::size() const
{
return m_path.size();
}
QGeoCoordinate QGeoPathPrivateBase::coordinateAt(qsizetype index) const
{
if (index < 0 || index >= m_path.size())
return QGeoCoordinate();
return m_path.at(index);
}
bool QGeoPathPrivateBase::containsCoordinate(const QGeoCoordinate &coordinate) const
{
return m_path.indexOf(coordinate) > -1;
}
void QGeoPathPrivateBase::translate(double degreesLatitude, double degreesLongitude)
{
// Need min/maxLati, so update bbox
QList<double> deltaXs;
double minX, maxX, minLati, maxLati;
computeBBox(m_path, deltaXs, minX, maxX, minLati, maxLati, m_bbox);
if (degreesLatitude > 0.0)
degreesLatitude = qMin(degreesLatitude, 90.0 - maxLati);
else
degreesLatitude = qMax(degreesLatitude, -90.0 - minLati);
for (QGeoCoordinate &p: m_path) {
p.setLatitude(p.latitude() + degreesLatitude);
p.setLongitude(QLocationUtils::wrapLong(p.longitude() + degreesLongitude));
}
m_bbox.translate(degreesLatitude, degreesLongitude);
m_bboxDirty.store(false, std::memory_order_release);
}
void QGeoPathPrivateBase::setPath(const QList<QGeoCoordinate> &path)
{
for (const QGeoCoordinate &c: path) {
if (!c.isValid())
return;
}
m_path = path;
markDirty();
}
void QGeoPathPrivateBase::clearPath()
{
m_path.clear();
markDirty();
}
void QGeoPathPrivateBase::addCoordinate(const QGeoCoordinate &coordinate)
{
if (!coordinate.isValid())
return;
m_path.append(coordinate);
markDirty();
}
void QGeoPathPrivateBase::insertCoordinate(qsizetype index, const QGeoCoordinate &coordinate)
{
if (index < 0 || index > m_path.size() || !coordinate.isValid())
return;
m_path.insert(index, coordinate);
markDirty();
}
void QGeoPathPrivateBase::replaceCoordinate(qsizetype index, const QGeoCoordinate &coordinate)
{
if (index < 0 || index >= m_path.size() || !coordinate.isValid())
return;
m_path[index] = coordinate;
markDirty();
}
void QGeoPathPrivateBase::removeCoordinate(const QGeoCoordinate &coordinate)
{
qsizetype index = m_path.lastIndexOf(coordinate);
removeCoordinate(index);
}
void QGeoPathPrivateBase::removeCoordinate(qsizetype index)
{
if (index < 0 || index >= m_path.size())
return;
m_path.removeAt(index);
markDirty();
}
void QGeoPathPrivateBase::markDirty()
{
m_bboxDirty.store(true, std::memory_order_release);
}
void QGeoPathPrivateBase::ensureBoundingBoxUpdated() const
{
if (m_bboxDirty.load(std::memory_order_acquire)) {
const std::scoped_lock lock(globalPathMutex);
if (m_bboxDirty.load(std::memory_order_acquire)) {
QList<double> deltaXs;
double minX, maxX, minLati, maxLati;
computeBBox(m_path, deltaXs, minX, maxX, minLati, maxLati, m_bbox);
m_bboxDirty.store(false, std::memory_order_release);
}
}
}
QGeoPathPrivate::QGeoPathPrivate()
: QGeoPathPrivateBase()
{
}
QGeoPathPrivate::QGeoPathPrivate(const QList<QGeoCoordinate> &path, const qreal width)
: QGeoPathPrivateBase(path)
{
setWidth(width);
}
QGeoPathPrivate::~QGeoPathPrivate()
{
}
QGeoShapePrivate *QGeoPathPrivate::clone() const
{
return new QGeoPathPrivate(*this);
}
bool QGeoPathPrivate::operator==(const QGeoShapePrivate &other) const
{
if (!QGeoPathPrivateBase::operator==(other)) // checks type
return false;
const QGeoPathPrivate &otherPath = static_cast<const QGeoPathPrivate &>(other);
return m_width == otherPath.m_width;
}
bool QGeoPathPrivate::lineContains(const QGeoCoordinate &coordinate) const
{
// Unoptimized approach:
// - consider each segment of the path (a rhumb line)
// - project it into Mercator space (in which it is straight)
// - find Mercator-closest point to coordinate
// - unproject the closest point
// - calculate coordinate to closest point distance with distanceTo()
// - if not within lineRadius, advance to next segment.
//
// When considering each segment, wrap its start-point to x-within 0.5 of
// its end-point and (initially) the target point to x-within the mid-point
// of the thus-narrowed segment. That position will usually find the actual
// Mercator-closest point and makes it easy to test for the obscure case
// where a different wrap-position for it might work better; when that is
// possible, try that other wrap-position for it in a second pass.
double lineRadius = qMax(width() * 0.5, 0.2); // minimum radius: 20cm
if (m_path.isEmpty())
return false;
else if (m_path.size() == 1)
return (m_path[0].distanceTo(coordinate) <= lineRadius);
Q_ASSERT(m_path.size() > 1);
const QDoubleVector2D pt = QWebMercator::coordToMercator(coordinate);
QDoubleVector2D last = QWebMercator::coordToMercator(m_path[0]);
for (qsizetype i = 1; i < m_path.size(); i++) {
const QDoubleVector2D here = QWebMercator::coordToMercator(m_path[i]);
// Wrap last to gets its x() within ±0.5 of that of here:
if (here.x() > last.x() + 0.5)
last.setX(last.x() + 1.0);
else if (here.x() < last.x() - 0.5)
last.setX(last.x() - 1.0);
if (here == last) {
// The whole line segment is one point, so easy to test.
if (m_path[i].distanceTo(coordinate) <= lineRadius)
return true;
continue;
}
QDoubleVector2D p = pt;
{
QDoubleVector2D mid = (last + here) / 2.0;
// Wrap p to gets its x() within ±0.5 of that of mid:
if (p.x() > mid.x() + 0.5)
p.setX(p.x() - 1.0);
else if (p.x() < mid.x() - 0.5)
p.setX(p.x() + 1.0);
}
// See comment on updating p after j == 0; loop to catch the corner case.
for (int j = 0; j < 2; ++j) {
const QDoubleVector2D pml = p - last, hml = here - last;
const double u = (pml.x() * hml.x() + pml.y() * hml.y()) / hml.lengthSquared();
// Interpolate between 0 < u < 1, use nearer end otherwise:
const QDoubleVector2D candidate = u > 0 ? u < 1 ? last + u * hml : here : last;
const double distance = coordinate.distanceTo(QWebMercator::mercatorToCoord(candidate));
if (distance <= lineRadius)
return true;
if (j == 0) {
/* Our initial p's .x() is within 0.5 of mid.x(), hence of at
least one of last.x() and here.x(). If it's within 0.5 of
both of these, the candidate we just tried is definitely our
best bet. Otherwise - though it's very obscure and can't
matter unless lineRadius is comparable with the rhumb line's
distance from the planet's spin axis - there are technically
two parabolic segments p could be in (y-closer to one end but
x-closer to the other), that would put p further from
last:here than from its more x()-distant end's wrapped
version the other side of p. So wrap p to its version that
would be closer to that distant end, were this to arise.
*/
if (p.x() > qMin(last.x(), here.x()) + 0.5)
p.setX(p.x() - 1.0);
else if (p.x() < qMax(last.x(), here.x()) - 0.5)
p.setX(p.x() + 1.0);
else
break;
}
}
// swap
last = here;
}
return false;
}
bool QGeoPathPrivate::contains(const QGeoCoordinate &coordinate) const
{
return lineContains(coordinate);
}
qreal QGeoPathPrivate::width() const
{
return m_width;
}
void QGeoPathPrivate::setWidth(const qreal &width)
{
if (qIsNaN(width) || width < 0.0)
return;
m_width = width;
}
size_t QGeoPathPrivate::hash(size_t seed) const
{
const size_t res = QGeoPathPrivateBase::hash(seed);
return qHashMulti(seed, res, m_width);
}
QGeoPathPrivateEager::QGeoPathPrivateEager()
: QGeoPathPrivate()
{
m_bboxDirty.store(false, std::memory_order_relaxed); // never dirty on the eager version
}
QGeoPathPrivateEager::QGeoPathPrivateEager(const QList<QGeoCoordinate> &path, const qreal width)
: QGeoPathPrivate(path, width)
{
m_bboxDirty.store(false, std::memory_order_relaxed); // never dirty on the eager version
markDirty(); // calculate the cached values
}
QGeoPathPrivateEager::QGeoPathPrivateEager(const QGeoPathPrivateEager &other)
: QGeoPathPrivate(other)
{
m_bboxDirty.store(false, std::memory_order_relaxed); // never dirty on the eager version
markDirty(); // calculate the cached values
}
QGeoPathPrivateEager::~QGeoPathPrivateEager()
{
}
QGeoShapePrivate *QGeoPathPrivateEager::clone() const
{
return new QGeoPathPrivateEager(*this);
}
void QGeoPathPrivateEager::markDirty()
{
// do the calculations directly
computeBBox(m_path, m_deltaXs, m_minX, m_maxX, m_minLati, m_maxLati, m_bbox);
}
void QGeoPathPrivateEager::translate(double degreesLatitude, double degreesLongitude)
{
if (degreesLatitude > 0.0)
degreesLatitude = qMin(degreesLatitude, 90.0 - m_maxLati);
else
degreesLatitude = qMax(degreesLatitude, -90.0 - m_minLati);
for (QGeoCoordinate &p: m_path) {
p.setLatitude(p.latitude() + degreesLatitude);
p.setLongitude(QLocationUtils::wrapLong(p.longitude() + degreesLongitude));
}
m_bbox.translate(degreesLatitude, degreesLongitude);
m_minLati += degreesLatitude;
m_maxLati += degreesLatitude;
}
void QGeoPathPrivateEager::addCoordinate(const QGeoCoordinate &coordinate)
{
if (!coordinate.isValid())
return;
m_path.append(coordinate);
//m_clipperDirty = true; // clipper not used in polylines
updateBoundingBox();
}
void QGeoPathPrivateEager::QGeoPathPrivateEager::updateBoundingBox()
{
updateBBox(m_path, m_deltaXs, m_minX, m_maxX, m_minLati, m_maxLati, m_bbox);
}
QGeoPathEager::QGeoPathEager() : QGeoPath()
{
d_ptr = new QGeoPathPrivateEager;
}
QGeoPathEager::QGeoPathEager(const QList<QGeoCoordinate> &path, const qreal &width) : QGeoPath()
{
d_ptr = new QGeoPathPrivateEager(path, width);
}
QGeoPathEager::QGeoPathEager(const QGeoPath &other) : QGeoPath()
{
d_ptr = new QGeoPathPrivateEager;
setPath(other.path());
setWidth(other.width());
}
QGeoPathEager::QGeoPathEager(const QGeoShape &other) : QGeoPath()
{
if (other.type() == QGeoShape::PathType)
*this = QGeoPathEager(QGeoPath(other));
else
d_ptr = new QGeoPathPrivateEager;
}
QGeoPathEager::~QGeoPathEager() {}
QT_END_NAMESPACE
#include "moc_qgeopath_p.cpp"
#include "moc_qgeopath.cpp"