QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
CorridorScanComplexItem.cc
Go to the documentation of this file.
2#include "JsonParsing.h"
3#include "SettingsManager.h"
4#include "AppSettings.h"
6#include "AppMessages.h"
7#include "QGCApplication.h"
9
10#include <QtCore/QJsonArray>
11
12QGC_LOGGING_CATEGORY(CorridorScanComplexItemLog, "Plan.CorridorScanComplexItem")
13
14CorridorScanComplexItem::CorridorScanComplexItem(PlanMasterController* masterController, bool flyView, const QString& kmlOrShpFile)
15 : TransectStyleComplexItem (masterController, flyView, settingsGroup)
16 , _entryPointLocation (EntryPointDefaultOrder)
17 , _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/CorridorScan.SettingsGroup.json"), this))
18 , _corridorWidthFact (settingsGroup, _metaDataMap[corridorWidthName])
19{
20 _editorQml = "qrc:/qml/QGroundControl/PlanView/CorridorScanEditor.qml";
21
22 // We override the altitude to the mission default
23 if (_cameraCalc.isManualCamera() || !_cameraCalc.valueSetIsDistance()->rawValue().toBool()) {
24 _cameraCalc.distanceToSurface()->setRawValue(SettingsManager::instance()->appSettings()->defaultMissionItemAltitude()->rawValue());
25 }
26
27 connect(&_corridorWidthFact, &Fact::valueChanged, this, &CorridorScanComplexItem::_setDirty);
28 connect(&_corridorPolyline, &QGCMapPolyline::pathChanged, this, &CorridorScanComplexItem::_setDirty);
29
30 connect(&_corridorPolyline, &QGCMapPolyline::dirtyChanged, this, &CorridorScanComplexItem::_polylineDirtyChanged);
31
32 connect(&_corridorPolyline, &QGCMapPolyline::pathChanged, this, &CorridorScanComplexItem::_rebuildCorridorPolygon);
33 connect(&_corridorWidthFact, &Fact::valueChanged, this, &CorridorScanComplexItem::_rebuildCorridorPolygon);
34
35 connect(&_corridorPolyline, &QGCMapPolyline::countChanged, this, &CorridorScanComplexItem::_updateSpecifiesCoordinate);
36
37 connect(&_corridorPolyline, &QGCMapPolyline::isValidChanged, this, &CorridorScanComplexItem::_updateWizardMode);
38 connect(&_corridorPolyline, &QGCMapPolyline::traceModeChanged, this, &CorridorScanComplexItem::_updateWizardMode);
39
40 // Anchor the cache to current state so correctness doesn't depend on constructor ordering
41 _updateSpecifiesCoordinate();
42
43 if (!kmlOrShpFile.isEmpty()) {
44 _corridorPolyline.loadKMLOrSHPFile(kmlOrShpFile);
45 _corridorPolyline.setDirty(false);
46 }
47 setDirty(false);
48}
49
50void CorridorScanComplexItem::save(QJsonArray& planItems)
51{
52 QJsonObject saveObject;
53
54 _saveCommon(saveObject);
55 planItems.append(saveObject);
56}
57
58void CorridorScanComplexItem::savePreset(const QString& presetName)
59{
60 QJsonObject saveObject;
61
62 _saveCommon(saveObject);
63 _savePresetJson(presetName, saveObject);
64}
65
66void CorridorScanComplexItem::_saveCommon(QJsonObject& saveObject)
67{
69
70 saveObject[JsonParsing::jsonVersionKey] = 2;
73 saveObject[corridorWidthName] = _corridorWidthFact.rawValue().toDouble();
74 saveObject[_jsonEntryPointKey] = static_cast<int>(_entryPointLocation);
75
76 _corridorPolyline.saveToJson(saveObject);
77}
78
79void CorridorScanComplexItem::loadPreset(const QString& presetName)
80{
81 QString errorString;
82
83 QJsonObject presetObject = _loadPresetJson(presetName);
84 if (!_loadWorker(presetObject, 0, errorString, true /* forPresets */)) {
85 QGC::showAppMessage(QStringLiteral("Internal Error: Preset load failed. Name: %1 Error: %2").arg(presetName).arg(errorString));
86 }
88}
89
90bool CorridorScanComplexItem::_loadWorker(const QJsonObject& complexObject, int sequenceNumber, QString& errorString, bool forPresets)
91{
92 _ignoreRecalc = !forPresets;
93
94 QList<JsonParsing::KeyValidateInfo> keyInfoList = {
95 { JsonParsing::jsonVersionKey, QJsonValue::Double, true },
96 { VisualMissionItem::jsonTypeKey, QJsonValue::String, true },
97 { ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true },
98 { corridorWidthName, QJsonValue::Double, true },
99 { _jsonEntryPointKey, QJsonValue::Double, true },
100 { QGCMapPolyline::jsonPolylineKey, QJsonValue::Array, true },
101 };
102 if (!JsonParsing::validateKeys(complexObject, keyInfoList, errorString)) {
103 _ignoreRecalc = false;
104 return false;
105 }
106
107 QString itemType = complexObject[VisualMissionItem::jsonTypeKey].toString();
108 QString complexType = complexObject[ComplexMissionItem::jsonComplexItemTypeKey].toString();
110 errorString = tr("%1 does not support loading this complex mission item type: %2:%3").arg(qgcApp()->applicationName()).arg(itemType).arg(complexType);
111 _ignoreRecalc = false;
112 return false;
113 }
114
115 int version = complexObject[JsonParsing::jsonVersionKey].toInt();
116 if (version != 2) {
117 errorString = tr("%1 complex item version %2 not supported").arg(jsonComplexItemTypeValue).arg(version);
118 _ignoreRecalc = false;
119 return false;
120 }
121
122 if (!forPresets) {
123 if (!_corridorPolyline.loadFromJson(complexObject, true, errorString)) {
124 _ignoreRecalc = false;
125 return false;
126 }
127 }
128
130
131 if (!_load(complexObject, forPresets, errorString)) {
132 _ignoreRecalc = false;
133 return false;
134 }
135
136 _corridorWidthFact.setRawValue(complexObject[corridorWidthName].toDouble());
137
138 _entryPointLocation = static_cast<EntryPointLocation>(complexObject[_jsonEntryPointKey].toInt());
139
140 _ignoreRecalc = false;
141
143 if (_cameraShots == 0) {
144 // Shot count was possibly not available from plan file
145 _recalcCameraShots();
146 }
147
148 return true;
149}
150
151bool CorridorScanComplexItem::load(const QJsonObject& complexObject, int sequenceNumber, QString& errorString)
152{
153 return _loadWorker(complexObject, sequenceNumber, errorString, false /* forPresets */);
154}
155
157{
158 return _corridorPolyline.count() > 1;
159}
160
161// specifiesCoordinate() depends on polyline count, so the NOTIFY signal must be emitted when it flips
162void CorridorScanComplexItem::_updateSpecifiesCoordinate(void)
163{
164 const bool newSpecifiesCoordinate = specifiesCoordinate();
165 if (newSpecifiesCoordinate != _specifiesCoordinate) {
166 _specifiesCoordinate = newSpecifiesCoordinate;
168 }
169}
170
171void CorridorScanComplexItem::setCoordinate(const QGeoCoordinate& coordinate)
172{
173 if (!coordinate.isValid() || !_entryCoordinate.isValid() || _corridorPolyline.count() < 2) {
174 return;
175 }
176
177 const double distanceMeters = _entryCoordinate.distanceTo(coordinate);
178 const double azimuthDegrees = _entryCoordinate.azimuthTo(coordinate);
179 const QList<QGeoCoordinate> vertices = _corridorPolyline.coordinateList();
180
181 QList<QGeoCoordinate> translatedVertices;
182 translatedVertices.reserve(vertices.count());
183 for (const QGeoCoordinate& vertex: vertices) {
184 translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees));
185 }
186
187 _corridorPolyline.setPath(translatedVertices);
188}
189
190int CorridorScanComplexItem::_calcTransectCount(void) const
191{
192 double fullWidth = _corridorWidthFact.rawValue().toDouble();
193 if (fullWidth <= 0.0) {
194 return 1;
195 }
196 const double spacing = _calcTransectSpacing();
197 return spacing > 0.0 ? qMin(qCeil(fullWidth / spacing), maxTransectCount) : 1;
198}
199
200void CorridorScanComplexItem::_polylineDirtyChanged(bool dirty)
201{
202 if (dirty) {
203 setDirty(true);
204 }
205}
206
208{
209 int modeAsInt = static_cast<int>(_entryPointLocation);
210
211 if (_calcTransectCount() < 2) {
212 // A single transect has no "opposite side of center" so we need to bump by 2 to get to the opposite end of the scan
213 modeAsInt += 2;
214 } else {
215 modeAsInt++;
216 }
217
219 modeAsInt = 0;
220 }
221
222 _entryPointLocation = static_cast<EntryPointLocation>(modeAsInt);
223
225}
226
227void CorridorScanComplexItem::_rebuildCorridorPolygon(void)
228{
229 if (_corridorPolyline.count() < 2) {
231 return;
232 }
233
234 double halfWidth = _corridorWidthFact.rawValue().toDouble() / 2.0;
235
236 QList<QGeoCoordinate> firstSideVertices = _corridorPolyline.offsetPolyline(halfWidth);
237 QList<QGeoCoordinate> secondSideVertices = _corridorPolyline.offsetPolyline(-halfWidth);
238
240
241 QList<QGeoCoordinate> rgCoord;
242 for (const QGeoCoordinate& vertex: firstSideVertices) {
243 rgCoord.append(vertex);
244 }
245 for (int i=secondSideVertices.count() - 1; i >= 0; i--) {
246 rgCoord.append(secondSideVertices[i]);
247 }
249}
250
251void CorridorScanComplexItem::_rebuildTransectsPhase1(void)
252{
253 if (_ignoreRecalc) {
254 return;
255 }
256
257 // If the transects are getting rebuilt then any previsouly loaded mission items are now invalid
259 _loadedMissionItems.clear();
260 _loadedMissionItemsParent->deleteLater();
262 }
263
264 double transectSpacing = _calcTransectSpacing();
265 double fullWidth = _corridorWidthFact.rawValue().toDouble();
266 double halfWidth = fullWidth / 2.0;
267 int transectCount = _calcTransectCount();
268 double normalizedTransectPosition = transectSpacing / 2.0;
269
270 if (_corridorPolyline.count() >= 2) {
271 // First build up the transects all going the same direction
272 //qDebug() << "_rebuildTransectsPhase1";
273 for (int i=0; i<transectCount; i++) {
274 //qDebug() << "start transect";
275 double offsetDistance;
276 if (transectCount == 1) {
277 // Single transect is flown over scan line
278 offsetDistance = 0;
279 } else {
280 // Convert from normalized to absolute transect offset distance
281 offsetDistance = halfWidth - normalizedTransectPosition;
282 }
283
284 // Turn transect into CoordInfo transect
285 QList<TransectStyleComplexItem::CoordInfo_t> transect;
286 QList<QGeoCoordinate> transectCoords = _corridorPolyline.offsetPolyline(offsetDistance);
287 for (int j=1; j<transectCoords.count() - 1; j++) {
288 TransectStyleComplexItem::CoordInfo_t coordInfo = { transectCoords[j], CoordTypeInterior };
289 transect.append(coordInfo);
290 }
291 TransectStyleComplexItem::CoordInfo_t coordInfo = { transectCoords.first(), CoordTypeSurveyEntry };
292 transect.prepend(coordInfo);
293 coordInfo = { transectCoords.last(), CoordTypeSurveyExit };
294 transect.append(coordInfo);
295
296 // Extend the transect ends for turnaround
297 if (_hasTurnaround()) {
298 QGeoCoordinate turnaroundCoord;
300
301 double azimuth = transectCoords[0].azimuthTo(transectCoords[1]);
302 turnaroundCoord = transectCoords[0].atDistanceAndAzimuth(-turnAroundDistance, azimuth);
303 turnaroundCoord.setAltitude(qQNaN());
304 TransectStyleComplexItem::CoordInfo_t turnaroundCoordInfo = { turnaroundCoord, CoordTypeTurnaround };
305 transect.prepend(turnaroundCoordInfo);
306
307 azimuth = transectCoords.last().azimuthTo(transectCoords[transectCoords.count() - 2]);
308 turnaroundCoord = transectCoords.last().atDistanceAndAzimuth(-turnAroundDistance, azimuth);
309 turnaroundCoord.setAltitude(qQNaN());
310 coordInfo = { turnaroundCoord, CoordTypeTurnaround };
311 transect.append(coordInfo);
312 }
313
314#if 0
315 qDebug() << "transect debug";
316 for (const TransectStyleComplexItem::CoordInfo_t& coordInfo: transect) {
317 qDebug() << coordInfo.coordType;
318 }
319#endif
320
321 _transects.append(transect);
322 normalizedTransectPosition += transectSpacing;
323 }
324
325 // Now deal with fixing up the entry point:
326 // 0: Leave alone
327 // 1: Start at same end, opposite side of center
328 // 2: Start at opposite end, same side
329 // 3: Start at opposite end, opposite side
330
331 bool reverseTransects = false;
332 bool reverseVertices = false;
333 switch (_entryPointLocation) {
335 reverseTransects = false;
336 reverseVertices = false;
337 break;
339 reverseTransects = true;
340 reverseVertices = false;
341 break;
343 reverseTransects = false;
344 reverseVertices = true;
345 break;
347 reverseTransects = true;
348 reverseVertices = true;
349 break;
350 }
351 if (reverseTransects) {
352 QList<QList<TransectStyleComplexItem::CoordInfo_t>> reversedTransects;
353 for (const QList<TransectStyleComplexItem::CoordInfo_t>& transect: _transects) {
354 reversedTransects.prepend(transect);
355 }
356 _transects = reversedTransects;
357 }
358 if (reverseVertices) {
359 for (int i=0; i<_transects.count(); i++) {
360 QList<TransectStyleComplexItem::CoordInfo_t> reversedVertices;
361 for (const TransectStyleComplexItem::CoordInfo_t& vertex: _transects[i]) {
362 reversedVertices.prepend(vertex);
363 }
364 _transects[i] = reversedVertices;
365 }
366 }
367
368 // Adjust to lawnmower pattern
369 reverseVertices = false;
370 for (int i=0; i<_transects.count(); i++) {
371 // We must reverse the vertices for every other transect in order to make a lawnmower pattern
372 QList<TransectStyleComplexItem::CoordInfo_t> transectVertices = _transects[i];
373 if (reverseVertices) {
374 reverseVertices = false;
375 QList<TransectStyleComplexItem::CoordInfo_t> reversedVertices;
376 for (int j=transectVertices.count()-1; j>=0; j--) {
377 reversedVertices.append(transectVertices[j]);
378
379 // as we are flying the transect reversed, we also need to swap entry and exit coordinate types
380 if (reversedVertices.last().coordType == CoordTypeSurveyEntry) {
381 reversedVertices.last().coordType = CoordTypeSurveyExit;
382 } else if (reversedVertices.last().coordType == CoordTypeSurveyExit) {
383 reversedVertices.last().coordType = CoordTypeSurveyEntry;
384 }
385 }
386
387 transectVertices = reversedVertices;
388 } else {
389 reverseVertices = true;
390 }
391 _transects[i] = transectVertices;
392 }
393 }
394}
395
396void CorridorScanComplexItem::_recalcCameraShots(void)
397{
399 if (triggerDistance == 0) {
400 _cameraShots = 0;
401 } else {
404 } else {
405 int singleTransectImageCount = qCeil(_corridorPolyline.length() / triggerDistance);
406 _cameraShots = singleTransectImageCount * _calcTransectCount();
407 }
408 }
409 emit cameraShotsChanged();
410}
411
416
421
422double CorridorScanComplexItem::_calcTransectSpacing(void) const
423{
424 double transectSpacing = _cameraCalc.adjustedFootprintSide()->rawValue().toDouble();
425 if (transectSpacing <= 0) {
426 return 0;
427 }
428
429 // Cap spacing so the corridor never generates more than maxTransectCount transects.
430 // The relevant extent is the corridor width (transects run perpendicular to the path).
431 const double corridorWidth = _corridorWidthFact.rawValue().toDouble();
432 if (corridorWidth <= 0.0) {
433 qCWarning(CorridorScanComplexItemLog) << "Corridor width" << corridorWidth << "is invalid, skipping transect count cap";
434 return transectSpacing;
435 }
436 if (transectSpacing < corridorWidth / maxTransectCount) {
437 qCWarning(CorridorScanComplexItemLog) << "Transect spacing" << transectSpacing << "raised to" << corridorWidth / maxTransectCount << "to limit transect count to" << maxTransectCount;
438 transectSpacing = corridorWidth / maxTransectCount;
439 }
440
441 return transectSpacing;
442}
443
444void CorridorScanComplexItem::_updateWizardMode(void)
445{
446 if (_corridorPolyline.isValid() && !_corridorPolyline.traceMode()) {
447 setWizardMode(false);
448 }
449}
#define qgcApp()
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Fact * adjustedFootprintSide(void)
Definition CameraCalc.h:57
Fact * adjustedFootprintFrontal(void)
Definition CameraCalc.h:58
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)
bool load(const QJsonObject &complexObject, int sequenceNumber, QString &errorString) final
void savePreset(const QString &name)
void setCoordinate(const QGeoCoordinate &coordinate) final
void save(QJsonArray &planItems) final
static constexpr const char * corridorWidthName
bool specifiesCoordinate(void) const final
void loadPreset(const QString &name)
Q_INVOKABLE void rotateEntryPoint(void)
static constexpr const char * jsonComplexItemTypeValue
ReadyForSaveState readyForSaveState(void) const final
Holds the meta data associated with a Fact.
void setRawValue(const QVariant &value)
Definition Fact.cc:134
QVariant rawValue() const
Definition Fact.h:90
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.
Q_INVOKABLE void clear(void)
Q_INVOKABLE void appendVertices(const QVariantList &varCoords)
QList< QGeoCoordinate > offsetPolyline(double distance)
void dirtyChanged(bool dirty)
static constexpr const char * jsonPolylineKey
void pathChanged(void)
void traceModeChanged(bool traceMode)
bool isValid(void) const
void setPath(const QList< QGeoCoordinate > &path)
int count(void) const
void saveToJson(QJsonObject &json)
QList< QGeoCoordinate > coordinateList(void) const
Returns the path in a list of QGeoCoordinate's format.
void isValidChanged(void)
bool loadFromJson(const QJsonObject &json, bool required, QString &errorString)
double length(void) const
Returns the length of the polyline in meters.
void countChanged(int count)
bool traceMode(void) const
Provides access to group of settings.
static SettingsManager * instance()
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)
void _save(QJsonObject &saveObject)
@ CoordTypeTurnaround
Turnaround extension waypoint.
@ CoordTypeSurveyExit
Waypoint at exit edge of survey polygon.
@ CoordTypeInterior
Interior waypoint for flight path only (example: interior corridor point)
@ CoordTypeSurveyEntry
Waypoint at entry edge of survey polygon.
static constexpr const char * jsonTypeComplexItemValue
Item type is Complex Item.
void setWizardMode(bool wizardMode)
void specifiesCoordinateChanged(void)
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 showAppMessage(const QString &message, const QString &title)
Modal application message. Queued if the UI isn't ready yet.
Definition AppMessages.cc:9