/**************************************************************************** * * (c) 2009-2020 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org> * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. * ****************************************************************************/ #include "QGCMapPolyline.h" #include "QGCGeo.h" #include "JsonHelper.h" #include "QGCQGeoCoordinate.h" #include "QGCApplication.h" #include "KMLFileHelper.h" #include <QGeoRectangle> #include <QDebug> #include <QJsonArray> #include <QLineF> #include <QFile> #include <QDomDocument> const char* QGCMapPolyline::jsonPolylineKey = "polyline"; QGCMapPolyline::QGCMapPolyline(QObject* parent) : QObject (parent) , _dirty (false) , _interactive (false) , _resetActive (false) { _init(); } QGCMapPolyline::QGCMapPolyline(const QGCMapPolyline& other, QObject* parent) : QObject (parent) , _dirty (false) , _interactive (false) , _resetActive (false) { *this = other; _init(); } const QGCMapPolyline& QGCMapPolyline::operator=(const QGCMapPolyline& other) { clear(); QVariantList vertices = other.path(); for (int i=0; i<vertices.count(); i++) { appendVertex(vertices[i].value<QGeoCoordinate>()); } setDirty(true); return *this; } void QGCMapPolyline::_init(void) { connect(&_polylineModel, &QmlObjectListModel::dirtyChanged, this, &QGCMapPolyline::_polylineModelDirtyChanged); connect(&_polylineModel, &QmlObjectListModel::countChanged, this, &QGCMapPolyline::_polylineModelCountChanged); connect(this, &QGCMapPolyline::countChanged, this, &QGCMapPolyline::isValidChanged); connect(this, &QGCMapPolyline::countChanged, this, &QGCMapPolyline::isEmptyChanged); } void QGCMapPolyline::clear(void) { _polylinePath.clear(); emit pathChanged(); _polylineModel.clearAndDeleteContents(); emit cleared(); setDirty(true); } void QGCMapPolyline::adjustVertex(int vertexIndex, const QGeoCoordinate coordinate) { _polylinePath[vertexIndex] = QVariant::fromValue(coordinate); emit pathChanged(); _polylineModel.value<QGCQGeoCoordinate*>(vertexIndex)->setCoordinate(coordinate); setDirty(true); } void QGCMapPolyline::setDirty(bool dirty) { if (_dirty != dirty) { _dirty = dirty; if (!dirty) { _polylineModel.setDirty(false); } emit dirtyChanged(dirty); } } QGeoCoordinate QGCMapPolyline::_coordFromPointF(const QPointF& point) const { QGeoCoordinate coord; if (_polylinePath.count() > 0) { QGeoCoordinate tangentOrigin = _polylinePath[0].value<QGeoCoordinate>(); convertNedToGeo(-point.y(), point.x(), 0, tangentOrigin, &coord); } return coord; } QPointF QGCMapPolyline::_pointFFromCoord(const QGeoCoordinate& coordinate) const { if (_polylinePath.count() > 0) { double y, x, down; QGeoCoordinate tangentOrigin = _polylinePath[0].value<QGeoCoordinate>(); convertGeoToNed(coordinate, tangentOrigin, &y, &x, &down); return QPointF(x, -y); } return QPointF(); } void QGCMapPolyline::setPath(const QList<QGeoCoordinate>& path) { _beginResetIfNotActive(); _polylinePath.clear(); _polylineModel.clearAndDeleteContents(); for (const QGeoCoordinate& coord: path) { _polylinePath.append(QVariant::fromValue(coord)); _polylineModel.append(new QGCQGeoCoordinate(coord, this)); } setDirty(true); _endResetIfNotActive(); } void QGCMapPolyline::setPath(const QVariantList& path) { _beginResetIfNotActive(); _polylinePath = path; _polylineModel.clearAndDeleteContents(); for (int i=0; i<_polylinePath.count(); i++) { _polylineModel.append(new QGCQGeoCoordinate(_polylinePath[i].value<QGeoCoordinate>(), this)); } setDirty(true); _endResetIfNotActive(); } void QGCMapPolyline::saveToJson(QJsonObject& json) { QJsonValue jsonValue; JsonHelper::saveGeoCoordinateArray(_polylinePath, false /* writeAltitude*/, jsonValue); json.insert(jsonPolylineKey, jsonValue); setDirty(false); } bool QGCMapPolyline::loadFromJson(const QJsonObject& json, bool required, QString& errorString) { errorString.clear(); clear(); if (required) { if (!JsonHelper::validateRequiredKeys(json, QStringList(jsonPolylineKey), errorString)) { return false; } } else if (!json.contains(jsonPolylineKey)) { return true; } if (!JsonHelper::loadGeoCoordinateArray(json[jsonPolylineKey], false /* altitudeRequired */, _polylinePath, errorString)) { return false; } for (int i=0; i<_polylinePath.count(); i++) { _polylineModel.append(new QGCQGeoCoordinate(_polylinePath[i].value<QGeoCoordinate>(), this)); } setDirty(false); emit pathChanged(); return true; } QList<QGeoCoordinate> QGCMapPolyline::coordinateList(void) const { QList<QGeoCoordinate> coords; for (int i=0; i<_polylinePath.count(); i++) { coords.append(_polylinePath[i].value<QGeoCoordinate>()); } return coords; } void QGCMapPolyline::splitSegment(int vertexIndex) { int nextIndex = vertexIndex + 1; if (nextIndex > _polylinePath.length() - 1) { return; } QGeoCoordinate firstVertex = _polylinePath[vertexIndex].value<QGeoCoordinate>(); QGeoCoordinate nextVertex = _polylinePath[nextIndex].value<QGeoCoordinate>(); double distance = firstVertex.distanceTo(nextVertex); double azimuth = firstVertex.azimuthTo(nextVertex); QGeoCoordinate newVertex = firstVertex.atDistanceAndAzimuth(distance / 2, azimuth); if (nextIndex == 0) { appendVertex(newVertex); } else { _polylineModel.insert(nextIndex, new QGCQGeoCoordinate(newVertex, this)); _polylinePath.insert(nextIndex, QVariant::fromValue(newVertex)); emit pathChanged(); } } void QGCMapPolyline::appendVertex(const QGeoCoordinate& coordinate) { _polylinePath.append(QVariant::fromValue(coordinate)); _polylineModel.append(new QGCQGeoCoordinate(coordinate, this)); emit pathChanged(); } void QGCMapPolyline::removeVertex(int vertexIndex) { if (vertexIndex < 0 || vertexIndex > _polylinePath.length() - 1) { qWarning() << "Call to removeVertex with bad vertexIndex:count" << vertexIndex << _polylinePath.length(); return; } if (_polylinePath.length() <= 2) { // Don't allow the user to trash the polyline return; } QObject* coordObj = _polylineModel.removeAt(vertexIndex); coordObj->deleteLater(); _polylinePath.removeAt(vertexIndex); emit pathChanged(); } void QGCMapPolyline::setInteractive(bool interactive) { if (_interactive != interactive) { _interactive = interactive; emit interactiveChanged(interactive); } } QGeoCoordinate QGCMapPolyline::vertexCoordinate(int vertex) const { if (vertex >= 0 && vertex < _polylinePath.count()) { return _polylinePath[vertex].value<QGeoCoordinate>(); } else { qWarning() << "QGCMapPolyline::vertexCoordinate bad vertex requested"; return QGeoCoordinate(); } } QList<QPointF> QGCMapPolyline::nedPolyline(void) { QList<QPointF> nedPolyline; if (count() > 0) { QGeoCoordinate tangentOrigin = vertexCoordinate(0); for (int i=0; i<_polylinePath.count(); i++) { double y, x, down; QGeoCoordinate vertex = vertexCoordinate(i); if (i == 0) { // This avoids a nan calculation that comes out of convertGeoToNed x = y = 0; } else { convertGeoToNed(vertex, tangentOrigin, &y, &x, &down); } nedPolyline += QPointF(x, y); } } return nedPolyline; } QList<QGeoCoordinate> QGCMapPolyline::offsetPolyline(double distance) { QList<QGeoCoordinate> rgNewPolyline; // I'm sure there is some beautiful famous algorithm to do this, but here is a brute force method if (count() > 1) { // Convert the polygon to NED QList<QPointF> rgNedVertices = nedPolyline(); // Walk the edges, offsetting by the specified distance QList<QLineF> rgOffsetEdges; for (int i=0; i<rgNedVertices.count() - 1; i++) { QLineF offsetEdge; QLineF originalEdge(rgNedVertices[i], rgNedVertices[i + 1]); QLineF workerLine = originalEdge; workerLine.setLength(distance); workerLine.setAngle(workerLine.angle() - 90.0); offsetEdge.setP1(workerLine.p2()); workerLine.setPoints(originalEdge.p2(), originalEdge.p1()); workerLine.setLength(distance); workerLine.setAngle(workerLine.angle() + 90.0); offsetEdge.setP2(workerLine.p2()); rgOffsetEdges.append(offsetEdge); } QGeoCoordinate tangentOrigin = vertexCoordinate(0); // Add first vertex QGeoCoordinate coord; convertNedToGeo(rgOffsetEdges[0].p1().y(), rgOffsetEdges[0].p1().x(), 0, tangentOrigin, &coord); rgNewPolyline.append(coord); // Intersect the offset edges to generate new central vertices QPointF newVertex; for (int i=1; i<rgOffsetEdges.count(); i++) { if (rgOffsetEdges[i - 1].intersect(rgOffsetEdges[i], &newVertex) == QLineF::NoIntersection) { // Two lines are colinear newVertex = rgOffsetEdges[i].p2(); } convertNedToGeo(newVertex.y(), newVertex.x(), 0, tangentOrigin, &coord); rgNewPolyline.append(coord); } // Add last vertex int lastIndex = rgOffsetEdges.count() - 1; convertNedToGeo(rgOffsetEdges[lastIndex].p2().y(), rgOffsetEdges[lastIndex].p2().x(), 0, tangentOrigin, &coord); rgNewPolyline.append(coord); } return rgNewPolyline; } bool QGCMapPolyline::loadKMLFile(const QString& kmlFile) { _beginResetIfNotActive(); QString errorString; QList<QGeoCoordinate> rgCoords; if (!KMLFileHelper::loadPolylineFromFile(kmlFile, rgCoords, errorString)) { qgcApp()->showMessage(errorString); return false; } clear(); appendVertices(rgCoords); _endResetIfNotActive(); return true; } void QGCMapPolyline::_polylineModelDirtyChanged(bool dirty) { if (dirty) { setDirty(true); } } void QGCMapPolyline::_polylineModelCountChanged(int count) { emit countChanged(count); } double QGCMapPolyline::length(void) const { double length = 0; for (int i=0; i<_polylinePath.count() - 1; i++) { QGeoCoordinate from = _polylinePath[i].value<QGeoCoordinate>(); QGeoCoordinate to = _polylinePath[i+1].value<QGeoCoordinate>(); length += from.distanceTo(to); } return length; } void QGCMapPolyline::appendVertices(const QList<QGeoCoordinate>& coordinates) { _beginResetIfNotActive(); QList<QObject*> objects; for (const QGeoCoordinate& coordinate: coordinates) { objects.append(new QGCQGeoCoordinate(coordinate, this)); _polylinePath.append(QVariant::fromValue(coordinate)); } _polylineModel.append(objects); _endResetIfNotActive(); } void QGCMapPolyline::beginReset(void) { _resetActive = true; _polylineModel.beginReset(); } void QGCMapPolyline::endReset(void) { _resetActive = false; _polylineModel.endReset(); emit pathChanged(); } void QGCMapPolyline::_beginResetIfNotActive(void) { if (!_resetActive) { beginReset(); } } void QGCMapPolyline::_endResetIfNotActive(void) { if (!_resetActive) { endReset(); } }