You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
544 lines
16 KiB
544 lines
16 KiB
/**************************************************************************** |
|
* |
|
* (c) 2009-2016 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 "QGCMapPolygon.h" |
|
#include "QGCGeo.h" |
|
#include "JsonHelper.h" |
|
#include "QGCQGeoCoordinate.h" |
|
#include "QGCApplication.h" |
|
|
|
#include <QGeoRectangle> |
|
#include <QDebug> |
|
#include <QJsonArray> |
|
#include <QLineF> |
|
#include <QFile> |
|
#include <QDomDocument> |
|
|
|
const char* QGCMapPolygon::jsonPolygonKey = "polygon"; |
|
|
|
QGCMapPolygon::QGCMapPolygon(QObject* parent) |
|
: QObject (parent) |
|
, _dirty (false) |
|
, _centerDrag (false) |
|
, _ignoreCenterUpdates (false) |
|
, _interactive (false) |
|
{ |
|
_init(); |
|
} |
|
|
|
QGCMapPolygon::QGCMapPolygon(const QGCMapPolygon& other, QObject* parent) |
|
: QObject (parent) |
|
, _dirty (false) |
|
, _centerDrag (false) |
|
, _ignoreCenterUpdates (false) |
|
, _interactive (false) |
|
{ |
|
*this = other; |
|
|
|
_init(); |
|
} |
|
|
|
void QGCMapPolygon::_init(void) |
|
{ |
|
connect(&_polygonModel, &QmlObjectListModel::dirtyChanged, this, &QGCMapPolygon::_polygonModelDirtyChanged); |
|
connect(&_polygonModel, &QmlObjectListModel::countChanged, this, &QGCMapPolygon::_polygonModelCountChanged); |
|
connect(this, &QGCMapPolygon::pathChanged, this, &QGCMapPolygon::_updateCenter); |
|
} |
|
|
|
const QGCMapPolygon& QGCMapPolygon::operator=(const QGCMapPolygon& other) |
|
{ |
|
clear(); |
|
|
|
QVariantList vertices = other.path(); |
|
for (int i=0; i<vertices.count(); i++) { |
|
appendVertex(vertices[i].value<QGeoCoordinate>()); |
|
} |
|
|
|
setDirty(true); |
|
|
|
return *this; |
|
} |
|
|
|
void QGCMapPolygon::clear(void) |
|
{ |
|
// Bug workaround, see below |
|
while (_polygonPath.count() > 1) { |
|
_polygonPath.takeLast(); |
|
} |
|
emit pathChanged(); |
|
|
|
// Although this code should remove the polygon from the map it doesn't. There appears |
|
// to be a bug in QGCMapPolygon which causes it to not be redrawn if the list is empty. So |
|
// we work around it by using the code above to remove all but the last point which in turn |
|
// will cause the polygon to go away. |
|
_polygonPath.clear(); |
|
|
|
_polygonModel.clearAndDeleteContents(); |
|
|
|
emit cleared(); |
|
|
|
setDirty(true); |
|
} |
|
|
|
void QGCMapPolygon::adjustVertex(int vertexIndex, const QGeoCoordinate coordinate) |
|
{ |
|
_polygonPath[vertexIndex] = QVariant::fromValue(coordinate); |
|
if (!_centerDrag) { |
|
// When dragging center we don't signal path changed until add vertices are updated |
|
emit pathChanged(); |
|
} |
|
_polygonModel.value<QGCQGeoCoordinate*>(vertexIndex)->setCoordinate(coordinate); |
|
setDirty(true); |
|
} |
|
|
|
void QGCMapPolygon::setDirty(bool dirty) |
|
{ |
|
if (_dirty != dirty) { |
|
_dirty = dirty; |
|
if (!dirty) { |
|
_polygonModel.setDirty(false); |
|
} |
|
emit dirtyChanged(dirty); |
|
} |
|
} |
|
|
|
QGeoCoordinate QGCMapPolygon::_coordFromPointF(const QPointF& point) const |
|
{ |
|
QGeoCoordinate coord; |
|
|
|
if (_polygonPath.count() > 0) { |
|
QGeoCoordinate tangentOrigin = _polygonPath[0].value<QGeoCoordinate>(); |
|
convertNedToGeo(-point.y(), point.x(), 0, tangentOrigin, &coord); |
|
} |
|
|
|
return coord; |
|
} |
|
|
|
QPointF QGCMapPolygon::_pointFFromCoord(const QGeoCoordinate& coordinate) const |
|
{ |
|
if (_polygonPath.count() > 0) { |
|
double y, x, down; |
|
QGeoCoordinate tangentOrigin = _polygonPath[0].value<QGeoCoordinate>(); |
|
|
|
convertGeoToNed(coordinate, tangentOrigin, &y, &x, &down); |
|
return QPointF(x, -y); |
|
} |
|
|
|
return QPointF(); |
|
} |
|
|
|
QPolygonF QGCMapPolygon::_toPolygonF(void) const |
|
{ |
|
QPolygonF polygon; |
|
|
|
if (_polygonPath.count() > 2) { |
|
for (int i=0; i<_polygonPath.count(); i++) { |
|
polygon.append(_pointFFromCoord(_polygonPath[i].value<QGeoCoordinate>())); |
|
} |
|
} |
|
|
|
return polygon; |
|
} |
|
|
|
bool QGCMapPolygon::containsCoordinate(const QGeoCoordinate& coordinate) const |
|
{ |
|
if (_polygonPath.count() > 2) { |
|
return _toPolygonF().containsPoint(_pointFFromCoord(coordinate), Qt::OddEvenFill); |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
void QGCMapPolygon::setPath(const QList<QGeoCoordinate>& path) |
|
{ |
|
_polygonPath.clear(); |
|
_polygonModel.clearAndDeleteContents(); |
|
foreach(const QGeoCoordinate& coord, path) { |
|
_polygonPath.append(QVariant::fromValue(coord)); |
|
_polygonModel.append(new QGCQGeoCoordinate(coord, this)); |
|
} |
|
|
|
setDirty(true); |
|
emit pathChanged(); |
|
} |
|
|
|
void QGCMapPolygon::setPath(const QVariantList& path) |
|
{ |
|
_polygonPath = path; |
|
|
|
_polygonModel.clearAndDeleteContents(); |
|
for (int i=0; i<_polygonPath.count(); i++) { |
|
_polygonModel.append(new QGCQGeoCoordinate(_polygonPath[i].value<QGeoCoordinate>(), this)); |
|
} |
|
|
|
setDirty(true); |
|
emit pathChanged(); |
|
} |
|
|
|
void QGCMapPolygon::saveToJson(QJsonObject& json) |
|
{ |
|
QJsonValue jsonValue; |
|
|
|
JsonHelper::saveGeoCoordinateArray(_polygonPath, false /* writeAltitude*/, jsonValue); |
|
json.insert(jsonPolygonKey, jsonValue); |
|
setDirty(false); |
|
} |
|
|
|
bool QGCMapPolygon::loadFromJson(const QJsonObject& json, bool required, QString& errorString) |
|
{ |
|
errorString.clear(); |
|
clear(); |
|
|
|
if (required) { |
|
if (!JsonHelper::validateRequiredKeys(json, QStringList(jsonPolygonKey), errorString)) { |
|
return false; |
|
} |
|
} else if (!json.contains(jsonPolygonKey)) { |
|
return true; |
|
} |
|
|
|
if (!JsonHelper::loadGeoCoordinateArray(json[jsonPolygonKey], false /* altitudeRequired */, _polygonPath, errorString)) { |
|
return false; |
|
} |
|
|
|
for (int i=0; i<_polygonPath.count(); i++) { |
|
_polygonModel.append(new QGCQGeoCoordinate(_polygonPath[i].value<QGeoCoordinate>(), this)); |
|
} |
|
|
|
setDirty(false); |
|
emit pathChanged(); |
|
|
|
return true; |
|
} |
|
|
|
QList<QGeoCoordinate> QGCMapPolygon::coordinateList(void) const |
|
{ |
|
QList<QGeoCoordinate> coords; |
|
|
|
for (int i=0; i<_polygonPath.count(); i++) { |
|
coords.append(_polygonPath[i].value<QGeoCoordinate>()); |
|
} |
|
|
|
return coords; |
|
} |
|
|
|
void QGCMapPolygon::splitPolygonSegment(int vertexIndex) |
|
{ |
|
int nextIndex = vertexIndex + 1; |
|
if (nextIndex > _polygonPath.length() - 1) { |
|
nextIndex = 0; |
|
} |
|
|
|
QGeoCoordinate firstVertex = _polygonPath[vertexIndex].value<QGeoCoordinate>(); |
|
QGeoCoordinate nextVertex = _polygonPath[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 { |
|
_polygonModel.insert(nextIndex, new QGCQGeoCoordinate(newVertex, this)); |
|
_polygonPath.insert(nextIndex, QVariant::fromValue(newVertex)); |
|
emit pathChanged(); |
|
} |
|
} |
|
|
|
void QGCMapPolygon::appendVertex(const QGeoCoordinate& coordinate) |
|
{ |
|
_polygonPath.append(QVariant::fromValue(coordinate)); |
|
_polygonModel.append(new QGCQGeoCoordinate(coordinate, this)); |
|
emit pathChanged(); |
|
} |
|
|
|
void QGCMapPolygon::appendVertices(const QList<QGeoCoordinate>& coordinates) |
|
{ |
|
QList<QObject*> objects; |
|
|
|
foreach (const QGeoCoordinate& coordinate, coordinates) { |
|
objects.append(new QGCQGeoCoordinate(coordinate, this)); |
|
_polygonPath.append(QVariant::fromValue(coordinate)); |
|
} |
|
_polygonModel.append(objects); |
|
emit pathChanged(); |
|
} |
|
|
|
|
|
void QGCMapPolygon::_polygonModelDirtyChanged(bool dirty) |
|
{ |
|
if (dirty) { |
|
setDirty(true); |
|
} |
|
} |
|
|
|
void QGCMapPolygon::removeVertex(int vertexIndex) |
|
{ |
|
if (vertexIndex < 0 && vertexIndex > _polygonPath.length() - 1) { |
|
qWarning() << "Call to removePolygonCoordinate with bad vertexIndex:count" << vertexIndex << _polygonPath.length(); |
|
return; |
|
} |
|
|
|
if (_polygonPath.length() <= 3) { |
|
// Don't allow the user to trash the polygon |
|
return; |
|
} |
|
|
|
QObject* coordObj = _polygonModel.removeAt(vertexIndex); |
|
coordObj->deleteLater(); |
|
|
|
_polygonPath.removeAt(vertexIndex); |
|
emit pathChanged(); |
|
} |
|
|
|
void QGCMapPolygon::_polygonModelCountChanged(int count) |
|
{ |
|
emit countChanged(count); |
|
} |
|
|
|
void QGCMapPolygon::_updateCenter(void) |
|
{ |
|
if (!_ignoreCenterUpdates) { |
|
QGeoCoordinate center; |
|
|
|
if (_polygonPath.count() > 2) { |
|
QPointF centroid(0, 0); |
|
QPolygonF polygonF = _toPolygonF(); |
|
for (int i=0; i<polygonF.count(); i++) { |
|
centroid += polygonF[i]; |
|
} |
|
center = _coordFromPointF(QPointF(centroid.x() / polygonF.count(), centroid.y() / polygonF.count())); |
|
} |
|
if (_center != center) { |
|
_center = center; |
|
emit centerChanged(center); |
|
} |
|
} |
|
} |
|
|
|
void QGCMapPolygon::setCenter(QGeoCoordinate newCenter) |
|
{ |
|
if (newCenter != _center) { |
|
_ignoreCenterUpdates = true; |
|
|
|
// Adjust polygon vertices to new center |
|
double distance = _center.distanceTo(newCenter); |
|
double azimuth = _center.azimuthTo(newCenter); |
|
|
|
for (int i=0; i<count(); i++) { |
|
QGeoCoordinate oldVertex = _polygonPath[i].value<QGeoCoordinate>(); |
|
QGeoCoordinate newVertex = oldVertex.atDistanceAndAzimuth(distance, azimuth); |
|
adjustVertex(i, newVertex); |
|
} |
|
|
|
if (_centerDrag) { |
|
// When center dragging signals are delayed until all vertices are updated |
|
emit pathChanged(); |
|
} |
|
|
|
_ignoreCenterUpdates = false; |
|
|
|
_center = newCenter; |
|
emit centerChanged(newCenter); |
|
} |
|
} |
|
|
|
void QGCMapPolygon::setCenterDrag(bool centerDrag) |
|
{ |
|
if (centerDrag != _centerDrag) { |
|
_centerDrag = centerDrag; |
|
emit centerDragChanged(centerDrag); |
|
} |
|
} |
|
|
|
void QGCMapPolygon::setInteractive(bool interactive) |
|
{ |
|
if (_interactive != interactive) { |
|
_interactive = interactive; |
|
emit interactiveChanged(interactive); |
|
} |
|
} |
|
|
|
QGeoCoordinate QGCMapPolygon::vertexCoordinate(int vertex) const |
|
{ |
|
if (vertex >= 0 && vertex < _polygonPath.count()) { |
|
return _polygonPath[vertex].value<QGeoCoordinate>(); |
|
} else { |
|
qWarning() << "QGCMapPolygon::vertexCoordinate bad vertex requested"; |
|
return QGeoCoordinate(); |
|
} |
|
} |
|
|
|
QList<QPointF> QGCMapPolygon::nedPolygon(void) const |
|
{ |
|
QList<QPointF> nedPolygon; |
|
|
|
if (count() > 0) { |
|
QGeoCoordinate tangentOrigin = vertexCoordinate(0); |
|
|
|
for (int i=0; i<_polygonModel.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); |
|
} |
|
nedPolygon += QPointF(x, y); |
|
} |
|
} |
|
|
|
return nedPolygon; |
|
} |
|
|
|
|
|
void QGCMapPolygon::offset(double distance) |
|
{ |
|
QList<QGeoCoordinate> rgNewPolygon; |
|
|
|
// I'm sure there is some beautiful famous algorithm to do this, but here is a brute force method |
|
|
|
if (count() > 2) { |
|
// Convert the polygon to NED |
|
QList<QPointF> rgNedVertices = nedPolygon(); |
|
|
|
// Walk the edges, offsetting by the specified distance |
|
QList<QLineF> rgOffsetEdges; |
|
for (int i=0; i<rgNedVertices.count(); i++) { |
|
int lastIndex = i == rgNedVertices.count() - 1 ? 0 : i + 1; |
|
QLineF offsetEdge; |
|
QLineF originalEdge(rgNedVertices[i], rgNedVertices[lastIndex]); |
|
|
|
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); |
|
} |
|
|
|
// Intersect the offset edges to generate new vertices |
|
QPointF newVertex; |
|
QGeoCoordinate tangentOrigin = vertexCoordinate(0); |
|
for (int i=0; i<rgOffsetEdges.count(); i++) { |
|
int prevIndex = i == 0 ? rgOffsetEdges.count() - 1 : i - 1; |
|
if (rgOffsetEdges[prevIndex].intersect(rgOffsetEdges[i], &newVertex) == QLineF::NoIntersection) { |
|
// FIXME: Better error handling? |
|
qWarning("Intersection failed"); |
|
return; |
|
} |
|
QGeoCoordinate coord; |
|
convertNedToGeo(newVertex.y(), newVertex.x(), 0, tangentOrigin, &coord); |
|
rgNewPolygon.append(coord); |
|
} |
|
} |
|
|
|
// Update internals |
|
clear(); |
|
for (int i=0; i<rgNewPolygon.count(); i++) { |
|
appendVertex(rgNewPolygon[i]); |
|
} |
|
} |
|
|
|
bool QGCMapPolygon::loadKMLFile(const QString& kmlFile) |
|
{ |
|
QFile file(kmlFile); |
|
|
|
if (!file.exists()) { |
|
qgcApp()->showMessage(tr("File not found: %1").arg(kmlFile)); |
|
return false; |
|
} |
|
|
|
if (!file.open(QIODevice::ReadOnly)) { |
|
qgcApp()->showMessage(tr("Unable to open file: %1 error: $%2").arg(kmlFile).arg(file.errorString())); |
|
return false; |
|
} |
|
|
|
QDomDocument doc; |
|
QString errorMessage; |
|
int errorLine; |
|
if (!doc.setContent(&file, &errorMessage, &errorLine)) { |
|
qgcApp()->showMessage(tr("Unable to parse KML file: %1 error: %2 line: %3").arg(kmlFile).arg(errorMessage).arg(errorLine)); |
|
return false; |
|
} |
|
|
|
QDomNodeList rgNodes = doc.elementsByTagName("Polygon"); |
|
if (rgNodes.count() == 0) { |
|
qgcApp()->showMessage(tr("Unable to find Polygon node in KML")); |
|
return false; |
|
} |
|
|
|
QDomNode coordinatesNode = rgNodes.item(0).namedItem("outerBoundaryIs").namedItem("LinearRing").namedItem("coordinates"); |
|
if (coordinatesNode.isNull()) { |
|
qgcApp()->showMessage(tr("Internal error: Unable to find coordinates node in KML")); |
|
return false; |
|
} |
|
|
|
QString coordinatesString = coordinatesNode.toElement().text().simplified(); |
|
QStringList rgCoordinateStrings = coordinatesString.split(" "); |
|
|
|
QList<QGeoCoordinate> rgCoords; |
|
for (int i=0; i<rgCoordinateStrings.count()-1; i++) { |
|
QString coordinateString = rgCoordinateStrings[i]; |
|
|
|
QStringList rgValueStrings = coordinateString.split(","); |
|
|
|
QGeoCoordinate coord; |
|
coord.setLongitude(rgValueStrings[0].toDouble()); |
|
coord.setLatitude(rgValueStrings[1].toDouble()); |
|
|
|
rgCoords.append(coord); |
|
} |
|
|
|
// Determine winding, reverse if needed |
|
double sum = 0; |
|
for (int i=0; i<rgCoords.count(); i++) { |
|
QGeoCoordinate coord1 = rgCoords[i]; |
|
QGeoCoordinate coord2 = (i == rgCoords.count() - 1) ? rgCoords[0] : rgCoords[i+1]; |
|
|
|
sum += (coord2.longitude() - coord1.longitude()) * (coord2.latitude() + coord1.latitude()); |
|
} |
|
bool reverse = sum < 0.0; |
|
|
|
if (reverse) { |
|
QList<QGeoCoordinate> rgReversed; |
|
|
|
for (int i=0; i<rgCoords.count(); i++) { |
|
rgReversed.prepend(rgCoords[i]); |
|
} |
|
rgCoords = rgReversed; |
|
} |
|
|
|
clear(); |
|
appendVertices(rgCoords); |
|
|
|
return true; |
|
} |
|
|
|
double QGCMapPolygon::area(void) const |
|
{ |
|
// https://www.mathopenref.com/coordpolygonarea2.html |
|
|
|
double coveredArea = 0.0; |
|
QList<QPointF> nedVertices = nedPolygon(); |
|
for (int i=0; i<nedVertices.count(); i++) { |
|
if (i != 0) { |
|
coveredArea += nedVertices[i - 1].x() * nedVertices[i].y() - nedVertices[i].x() * nedVertices[i -1].y(); |
|
} else { |
|
coveredArea += nedVertices.last().x() * nedVertices[i].y() - nedVertices[i].x() * nedVertices.last().y(); |
|
} |
|
} |
|
return 0.5 * fabs(coveredArea); |
|
}
|
|
|