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