QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
SurveyComplexItem.cc
Go to the documentation of this file.
1#include "SurveyComplexItem.h"
2#include "JsonParsing.h"
3#include "QGCGeo.h"
4#include "QGCQGeoCoordinate.h"
5#include "SettingsManager.h"
6#include "AppSettings.h"
8#include "MissionItem.h"
9#include "AppMessages.h"
10#include "QGCApplication.h"
11#include "Vehicle.h"
12#include "QGCLoggingCategory.h"
13
14#include <QtGui/QPolygonF>
15#include <QtCore/QJsonArray>
16#include <QtCore/QLineF>
17
18QGC_LOGGING_CATEGORY(SurveyComplexItemLog, "Plan.SurveyComplexItem")
19
20SurveyComplexItem::SurveyComplexItem(PlanMasterController* masterController, bool flyView, const QString& kmlOrShpFile)
21 : TransectStyleComplexItem (masterController, flyView, settingsGroup)
22 , _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/Survey.SettingsGroup.json"), this))
23 , _gridAngleFact (settingsGroup, _metaDataMap[gridAngleName])
24 , _flyAlternateTransectsFact(settingsGroup, _metaDataMap[flyAlternateTransectsName])
25 , _splitConcavePolygonsFact (settingsGroup, _metaDataMap[splitConcavePolygonsName])
26 , _entryPoint (EntryLocationTopLeft)
27{
28 _editorQml = "qrc:/qml/QGroundControl/PlanView/SurveyItemEditor.qml";
29
30 if (_controllerVehicle && !(_controllerVehicle->fixedWing() || _controllerVehicle->vtol())) {
31 // Only fixed wing flight paths support alternate transects
32 _flyAlternateTransectsFact.setRawValue(false);
33 }
34
35 // We override the altitude to the mission default
36 if (_cameraCalc.isManualCamera() || !_cameraCalc.valueSetIsDistance()->rawValue().toBool()) {
37 _cameraCalc.distanceToSurface()->setRawValue(SettingsManager::instance()->appSettings()->defaultMissionItemAltitude()->rawValue());
38 }
39
40 connect(&_gridAngleFact, &Fact::valueChanged, this, &SurveyComplexItem::_setDirty);
41 connect(&_flyAlternateTransectsFact,&Fact::valueChanged, this, &SurveyComplexItem::_setDirty);
42 connect(&_splitConcavePolygonsFact, &Fact::valueChanged, this, &SurveyComplexItem::_setDirty);
44
45 connect(&_gridAngleFact, &Fact::valueChanged, this, &SurveyComplexItem::_rebuildTransects);
46 connect(&_flyAlternateTransectsFact,&Fact::valueChanged, this, &SurveyComplexItem::_rebuildTransects);
47 connect(&_splitConcavePolygonsFact, &Fact::valueChanged, this, &SurveyComplexItem::_rebuildTransects);
49
50 connect(&_surveyAreaPolygon, &QGCMapPolygon::isValidChanged, this, &SurveyComplexItem::_updateWizardMode);
51 connect(&_surveyAreaPolygon, &QGCMapPolygon::traceModeChanged, this, &SurveyComplexItem::_updateWizardMode);
52
53 if (!kmlOrShpFile.isEmpty()) {
54 _surveyAreaPolygon.loadKMLOrSHPFile(kmlOrShpFile);
55 _surveyAreaPolygon.setDirty(false);
56 }
57 setDirty(false);
58}
59
60void SurveyComplexItem::save(QJsonArray& planItems)
61{
62 QJsonObject saveObject;
63
64 _saveCommon(saveObject);
65 planItems.append(saveObject);
66}
67
68void SurveyComplexItem::savePreset(const QString& presetName)
69{
70 QJsonObject saveObject;
71
72 _saveCommon(saveObject);
73 _savePresetJson(presetName, saveObject);
74}
75
76void SurveyComplexItem::_saveCommon(QJsonObject& saveObject)
77{
79
80 saveObject[JsonParsing::jsonVersionKey] = 5;
83 saveObject[_jsonGridAngleKey] = _gridAngleFact.rawValue().toDouble();
84 saveObject[_jsonFlyAlternateTransectsKey] = _flyAlternateTransectsFact.rawValue().toBool();
85 saveObject[_jsonSplitConcavePolygonsKey] = _splitConcavePolygonsFact.rawValue().toBool();
86 saveObject[_jsonEntryPointKey] = _entryPoint;
87
88 // Polygon shape
90}
91
92void SurveyComplexItem::loadPreset(const QString& presetName)
93{
94 QString errorString;
95
96 QJsonObject presetObject = _loadPresetJson(presetName);
97 if (!_loadV4V5(presetObject, 0, errorString, 5, true /* forPresets */)) {
98 QGC::showAppMessage(QStringLiteral("Internal Error: Preset load failed. Name: %1 Error: %2").arg(presetName).arg(errorString));
99 }
101}
102
104{
105 Q_UNUSED(prevAltitude);
106 _cameraCalc.setDistanceMode(prevAltFrame);
107}
108
109bool SurveyComplexItem::load(const QJsonObject& complexObject, int sequenceNumber, QString& errorString)
110{
111 // We need to pull version first to determine what validation/conversion needs to be performed
112 QList<JsonParsing::KeyValidateInfo> versionKeyInfoList = {
113 { JsonParsing::jsonVersionKey, QJsonValue::Double, true },
114 };
115 if (!JsonParsing::validateKeys(complexObject, versionKeyInfoList, errorString)) {
116 return false;
117 }
118
119 int version = complexObject[JsonParsing::jsonVersionKey].toInt();
120 if (version < 2 || version > 5) {
121 errorString = tr("Survey items do not support version %1").arg(version);
122 return false;
123 }
124
125 if (version == 4 || version == 5) {
126 if (!_loadV4V5(complexObject, sequenceNumber, errorString, version, false /* forPresets */)) {
127 return false;
128 }
129
131 if (_cameraShots == 0) {
132 // Shot count was possibly not available from plan file
133 _recalcCameraShots();
134 }
135 } else {
136 // Must be v2 or v3
137 QJsonObject v3ComplexObject = complexObject;
138 if (version == 2) {
139 // Convert to v3
140 if (v3ComplexObject.contains(VisualMissionItem::jsonTypeKey) && v3ComplexObject[VisualMissionItem::jsonTypeKey].toString() == QStringLiteral("survey")) {
143 }
144 }
145 if (!_loadV3(complexObject, sequenceNumber, errorString)) {
146 return false;
147 }
148
149 // V2/3 doesn't include individual items so we need to rebuild manually
151 }
152
153 return true;
154}
155
156bool SurveyComplexItem::_loadV4V5(const QJsonObject& complexObject, int sequenceNumber, QString& errorString, int version, bool forPresets)
157{
158 QList<JsonParsing::KeyValidateInfo> keyInfoList = {
159 { VisualMissionItem::jsonTypeKey, QJsonValue::String, true },
160 { ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true },
161 { _jsonEntryPointKey, QJsonValue::Double, true },
162 { _jsonGridAngleKey, QJsonValue::Double, true },
163 { _jsonFlyAlternateTransectsKey, QJsonValue::Bool, false },
164 };
165
166 if(version == 5) {
167 JsonParsing::KeyValidateInfo jSplitPolygon = { _jsonSplitConcavePolygonsKey, QJsonValue::Bool, true };
168 keyInfoList.append(jSplitPolygon);
169 }
170
171 if (!JsonParsing::validateKeys(complexObject, keyInfoList, errorString)) {
172 return false;
173 }
174
175 QString itemType = complexObject[VisualMissionItem::jsonTypeKey].toString();
176 QString complexType = complexObject[ComplexMissionItem::jsonComplexItemTypeKey].toString();
178 errorString = tr("%1 does not support loading this complex mission item type: %2:%3").arg(qgcApp()->applicationName()).arg(itemType).arg(complexType);
179 return false;
180 }
181
182 _ignoreRecalc = !forPresets;
183
184 if (!forPresets) {
186
187 if (!_surveyAreaPolygon.loadFromJson(complexObject, true /* required */, errorString)) {
189 return false;
190 }
191 }
192
193 if (!TransectStyleComplexItem::_load(complexObject, forPresets, errorString)) {
194 _ignoreRecalc = false;
195 return false;
196 }
197
198 _gridAngleFact.setRawValue (complexObject[_jsonGridAngleKey].toDouble());
199 _flyAlternateTransectsFact.setRawValue (complexObject[_jsonFlyAlternateTransectsKey].toBool(false));
200
201 if (version == 5) {
202 _splitConcavePolygonsFact.setRawValue (complexObject[_jsonSplitConcavePolygonsKey].toBool(true));
203 }
204
205 _entryPoint = complexObject[_jsonEntryPointKey].toInt();
206
207 _ignoreRecalc = false;
208
209 return true;
210}
211
212bool SurveyComplexItem::_loadV3(const QJsonObject& complexObject, int sequenceNumber, QString& errorString)
213{
214 QList<JsonParsing::KeyValidateInfo> mainKeyInfoList = {
215 { VisualMissionItem::jsonTypeKey, QJsonValue::String, true },
216 { ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true },
217 { QGCMapPolygon::jsonPolygonKey, QJsonValue::Array, true },
218 { _jsonV3GridObjectKey, QJsonValue::Object, true },
219 { _jsonV3CameraObjectKey, QJsonValue::Object, false },
220 { _jsonV3CameraTriggerDistanceKey, QJsonValue::Double, true },
221 { _jsonV3ManualGridKey, QJsonValue::Bool, true },
222 { _jsonV3FixedValueIsAltitudeKey, QJsonValue::Bool, true },
223 { _jsonV3HoverAndCaptureKey, QJsonValue::Bool, false },
224 { _jsonV3Refly90DegreesKey, QJsonValue::Bool, false },
225 { _jsonV3CameraTriggerInTurnaroundKey, QJsonValue::Bool, false }, // Should really be required, but it was missing from initial code due to bug
226 };
227 if (!JsonParsing::validateKeys(complexObject, mainKeyInfoList, errorString)) {
228 return false;
229 }
230
231 QString itemType = complexObject[VisualMissionItem::jsonTypeKey].toString();
232 QString complexType = complexObject[ComplexMissionItem::jsonComplexItemTypeKey].toString();
234 errorString = tr("%1 does not support loading this complex mission item type: %2:%3").arg(qgcApp()->applicationName()).arg(itemType).arg(complexType);
235 return false;
236 }
237
238 _ignoreRecalc = true;
239
241
242 _hoverAndCaptureFact.setRawValue (complexObject[_jsonV3HoverAndCaptureKey].toBool(false));
243 _refly90DegreesFact.setRawValue (complexObject[_jsonV3Refly90DegreesKey].toBool(false));
244 _cameraTriggerInTurnAroundFact.setRawValue (complexObject[_jsonV3CameraTriggerInTurnaroundKey].toBool(true));
245
246 _cameraCalc.valueSetIsDistance()->setRawValue (complexObject[_jsonV3FixedValueIsAltitudeKey].toBool(true));
247 _cameraCalc.setDistanceMode(complexObject[_jsonV3GridAltitudeRelativeKey].toBool(true) ? QGroundControlQmlGlobal::AltitudeFrameRelative : QGroundControlQmlGlobal::AltitudeFrameAbsolute);
248
249 bool manualGrid = complexObject[_jsonV3ManualGridKey].toBool(true);
250
251 QList<JsonParsing::KeyValidateInfo> gridKeyInfoList = {
252 { _jsonV3GridAltitudeKey, QJsonValue::Double, true },
253 { _jsonV3GridAltitudeRelativeKey, QJsonValue::Bool, true },
254 { _jsonV3GridAngleKey, QJsonValue::Double, true },
255 { _jsonV3GridSpacingKey, QJsonValue::Double, true },
256 { _jsonEntryPointKey, QJsonValue::Double, false },
257 { _jsonV3TurnaroundDistKey, QJsonValue::Double, true },
258 };
259 QJsonObject gridObject = complexObject[_jsonV3GridObjectKey].toObject();
260 if (!JsonParsing::validateKeys(gridObject, gridKeyInfoList, errorString)) {
261 _ignoreRecalc = false;
262 return false;
263 }
264
265 _gridAngleFact.setRawValue (gridObject[_jsonV3GridAngleKey].toDouble());
266 _turnAroundDistanceFact.setRawValue (gridObject[_jsonV3TurnaroundDistKey].toDouble());
267
268 if (gridObject.contains(_jsonEntryPointKey)) {
269 _entryPoint = gridObject[_jsonEntryPointKey].toInt();
270 } else {
271 _entryPoint = EntryLocationTopRight;
272 }
273
274 _cameraCalc.distanceToSurface()->setRawValue (gridObject[_jsonV3GridAltitudeKey].toDouble());
275 _cameraCalc.adjustedFootprintSide()->setRawValue (gridObject[_jsonV3GridSpacingKey].toDouble());
276 _cameraCalc.adjustedFootprintFrontal()->setRawValue (complexObject[_jsonV3CameraTriggerDistanceKey].toDouble());
277
278 if (manualGrid) {
280 } else {
281 if (!complexObject.contains(_jsonV3CameraObjectKey)) {
282 errorString = tr("%1 but %2 object is missing").arg("manualGrid = false").arg("camera");
283 _ignoreRecalc = false;
284 return false;
285 }
286
287 QJsonObject cameraObject = complexObject[_jsonV3CameraObjectKey].toObject();
288
289 // Older code had typo on "imageSideOverlap" incorrectly being "imageSizeOverlap"
290 QString incorrectImageSideOverlap = "imageSizeOverlap";
291 if (cameraObject.contains(incorrectImageSideOverlap)) {
292 cameraObject[_jsonV3SideOverlapKey] = cameraObject[incorrectImageSideOverlap];
293 cameraObject.remove(incorrectImageSideOverlap);
294 }
295
296 QList<JsonParsing::KeyValidateInfo> cameraKeyInfoList = {
297 { _jsonV3GroundResolutionKey, QJsonValue::Double, true },
298 { _jsonV3FrontalOverlapKey, QJsonValue::Double, true },
299 { _jsonV3SideOverlapKey, QJsonValue::Double, true },
300 { _jsonV3CameraSensorWidthKey, QJsonValue::Double, true },
301 { _jsonV3CameraSensorHeightKey, QJsonValue::Double, true },
302 { _jsonV3CameraResolutionWidthKey, QJsonValue::Double, true },
303 { _jsonV3CameraResolutionHeightKey, QJsonValue::Double, true },
304 { _jsonV3CameraFocalLengthKey, QJsonValue::Double, true },
305 { _jsonV3CameraNameKey, QJsonValue::String, true },
306 { _jsonV3CameraOrientationLandscapeKey, QJsonValue::Bool, true },
307 { _jsonV3CameraMinTriggerIntervalKey, QJsonValue::Double, false },
308 };
309 if (!JsonParsing::validateKeys(cameraObject, cameraKeyInfoList, errorString)) {
310 _ignoreRecalc = false;
311 return false;
312 }
313
314 _cameraCalc.landscape()->setRawValue (cameraObject[_jsonV3CameraOrientationLandscapeKey].toBool(true));
315 _cameraCalc.frontalOverlap()->setRawValue (cameraObject[_jsonV3FrontalOverlapKey].toInt());
316 _cameraCalc.sideOverlap()->setRawValue (cameraObject[_jsonV3SideOverlapKey].toInt());
317 _cameraCalc.sensorWidth()->setRawValue (cameraObject[_jsonV3CameraSensorWidthKey].toDouble());
318 _cameraCalc.sensorHeight()->setRawValue (cameraObject[_jsonV3CameraSensorHeightKey].toDouble());
319 _cameraCalc.focalLength()->setRawValue (cameraObject[_jsonV3CameraFocalLengthKey].toDouble());
320 _cameraCalc.imageWidth()->setRawValue (cameraObject[_jsonV3CameraResolutionWidthKey].toInt());
321 _cameraCalc.imageHeight()->setRawValue (cameraObject[_jsonV3CameraResolutionHeightKey].toInt());
322 _cameraCalc.minTriggerInterval()->setRawValue (cameraObject[_jsonV3CameraMinTriggerIntervalKey].toDouble(0));
323 _cameraCalc.imageDensity()->setRawValue (cameraObject[_jsonV3GroundResolutionKey].toDouble());
325 _cameraCalc._setCameraNameFromV3TransectLoad (cameraObject[_jsonV3CameraNameKey].toString());
326 }
327
328 // Polygon shape
334 if (!_surveyAreaPolygon.loadFromJson(complexObject, true /* required */, errorString)) {
336 _ignoreRecalc = false;
337 return false;
338 }
339
340 _ignoreRecalc = false;
341
342 return true;
343}
344
346void SurveyComplexItem::_reverseTransectOrder(QList<QList<QGeoCoordinate>>& transects)
347{
348 QList<QList<QGeoCoordinate>> rgReversedTransects;
349 for (int i=transects.count() - 1; i>=0; i--) {
350 rgReversedTransects.append(transects[i]);
351 }
352 transects = rgReversedTransects;
353}
354
356void SurveyComplexItem::_reverseInternalTransectPoints(QList<QList<QGeoCoordinate>>& transects)
357{
358 for (int i=0; i<transects.count(); i++) {
359 QList<QGeoCoordinate> rgReversedCoords;
360 QList<QGeoCoordinate>& rgOriginalCoords = transects[i];
361 for (int j=rgOriginalCoords.count()-1; j>=0; j--) {
362 rgReversedCoords.append(rgOriginalCoords[j]);
363 }
364 transects[i] = rgReversedCoords;
365 }
366}
367
372void SurveyComplexItem::_optimizeTransectsForShortestDistance(const QGeoCoordinate& distanceCoord, QList<QList<QGeoCoordinate>>& transects)
373{
374 double rgTransectDistance[4];
375 rgTransectDistance[0] = transects.first().first().distanceTo(distanceCoord);
376 rgTransectDistance[1] = transects.first().last().distanceTo(distanceCoord);
377 rgTransectDistance[2] = transects.last().first().distanceTo(distanceCoord);
378 rgTransectDistance[3] = transects.last().last().distanceTo(distanceCoord);
379
380 int shortestIndex = 0;
381 double shortestDistance = rgTransectDistance[0];
382 for (int i=1; i<3; i++) {
383 if (rgTransectDistance[i] < shortestDistance) {
384 shortestIndex = i;
385 shortestDistance = rgTransectDistance[i];
386 }
387 }
388
389 if (shortestIndex > 1) {
390 // We need to reverse the order of segments
391 _reverseTransectOrder(transects);
392 }
393 if (shortestIndex & 1) {
394 // We need to reverse the points within each segment
395 _reverseInternalTransectPoints(transects);
396 }
397}
398
399qreal SurveyComplexItem::_ccw(QPointF pt1, QPointF pt2, QPointF pt3)
400{
401 return (pt2.x()-pt1.x())*(pt3.y()-pt1.y()) - (pt2.y()-pt1.y())*(pt3.x()-pt1.x());
402}
403
404qreal SurveyComplexItem::_dp(QPointF pt1, QPointF pt2)
405{
406 return (pt2.x()-pt1.x())/qSqrt((pt2.x()-pt1.x())*(pt2.x()-pt1.x()) + (pt2.y()-pt1.y())*(pt2.y()-pt1.y()));
407}
408
409void SurveyComplexItem::_swapPoints(QList<QPointF>& points, int index1, int index2)
410{
411 QPointF temp = points[index1];
412 points[index1] = points[index2];
413 points[index2] = temp;
414}
415
417bool SurveyComplexItem::_gridAngleIsNorthSouthTransects()
418{
419 // Grid angle ranges from -360<->360
420 double gridAngle = qAbs(_gridAngleFact.rawValue().toDouble());
421 return gridAngle < 45.0 || (gridAngle > 360.0 - 45.0) || (gridAngle > 90.0 + 45.0 && gridAngle < 270.0 - 45.0);
422}
423
424void SurveyComplexItem::_adjustTransectsToEntryPointLocation(QList<QList<QGeoCoordinate>>& transects)
425{
426 if (transects.count() == 0) {
427 return;
428 }
429
430 bool reversePoints = false;
431 bool reverseTransects = false;
432
433 if (_entryPoint == EntryLocationBottomLeft || _entryPoint == EntryLocationBottomRight) {
434 reversePoints = true;
435 }
436 if (_entryPoint == EntryLocationTopRight || _entryPoint == EntryLocationBottomRight) {
437 reverseTransects = true;
438 }
439
440 if (reversePoints) {
441 qCDebug(SurveyComplexItemLog) << "_adjustTransectsToEntryPointLocation Reverse Points";
442 _reverseInternalTransectPoints(transects);
443 }
444 if (reverseTransects) {
445 qCDebug(SurveyComplexItemLog) << "_adjustTransectsToEntryPointLocation Reverse Transects";
446 _reverseTransectOrder(transects);
447 }
448
449 qCDebug(SurveyComplexItemLog) << "_adjustTransectsToEntryPointLocation Modified entry point:entryLocation" << transects.first().first() << _entryPoint;
450}
451
452QPointF SurveyComplexItem::_rotatePoint(const QPointF& point, const QPointF& origin, double angle)
453{
454 QPointF rotated;
455 double radians = (M_PI / 180.0) * -angle;
456
457 rotated.setX(((point.x() - origin.x()) * cos(radians)) - ((point.y() - origin.y()) * sin(radians)) + origin.x());
458 rotated.setY(((point.x() - origin.x()) * sin(radians)) + ((point.y() - origin.y()) * cos(radians)) + origin.y());
459
460 return rotated;
461}
462
463void SurveyComplexItem::_intersectLinesWithRect(const QList<QLineF>& lineList, const QRectF& boundRect, QList<QLineF>& resultLines)
464{
465 QLineF topLine (boundRect.topLeft(), boundRect.topRight());
466 QLineF bottomLine (boundRect.bottomLeft(), boundRect.bottomRight());
467 QLineF leftLine (boundRect.topLeft(), boundRect.bottomLeft());
468 QLineF rightLine (boundRect.topRight(), boundRect.bottomRight());
469
470 for (int i=0; i<lineList.count(); i++) {
471 QPointF intersectPoint;
472 QLineF intersectLine;
473 const QLineF& line = lineList[i];
474
475 auto isLineBoundedIntersect = [&line, &intersectPoint](const QLineF& linePosition) {
476 return line.intersects(linePosition, &intersectPoint) == QLineF::BoundedIntersection;
477 };
478
479 int foundCount = 0;
480 if (isLineBoundedIntersect(topLine)) {
481 intersectLine.setP1(intersectPoint);
482 foundCount++;
483 }
484 if (isLineBoundedIntersect(rightLine)) {
485 if (foundCount == 0) {
486 intersectLine.setP1(intersectPoint);
487 } else {
488 if (foundCount != 1) {
489 qWarning() << "Found more than two intersecting points";
490 }
491 intersectLine.setP2(intersectPoint);
492 }
493 foundCount++;
494 }
495 if (isLineBoundedIntersect(bottomLine)) {
496 if (foundCount == 0) {
497 intersectLine.setP1(intersectPoint);
498 } else {
499 if (foundCount != 1) {
500 qWarning() << "Found more than two intersecting points";
501 }
502 intersectLine.setP2(intersectPoint);
503 }
504 foundCount++;
505 }
506 if (isLineBoundedIntersect(leftLine)) {
507 if (foundCount == 0) {
508 intersectLine.setP1(intersectPoint);
509 } else {
510 if (foundCount != 1) {
511 qWarning() << "Found more than two intersecting points";
512 }
513 intersectLine.setP2(intersectPoint);
514 }
515 foundCount++;
516 }
517
518 if (foundCount == 2) {
519 resultLines += intersectLine;
520 }
521 }
522}
523
524void SurveyComplexItem::_intersectLinesWithPolygon(const QList<QLineF>& lineList, const QPolygonF& polygon, QList<QLineF>& resultLines)
525{
526 resultLines.clear();
527
528 for (int i=0; i<lineList.count(); i++) {
529 const QLineF& line = lineList[i];
530 QList<QPointF> intersections;
531
532 // Intersect the line with all the polygon edges
533 for (int j=0; j<polygon.count()-1; j++) {
534 QPointF intersectPoint;
535 QLineF polygonLine = QLineF(polygon[j], polygon[j+1]);
536
537 auto intersect = line.intersects(polygonLine, &intersectPoint);
538 if (intersect == QLineF::BoundedIntersection) {
539 if (!intersections.contains(intersectPoint)) {
540 intersections.append(intersectPoint);
541 }
542 }
543 }
544
545 // We now have one or more intersection points all along the same line. Find the two
546 // which are furthest away from each other to form the transect.
547 if (intersections.count() > 1) {
548 QPointF firstPoint;
549 QPointF secondPoint;
550 double currentMaxDistance = 0;
551
552 for (int intersectionIndex=0; intersectionIndex<intersections.count(); intersectionIndex++) {
553 for (int compareIndex=0; compareIndex<intersections.count(); compareIndex++) {
554 QLineF lineTest(intersections[intersectionIndex], intersections[compareIndex]);
555 double newMaxDistance = lineTest.length();
556 if (newMaxDistance > currentMaxDistance) {
557 firstPoint = intersections[intersectionIndex];
558 secondPoint = intersections[compareIndex];
559 currentMaxDistance = newMaxDistance;
560 }
561 }
562 }
563
564 resultLines += QLineF(firstPoint, secondPoint);
565 }
566 }
567}
568
570void SurveyComplexItem::_adjustLineDirection(const QList<QLineF>& lineList, QList<QLineF>& resultLines)
571{
572 qreal firstAngle = 0;
573 for (int i=0; i<lineList.count(); i++) {
574 const QLineF& line = lineList[i];
575 QLineF adjustedLine;
576
577 if (i == 0) {
578 firstAngle = line.angle();
579 }
580
581 if (qAbs(line.angle() - firstAngle) > 1.0) {
582 adjustedLine.setP1(line.p2());
583 adjustedLine.setP2(line.p1());
584 } else {
585 adjustedLine = line;
586 }
587
588 resultLines += adjustedLine;
589 }
590}
591
592double SurveyComplexItem::_clampGridAngle90(double gridAngle)
593{
594 // Clamp grid angle to -90<->90. This prevents transects from being rotated to a reversed order.
595 if (gridAngle > 90.0) {
596 gridAngle -= 180.0;
597 } else if (gridAngle < -90.0) {
598 gridAngle += 180;
599 }
600 return gridAngle;
601}
602
603bool SurveyComplexItem::_nextTransectCoord(const QList<QGeoCoordinate>& transectPoints, int pointIndex, QGeoCoordinate& coord)
604{
605 if (pointIndex > transectPoints.count()) {
606 qWarning() << "Bad grid generation";
607 return false;
608 }
609
610 coord = transectPoints[pointIndex];
611 return true;
612}
613
614bool SurveyComplexItem::_hasTurnaround(void) const
615{
616 return _turnAroundDistance() > 0;
617}
618
619double SurveyComplexItem::_turnaroundDistance(void) const
620{
621 return _turnAroundDistanceFact.rawValue().toDouble();
622}
623
624void SurveyComplexItem::_rebuildTransectsPhase1(void)
625{
626 _rebuildTransectsPhase1WorkerSinglePolygon(false /* refly */);
627 if (_refly90DegreesFact.rawValue().toBool()) {
628 _rebuildTransectsPhase1WorkerSinglePolygon(true /* refly */);
629 }
630}
631
632void SurveyComplexItem::_rebuildTransectsPhase1WorkerSinglePolygon(bool refly)
633{
634 if (_ignoreRecalc) {
635 return;
636 }
637
638 // If the transects are getting rebuilt then any previously loaded mission items are now invalid
640 _loadedMissionItems.clear();
641 _loadedMissionItemsParent->deleteLater();
643 }
644
645 if (_surveyAreaPolygon.count() < 3) {
646 return;
647 }
648
649 // Convert polygon to NED
650
651 QList<QPointF> polygonPoints;
652 QGeoCoordinate tangentOrigin = _surveyAreaPolygon.pathModel().value<QGCQGeoCoordinate*>(0)->coordinate();
653 qCDebug(SurveyComplexItemLog) << "_rebuildTransectsPhase1 Convert polygon to NED - _surveyAreaPolygon.count():tangentOrigin" << _surveyAreaPolygon.count() << tangentOrigin;
654 for (int i=0; i<_surveyAreaPolygon.count(); i++) {
655 double y, x, down;
656 QGeoCoordinate vertex = _surveyAreaPolygon.pathModel().value<QGCQGeoCoordinate*>(i)->coordinate();
657 if (i == 0) {
658 // This avoids a nan calculation that comes out of convertGeoToNed
659 x = y = 0;
660 } else {
661 QGCGeo::convertGeoToNed(vertex, tangentOrigin, y, x, down);
662 }
663 polygonPoints += QPointF(x, y);
664 qCDebug(SurveyComplexItemLog) << "_rebuildTransectsPhase1 vertex:x:y" << vertex << polygonPoints.last().x() << polygonPoints.last().y();
665 }
666
667 // Generate transects
668
669 double gridAngle = _gridAngleFact.rawValue().toDouble();
670 double gridSpacing = _cameraCalc.adjustedFootprintSide()->rawValue().toDouble();
671
672 gridAngle = _clampGridAngle90(gridAngle);
673 gridAngle += refly ? 90 : 0;
674 qCDebug(SurveyComplexItemLog) << "_rebuildTransectsPhase1 Clamped grid angle" << gridAngle;
675
676 qCDebug(SurveyComplexItemLog) << "_rebuildTransectsPhase1 gridSpacing:gridAngle:refly" << gridSpacing << gridAngle << refly;
677
678 // Convert polygon to bounding rect
679
680 qCDebug(SurveyComplexItemLog) << "_rebuildTransectsPhase1 Polygon";
681 QPolygonF polygon;
682 for (int i=0; i<polygonPoints.count(); i++) {
683 qCDebug(SurveyComplexItemLog) << "Vertex" << polygonPoints[i];
684 polygon << polygonPoints[i];
685 }
686 polygon << polygonPoints[0];
687 QRectF boundingRect = polygon.boundingRect();
688 QPointF boundingCenter = boundingRect.center();
689 qCDebug(SurveyComplexItemLog) << "Bounding rect" << boundingRect.topLeft().x() << boundingRect.topLeft().y() << boundingRect.bottomRight().x() << boundingRect.bottomRight().y();
690
691 // Create set of rotated parallel lines within the expanded bounding rect. Make the lines larger than the
692 // bounding box to guarantee intersection.
693
694 QList<QLineF> lineList;
695
696 // Sweep lines must extend beyond the polygon boundary regardless of grid angle.
697 // The worst case is when the polygon is rotated 45° relative to the sweep direction,
698 // where the required reach equals half the diagonal of the bounding rect.
699 // We use diagonal * 1.5 to provide a 50% safety margin beyond that worst case.
700 // Note: the old fixed +2000m was insufficient for polygons larger than ~10km.
701 const double diagonal = qSqrt(boundingRect.width() * boundingRect.width() + boundingRect.height() * boundingRect.height());
702 double maxWidth = diagonal * 1.5;
703 if (maxWidth <= 0.0) {
704 qCWarning(SurveyComplexItemLog) << "Degenerate polygon bounding rect (all vertices coincident or collinear), aborting transect rebuild";
705 return;
706 }
707
708 if (gridSpacing <= 0) {
709 // Invalid spacing: seed one center line so the < 2 fallback produces a single center transect.
710 qCWarning(SurveyComplexItemLog) << "Grid spacing" << gridSpacing << "is invalid, falling back to single center transect";
711 const double halfW = maxWidth / 2.0;
712 lineList += QLineF(
713 _rotatePoint(QPointF(boundingCenter.x(), boundingCenter.y() - halfW), boundingCenter, gridAngle),
714 _rotatePoint(QPointF(boundingCenter.x(), boundingCenter.y() + halfW), boundingCenter, gridAngle));
715 } else {
716 // Cap spacing so the sweep never generates more than maxTransectCount transects.
717 // Uses diagonal (not maxWidth) so the count reflects actual polygon-crossing transects.
718 if (gridSpacing < diagonal / maxTransectCount) {
719 qCWarning(SurveyComplexItemLog) << "Transect spacing" << gridSpacing << "raised to" << diagonal / maxTransectCount << "to limit transect count to" << maxTransectCount;
720 gridSpacing = diagonal / maxTransectCount;
721 }
722
723 double halfWidth = maxWidth / 2.0;
724 double transectX = boundingCenter.x() - halfWidth;
725 double transectXMax = transectX + maxWidth;
726 while (transectX < transectXMax) {
727 double transectYTop = boundingCenter.y() - halfWidth;
728 double transectYBottom = boundingCenter.y() + halfWidth;
729
730 lineList += QLineF(_rotatePoint(QPointF(transectX, transectYTop), boundingCenter, gridAngle), _rotatePoint(QPointF(transectX, transectYBottom), boundingCenter, gridAngle));
731 transectX += gridSpacing;
732 }
733 }
734
735 // Now intersect the lines with the polygon
736 QList<QLineF> intersectLines;
737#if 1
738 _intersectLinesWithPolygon(lineList, polygon, intersectLines);
739#else
740 // This is handy for debugging grid problems, not for release
741 intersectLines = lineList;
742#endif
743
744 // Less than two transects intersected with the polygon:
745 // Create a single transect which goes through the center of the polygon
746 // Intersect it with the polygon
747 if (intersectLines.count() < 2) {
749 QLineF firstLine = lineList.first();
750 QPointF lineCenter = firstLine.pointAt(0.5);
751 QPointF centerOffset = boundingCenter - lineCenter;
752 firstLine.translate(centerOffset);
753 lineList.clear();
754 lineList.append(firstLine);
755 intersectLines = lineList;
756 _intersectLinesWithPolygon(lineList, polygon, intersectLines);
757 }
758
759 // Make sure all lines are going the same direction. Polygon intersection leads to lines which
760 // can be in varied directions depending on the order of the intesecting sides.
761 QList<QLineF> resultLines;
762 _adjustLineDirection(intersectLines, resultLines);
763
764 // Convert from NED to Geo
765 QList<QList<QGeoCoordinate>> transects;
766 for (const QLineF& line : resultLines) {
767 QGeoCoordinate coord;
768 QList<QGeoCoordinate> transect;
769
770 QGCGeo::convertNedToGeo(line.p1().y(), line.p1().x(), 0, tangentOrigin, coord);
771 transect.append(coord);
772 QGCGeo::convertNedToGeo(line.p2().y(), line.p2().x(), 0, tangentOrigin, coord);
773 transect.append(coord);
774
775 transects.append(transect);
776 }
777
778 _adjustTransectsToEntryPointLocation(transects);
779
780 if (refly) {
781 _optimizeTransectsForShortestDistance(_transects.last().last().coord, transects);
782 }
783
784 if (_flyAlternateTransectsFact.rawValue().toBool()) {
785 QList<QList<QGeoCoordinate>> alternatingTransects;
786 for (int i=0; i<transects.count(); i++) {
787 if (!(i & 1)) {
788 alternatingTransects.append(transects[i]);
789 }
790 }
791 for (int i=transects.count()-1; i>0; i--) {
792 if (i & 1) {
793 alternatingTransects.append(transects[i]);
794 }
795 }
796 transects = alternatingTransects;
797 }
798
799 // Adjust to lawnmower pattern
800 bool reverseVertices = false;
801 for (int i=0; i<transects.count(); i++) {
802 // We must reverse the vertices for every other transect in order to make a lawnmower pattern
803 QList<QGeoCoordinate> transectVertices = transects[i];
804 if (reverseVertices) {
805 reverseVertices = false;
806 QList<QGeoCoordinate> reversedVertices;
807 for (int j=transectVertices.count()-1; j>=0; j--) {
808 reversedVertices.append(transectVertices[j]);
809 }
810 transectVertices = reversedVertices;
811 } else {
812 reverseVertices = true;
813 }
814 transects[i] = transectVertices;
815 }
816
817 // Convert to CoordInfo transects and append to _transects
818 for (const QList<QGeoCoordinate>& transect : transects) {
819 QGeoCoordinate coord;
820 QList<TransectStyleComplexItem::CoordInfo_t> coordInfoTransect;
822
823 coordInfo = { transect[0], CoordTypeSurveyEntry };
824 coordInfoTransect.append(coordInfo);
825 coordInfo = { transect[1], CoordTypeSurveyExit };
826 coordInfoTransect.append(coordInfo);
827
828 // For hover and capture we need points for each camera location within the transect
830 double transectLength = transect[0].distanceTo(transect[1]);
831 double transectAzimuth = transect[0].azimuthTo(transect[1]);
832 if (triggerDistance() < transectLength) {
833 int cInnerHoverPoints = static_cast<int>(floor(transectLength / triggerDistance()));
834 qCDebug(SurveyComplexItemLog) << "cInnerHoverPoints" << cInnerHoverPoints;
835 for (int i=0; i<cInnerHoverPoints; i++) {
836 QGeoCoordinate hoverCoord = transect[0].atDistanceAndAzimuth(triggerDistance() * (i + 1), transectAzimuth);
838 coordInfoTransect.insert(1 + i, hoverCoordInfo);
839 }
840 }
841 }
842
843 // Extend the transect ends for turnaround
844 if (_hasTurnaround()) {
845 QGeoCoordinate turnaroundCoord;
847
848 double azimuth = transect[0].azimuthTo(transect[1]);
849 turnaroundCoord = transect[0].atDistanceAndAzimuth(-turnAroundDistance, azimuth);
850 turnaroundCoord.setAltitude(qQNaN());
851 TransectStyleComplexItem::CoordInfo_t turnaroundCoordInfo = { turnaroundCoord, CoordTypeTurnaround };
852 coordInfoTransect.prepend(turnaroundCoordInfo);
853
854 azimuth = transect.last().azimuthTo(transect[transect.count() - 2]);
855 turnaroundCoord = transect.last().atDistanceAndAzimuth(-turnAroundDistance, azimuth);
856 turnaroundCoord.setAltitude(qQNaN());
857 coordInfo = { turnaroundCoord, CoordTypeTurnaround };
858 coordInfoTransect.append(coordInfo);
859 }
860
861 _transects.append(coordInfoTransect);
862 }
863}
864
865
866void SurveyComplexItem::_recalcCameraShots(void)
867{
868 double triggerDistance = this->triggerDistance();
869
870 if (triggerDistance == 0) {
871 _cameraShots = 0;
872 } else {
875 } else {
876 _cameraShots = 0;
877
879 // We have to do it the hard way based on the mission items themselves
881 // Count the number of camera triggers in the mission items
882 for (const MissionItem* missionItem: _loadedMissionItems) {
883 _cameraShots += missionItem->command() == MAV_CMD_IMAGE_START_CAPTURE ? 1 : 0;
884 }
885 } else {
886 bool waitingForTriggerStop = false;
887 QGeoCoordinate distanceStartCoord;
888 QGeoCoordinate distanceEndCoord;
889 for (const MissionItem* missionItem: _loadedMissionItems) {
890 if (missionItem->command() == MAV_CMD_NAV_WAYPOINT) {
891 if (waitingForTriggerStop) {
892 distanceEndCoord = QGeoCoordinate(missionItem->param5(), missionItem->param6());
893 } else {
894 distanceStartCoord = QGeoCoordinate(missionItem->param5(), missionItem->param6());
895 }
896 } else if (missionItem->command() == MAV_CMD_DO_SET_CAM_TRIGG_DIST) {
897 if (missionItem->param1() > 0) {
898 // Trigger start
899 waitingForTriggerStop = true;
900 } else {
901 // Trigger stop
902 waitingForTriggerStop = false;
903 _cameraShots += qCeil(distanceEndCoord.distanceTo(distanceStartCoord) / triggerDistance);
904 distanceStartCoord = QGeoCoordinate();
905 distanceEndCoord = QGeoCoordinate();
906 }
907 }
908 }
909
910 }
911 } else {
912 // We have transects available, calc from those
913 for (const QList<TransectStyleComplexItem::CoordInfo_t>& transect: _transects) {
914 QGeoCoordinate firstCameraCoord, lastCameraCoord;
915 if (_hasTurnaround() && !hoverAndCaptureEnabled()) {
916 firstCameraCoord = transect[1].coord;
917 lastCameraCoord = transect[transect.count() - 2].coord;
918 } else {
919 firstCameraCoord = transect.first().coord;
920 lastCameraCoord = transect.last().coord;
921 }
922 _cameraShots += qCeil(firstCameraCoord.distanceTo(lastCameraCoord) / triggerDistance);
923 }
924 }
925 }
926 }
927
928 emit cameraShotsChanged();
929}
930
935
937{
938 if (_entryPoint == EntryLocationLast) {
939 _entryPoint = EntryLocationFirst;
940 } else {
941 _entryPoint++;
942 }
943
945
946 setDirty(true);
947}
948
950{
951 return _vehicleSpeed == 0 ? 0 : triggerDistance() / _vehicleSpeed;
952}
953
955{
956 double hoverTime = 0;
957
959 for (const QList<TransectStyleComplexItem::CoordInfo_t>& transect: _transects) {
960 hoverTime += _hoverAndCaptureDelaySeconds * transect.count();
961 }
962 }
963
964 return hoverTime;
965}
966
967void SurveyComplexItem::_updateWizardMode(void)
968{
970 setWizardMode(false);
971 }
972}
#define qgcApp()
QString errorString
Geographic coordinate conversion utilities using GeographicLib.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void _setCameraNameFromV3TransectLoad(const QString &cameraName)
void setDistanceMode(QGroundControlQmlGlobal::AltitudeFrame altFrame)
Fact * imageDensity(void)
Definition CameraCalc.h:54
Fact * adjustedFootprintSide(void)
Definition CameraCalc.h:57
Fact * sideOverlap(void)
Definition CameraCalc.h:56
Fact * distanceToSurface(void)
Definition CameraCalc.h:53
Fact * adjustedFootprintFrontal(void)
Definition CameraCalc.h:58
void setCameraBrand(const QString &cameraBrand)
Fact * valueSetIsDistance(void)
Definition CameraCalc.h:52
Fact * frontalOverlap(void)
Definition CameraCalc.h:55
static QString canonicalManualCameraName(void)
SettingsFact * minTriggerInterval(void)
Definition CameraSpec.h:31
SettingsFact * imageHeight(void)
Definition CameraSpec.h:27
SettingsFact * sensorWidth(void)
Definition CameraSpec.h:24
SettingsFact * fixedOrientation(void)
Definition CameraSpec.h:30
SettingsFact * focalLength(void)
Definition CameraSpec.h:28
SettingsFact * sensorHeight(void)
Definition CameraSpec.h:25
SettingsFact * imageWidth(void)
Definition CameraSpec.h:26
SettingsFact * landscape(void)
Definition CameraSpec.h:29
void _savePresetJson(const QString &name, QJsonObject &presetObject)
static constexpr const char * jsonComplexItemTypeKey
This mission item attribute specifies the type of the complex item.
QJsonObject _loadPresetJson(const QString &name)
Holds the meta data associated with a Fact.
void setRawValue(const QVariant &value)
Definition Fact.cc:134
QVariant rawValue() const
Value after translation.
Definition Fact.h:85
void valueChanged(const QVariant &value)
This signal is only meant for use by the QT property system. It should not be connected to by client ...
Master controller for mission, fence, rally.
QmlObjectListModel & pathModel(void)
bool isValid(void) const
bool traceMode(void) const
QGeoCoordinate center(void) const
void traceModeChanged(bool traceMode)
void saveToJson(QJsonObject &json)
static constexpr const char * jsonPolygonKey
Q_INVOKABLE void clear(void)
bool isValidChanged(void)
int count(void) const
bool loadFromJson(const QJsonObject &json, bool required, QString &errorString)
This is a QGeoCoordinate within a QObject such that it can be used on a QmlObjectListModel.
T value(int index) const
Provides access to group of settings.
static SettingsManager * instance()
double timeBetweenShots(void) final
static constexpr const char * jsonComplexItemTypeValue
Q_INVOKABLE void rotateEntryPoint(void)
void refly90DegreesChanged(bool refly90Degrees)
ReadyForSaveState readyForSaveState(void) const final
double additionalTimeDelay(void) const final
bool load(const QJsonObject &complexObject, int sequenceNumber, QString &errorString) final
void applyPreviousAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame prevAltFrame, double prevAltitude) final
static constexpr const char * jsonV3ComplexItemTypeValue
void loadPreset(const QString &name)
void savePreset(const QString &name)
void save(QJsonArray &planItems) final
QObject * _loadedMissionItemsParent
Parent for all items in _loadedMissionItems for simpler delete.
QList< QList< CoordInfo_t > > _transects
QList< MissionItem * > _loadedMissionItems
Mission items loaded from plan file.
void setSequenceNumber(int sequenceNumber) final
QGeoCoordinate coordinate(void) const final
int sequenceNumber(void) const final
static constexpr int maxTransectCount
Maximum number of transects allowed; spacing is raised to enforce this limit.
ReadyForSaveState readyForSaveState(void) const override
bool _load(const QJsonObject &complexObject, bool forPresets, QString &errorString)
static constexpr int _hoverAndCaptureDelaySeconds
void _save(QJsonObject &saveObject)
@ CoordTypeTurnaround
Turnaround extension waypoint.
@ CoordTypeSurveyExit
Waypoint at exit edge of survey polygon.
@ CoordTypeInteriorHoverTrigger
Interior waypoint for hover and capture trigger.
@ CoordTypeSurveyEntry
Waypoint at entry edge of survey polygon.
static constexpr const char * jsonTypeComplexItemValue
Item type is Complex Item.
void setWizardMode(bool wizardMode)
static constexpr const char * jsonTypeKey
Json file attribute which specifies the item type.
double azimuth(void) const
bool validateKeys(const QJsonObject &jsonObject, const QList< KeyValidateInfo > &keyInfo, QString &errorString)
Validates that all required keys are present and that listed keys have the expected type.
constexpr const char * jsonVersionKey
Definition JsonParsing.h:12
void convertGeoToNed(const QGeoCoordinate &coord, const QGeoCoordinate &origin, double &x, double &y, double &z)
Definition QGCGeo.cc:34
void convertNedToGeo(double x, double y, double z, const QGeoCoordinate &origin, QGeoCoordinate &coord)
Definition QGCGeo.cc:56
void showAppMessage(const QString &message, const QString &title)
Modal application message. Queued if the UI isn't ready yet.
Definition AppMessages.cc:9