QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
KMLHelper.cc
Go to the documentation of this file.
1#include "KMLHelper.h"
4
5#include <QtCore/QCoreApplication>
6#include <QtCore/QFile>
7#include <QtXml/QDomDocument>
8
9#include <algorithm>
10
11QGC_LOGGING_CATEGORY(KMLHelperLog, "Utilities.KMLHelper")
12
13namespace KMLHelper
14{
15 QDomDocument _loadFile(const QString &kmlFile, QString &errorString);
16 bool _parseCoordinateString(const QString &coordinatesString, QList<QGeoCoordinate> &coords, QString &errorString);
17 void _filterVertices(QList<QGeoCoordinate> &vertices, double filterMeters, int minVertices);
18 void _checkAltitudeMode(const QDomNode &geometryNode, const QString &geometryType, int index);
19
20 constexpr const char *_errorPrefix = QT_TRANSLATE_NOOP("KMLHelper", "KML file load failed. %1");
21}
22
23QDomDocument KMLHelper::_loadFile(const QString &kmlFile, QString &errorString)
24{
25 errorString.clear();
26
27 QFile file(kmlFile);
28 if (!file.exists()) {
29 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "File not found: %1").arg(kmlFile));
30 return QDomDocument();
31 }
32
33 if (!file.open(QIODevice::ReadOnly)) {
34 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "Unable to open file: %1 error: %2").arg(kmlFile, file.errorString()));
35 return QDomDocument();
36 }
37
38 QDomDocument doc;
39 const QDomDocument::ParseResult result = doc.setContent(&file, QDomDocument::ParseOption::Default);
40 if (!result) {
41 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "Unable to parse KML file: %1 error: %2 line: %3").arg(kmlFile).arg(result.errorMessage).arg(result.errorLine));
42 return QDomDocument();
43 }
44
45 return doc;
46}
47
48bool KMLHelper::_parseCoordinateString(const QString &coordinatesString, QList<QGeoCoordinate> &coords, QString &errorString)
49{
50 coords.clear();
51 const QString simplified = coordinatesString.simplified();
52 if (simplified.isEmpty()) {
53 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "Empty coordinates string"));
54 return false;
55 }
56
57 const QStringList rgCoordinateStrings = simplified.split(' ');
58 for (const QString &coordinateString : rgCoordinateStrings) {
59 if (coordinateString.isEmpty()) {
60 continue;
61 }
62 const QStringList rgValueStrings = coordinateString.split(',');
63 if (rgValueStrings.size() < 2) {
64 qCWarning(KMLHelperLog) << "Invalid coordinate format, expected lon,lat[,alt]:" << coordinateString;
65 continue;
66 }
67 bool lonOk = false, latOk = false;
68 const double lon = rgValueStrings[0].toDouble(&lonOk);
69 const double lat = rgValueStrings[1].toDouble(&latOk);
70 if (!lonOk || !latOk) {
71 qCWarning(KMLHelperLog) << "Failed to parse coordinate values:" << coordinateString;
72 continue;
73 }
74 if (lat < -90.0 || lat > 90.0) {
75 qCWarning(KMLHelperLog) << "Latitude out of range [-90, 90]:" << lat << "in:" << coordinateString;
76 continue;
77 }
78 if (lon < -180.0 || lon > 180.0) {
79 qCWarning(KMLHelperLog) << "Longitude out of range [-180, 180]:" << lon << "in:" << coordinateString;
80 continue;
81 }
82 double alt = 0.0;
83 if (rgValueStrings.size() >= 3) {
84 alt = rgValueStrings[2].toDouble();
85 }
86 coords.append(QGeoCoordinate(lat, lon, alt));
87 }
88
89 if (coords.isEmpty()) {
90 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "No valid coordinates found"));
91 return false;
92 }
93 return true;
94}
95
96void KMLHelper::_filterVertices(QList<QGeoCoordinate> &vertices, double filterMeters, int minVertices)
97{
98 if (filterMeters <= 0 || vertices.count() <= minVertices) {
99 return;
100 }
101
102 int i = 0;
103 while (i < (vertices.count() - 1)) {
104 if ((vertices.count() > minVertices) && (vertices[i].distanceTo(vertices[i + 1]) < filterMeters)) {
105 vertices.removeAt(i + 1);
106 } else {
107 i++;
108 }
109 }
110}
111
112void KMLHelper::_checkAltitudeMode(const QDomNode &geometryNode, const QString &geometryType, int index)
113{
114 // Validate altitudeMode using schema-derived rules
115 // QGC treats all coordinates as absolute (AMSL), so warn if a different mode is specified
116 const QDomNode altModeNode = geometryNode.namedItem("altitudeMode");
117 if (!altModeNode.isNull()) {
118 const QString mode = altModeNode.toElement().text();
119 if (mode.isEmpty()) {
120 return;
121 }
122 const auto *validator = KMLSchemaValidator::instance();
123 const QString location = QStringLiteral("(line %1)").arg(altModeNode.lineNumber());
124 if (!validator->isValidEnumValue("altitudeModeEnumType", mode)) {
125 qCWarning(KMLHelperLog) << geometryType << index << location << "has invalid altitudeMode:" << mode
126 << "- valid values are:" << validator->validEnumValues("altitudeModeEnumType").join(", ");
127 } else if (mode != "absolute") {
128 qCWarning(KMLHelperLog) << geometryType << index << location << "uses altitudeMode:" << mode
129 << "- QGC will treat coordinates as absolute (AMSL)";
130 }
131 }
132}
133
135{
136 using ShapeType = ShapeFileHelper::ShapeType;
137
138 const QDomDocument domDocument = KMLHelper::_loadFile(kmlFile, errorString);
139 if (!errorString.isEmpty()) {
140 return ShapeType::Error;
141 }
142
143 const QDomNodeList rgNodesPolygon = domDocument.elementsByTagName("Polygon");
144 if (!rgNodesPolygon.isEmpty()) {
145 return ShapeType::Polygon;
146 }
147
148 const QDomNodeList rgNodesLineString = domDocument.elementsByTagName("LineString");
149 if (!rgNodesLineString.isEmpty()) {
150 return ShapeType::Polyline;
151 }
152
153 const QDomNodeList rgNodesPoint = domDocument.elementsByTagName("Point");
154 if (!rgNodesPoint.isEmpty()) {
155 return ShapeType::Point;
156 }
157
158 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "No supported type found in KML file."));
159 return ShapeType::Error;
160}
161
162int KMLHelper::getEntityCount(const QString &kmlFile, QString &errorString)
163{
164 const QDomDocument domDocument = KMLHelper::_loadFile(kmlFile, errorString);
165 if (!errorString.isEmpty()) {
166 return 0;
167 }
168
169 int count = 0;
170 count += domDocument.elementsByTagName("Polygon").count();
171 count += domDocument.elementsByTagName("LineString").count();
172 count += domDocument.elementsByTagName("Point").count();
173 return count;
174}
175
176bool KMLHelper::loadPolygonsFromFile(const QString &kmlFile, QList<QList<QGeoCoordinate>> &polygons, QString &errorString, double filterMeters)
177{
178 errorString.clear();
179 polygons.clear();
180
181 const QDomDocument domDocument = KMLHelper::_loadFile(kmlFile, errorString);
182 if (!errorString.isEmpty()) {
183 return false;
184 }
185
186 const QDomNodeList rgNodes = domDocument.elementsByTagName("Polygon");
187 if (rgNodes.isEmpty()) {
188 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "Unable to find Polygon node in KML"));
189 return false;
190 }
191
192 for (int nodeIdx = 0; nodeIdx < rgNodes.count(); nodeIdx++) {
193 const QDomNode polygonNode = rgNodes.item(nodeIdx);
194 _checkAltitudeMode(polygonNode, "Polygon", nodeIdx);
195
196 const QDomNode coordinatesNode = polygonNode.namedItem("outerBoundaryIs").namedItem("LinearRing").namedItem("coordinates");
197 if (coordinatesNode.isNull()) {
198 qCWarning(KMLHelperLog) << "Polygon" << nodeIdx << QStringLiteral("(line %1)").arg(polygonNode.lineNumber())
199 << "missing coordinates node, skipping";
200 continue;
201 }
202
203 QList<QGeoCoordinate> rgCoords;
204 if (!_parseCoordinateString(coordinatesNode.toElement().text(), rgCoords, errorString)) {
205 qCWarning(KMLHelperLog) << "Polygon" << nodeIdx << QStringLiteral("(line %1)").arg(coordinatesNode.lineNumber())
206 << "failed to parse coordinates:" << errorString;
207 errorString.clear();
208 continue;
209 }
210
211 if (rgCoords.count() < 3) {
212 qCWarning(KMLHelperLog) << "Polygon" << nodeIdx << QStringLiteral("(line %1)").arg(polygonNode.lineNumber())
213 << "has fewer than 3 vertices, skipping";
214 continue;
215 }
216
217 // Remove duplicate closing vertex (KML polygons repeat first vertex at end)
218 if (rgCoords.count() > 3 && rgCoords.first().latitude() == rgCoords.last().latitude() &&
219 rgCoords.first().longitude() == rgCoords.last().longitude()) {
220 rgCoords.removeLast();
221 }
222
223 // Determine winding, reverse if needed. QGC wants clockwise winding
224 double sum = 0;
225 for (int i = 0; i < rgCoords.count(); i++) {
226 const QGeoCoordinate &coord1 = rgCoords[i];
227 const QGeoCoordinate &coord2 = rgCoords[(i + 1) % rgCoords.count()];
228 sum += (coord2.longitude() - coord1.longitude()) * (coord2.latitude() + coord1.latitude());
229 }
230 if (sum < 0.0) {
231 std::reverse(rgCoords.begin(), rgCoords.end());
232 }
233
234 _filterVertices(rgCoords, filterMeters, 3);
235 polygons.append(rgCoords);
236 }
237
238 if (polygons.isEmpty()) {
239 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "No valid polygons found in KML file"));
240 return false;
241 }
242
243 return true;
244}
245
246bool KMLHelper::loadPolylinesFromFile(const QString &kmlFile, QList<QList<QGeoCoordinate>> &polylines, QString &errorString, double filterMeters)
247{
248 errorString.clear();
249 polylines.clear();
250
251 const QDomDocument domDocument = KMLHelper::_loadFile(kmlFile, errorString);
252 if (!errorString.isEmpty()) {
253 return false;
254 }
255
256 const QDomNodeList rgNodes = domDocument.elementsByTagName("LineString");
257 if (rgNodes.isEmpty()) {
258 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "Unable to find LineString node in KML"));
259 return false;
260 }
261
262 for (int nodeIdx = 0; nodeIdx < rgNodes.count(); nodeIdx++) {
263 const QDomNode lineStringNode = rgNodes.item(nodeIdx);
264 _checkAltitudeMode(lineStringNode, "LineString", nodeIdx);
265
266 const QDomNode coordinatesNode = lineStringNode.namedItem("coordinates");
267 if (coordinatesNode.isNull()) {
268 qCWarning(KMLHelperLog) << "LineString" << nodeIdx << QStringLiteral("(line %1)").arg(lineStringNode.lineNumber())
269 << "missing coordinates node, skipping";
270 continue;
271 }
272
273 QList<QGeoCoordinate> rgCoords;
274 if (!_parseCoordinateString(coordinatesNode.toElement().text(), rgCoords, errorString)) {
275 qCWarning(KMLHelperLog) << "LineString" << nodeIdx << QStringLiteral("(line %1)").arg(coordinatesNode.lineNumber())
276 << "failed to parse coordinates:" << errorString;
277 errorString.clear();
278 continue;
279 }
280
281 if (rgCoords.count() < 2) {
282 qCWarning(KMLHelperLog) << "LineString" << nodeIdx << QStringLiteral("(line %1)").arg(lineStringNode.lineNumber())
283 << "has fewer than 2 vertices, skipping";
284 continue;
285 }
286
287 _filterVertices(rgCoords, filterMeters, 2);
288 polylines.append(rgCoords);
289 }
290
291 if (polylines.isEmpty()) {
292 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "No valid polylines found in KML file"));
293 return false;
294 }
295
296 return true;
297}
298
299bool KMLHelper::loadPointsFromFile(const QString &kmlFile, QList<QGeoCoordinate> &points, QString &errorString)
300{
301 errorString.clear();
302 points.clear();
303
304 const QDomDocument domDocument = KMLHelper::_loadFile(kmlFile, errorString);
305 if (!errorString.isEmpty()) {
306 return false;
307 }
308
309 const QDomNodeList rgNodes = domDocument.elementsByTagName("Point");
310 if (rgNodes.isEmpty()) {
311 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "Unable to find Point node in KML"));
312 return false;
313 }
314
315 for (int nodeIdx = 0; nodeIdx < rgNodes.count(); nodeIdx++) {
316 const QDomNode pointNode = rgNodes.item(nodeIdx);
317 _checkAltitudeMode(pointNode, "Point", nodeIdx);
318
319 const QDomNode coordinatesNode = pointNode.namedItem("coordinates");
320 if (coordinatesNode.isNull()) {
321 qCWarning(KMLHelperLog) << "Point" << nodeIdx << QStringLiteral("(line %1)").arg(pointNode.lineNumber())
322 << "missing coordinates node, skipping";
323 continue;
324 }
325
326 QList<QGeoCoordinate> coords;
327 if (!_parseCoordinateString(coordinatesNode.toElement().text(), coords, errorString)) {
328 qCWarning(KMLHelperLog) << "Point" << nodeIdx << QStringLiteral("(line %1)").arg(coordinatesNode.lineNumber())
329 << "failed to parse coordinates:" << errorString;
330 errorString.clear();
331 continue;
332 }
333
334 if (!coords.isEmpty()) {
335 points.append(coords.first());
336 }
337 }
338
339 if (points.isEmpty()) {
340 errorString = QCoreApplication::translate("KMLHelper", _errorPrefix).arg(QCoreApplication::translate("KML", "No valid points found in KML file"));
341 return false;
342 }
343
344 return true;
345}
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static KMLSchemaValidator * instance()
ShapeFileHelper::ShapeType determineShapeType(const QString &file, QString &errorString)
Definition KMLHelper.cc:134
bool loadPolygonsFromFile(const QString &kmlFile, QList< QList< QGeoCoordinate > > &polygons, QString &errorString, double filterMeters=ShapeFileHelper::kDefaultVertexFilterMeters)
Definition KMLHelper.cc:176
bool loadPointsFromFile(const QString &kmlFile, QList< QGeoCoordinate > &points, QString &errorString)
Load all point entities.
Definition KMLHelper.cc:299
bool _parseCoordinateString(const QString &coordinatesString, QList< QGeoCoordinate > &coords, QString &errorString)
Definition KMLHelper.cc:48
QDomDocument _loadFile(const QString &kmlFile, QString &errorString)
Definition KMLHelper.cc:23
void _filterVertices(QList< QGeoCoordinate > &vertices, double filterMeters, int minVertices)
Definition KMLHelper.cc:96
bool loadPolylinesFromFile(const QString &kmlFile, QList< QList< QGeoCoordinate > > &polylines, QString &errorString, double filterMeters=ShapeFileHelper::kDefaultVertexFilterMeters)
Definition KMLHelper.cc:246
void _checkAltitudeMode(const QDomNode &geometryNode, const QString &geometryType, int index)
Definition KMLHelper.cc:112
int getEntityCount(const QString &kmlFile, QString &errorString)
Get the number of geometry entities in the KML file.
Definition KMLHelper.cc:162