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::isValidChanged, this, &CorridorScanComplexItem::_updateWizardMode);
36 connect(&_corridorPolyline, &QGCMapPolyline::traceModeChanged, this, &CorridorScanComplexItem::_updateWizardMode);
37
38 if (!kmlOrShpFile.isEmpty()) {
39 _corridorPolyline.loadKMLOrSHPFile(kmlOrShpFile);
40 _corridorPolyline.setDirty(false);
41 }
42 setDirty(false);
43}
44
45void CorridorScanComplexItem::save(QJsonArray& planItems)
46{
47 QJsonObject saveObject;
48
49 _saveCommon(saveObject);
50 planItems.append(saveObject);
51}
52
53void CorridorScanComplexItem::savePreset(const QString& presetName)
54{
55 QJsonObject saveObject;
56
57 _saveCommon(saveObject);
58 _savePresetJson(presetName, saveObject);
59}
60
61void CorridorScanComplexItem::_saveCommon(QJsonObject& saveObject)
62{
64
65 saveObject[JsonParsing::jsonVersionKey] = 2;
68 saveObject[corridorWidthName] = _corridorWidthFact.rawValue().toDouble();
69 saveObject[_jsonEntryPointKey] = static_cast<int>(_entryPointLocation);
70
71 _corridorPolyline.saveToJson(saveObject);
72}
73
74void CorridorScanComplexItem::loadPreset(const QString& presetName)
75{
76 QString errorString;
77
78 QJsonObject presetObject = _loadPresetJson(presetName);
79 if (!_loadWorker(presetObject, 0, errorString, true /* forPresets */)) {
80 QGC::showAppMessage(QStringLiteral("Internal Error: Preset load failed. Name: %1 Error: %2").arg(presetName).arg(errorString));
81 }
83}
84
85bool CorridorScanComplexItem::_loadWorker(const QJsonObject& complexObject, int sequenceNumber, QString& errorString, bool forPresets)
86{
87 _ignoreRecalc = !forPresets;
88
89 QList<JsonParsing::KeyValidateInfo> keyInfoList = {
90 { JsonParsing::jsonVersionKey, QJsonValue::Double, true },
91 { VisualMissionItem::jsonTypeKey, QJsonValue::String, true },
92 { ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true },
93 { corridorWidthName, QJsonValue::Double, true },
94 { _jsonEntryPointKey, QJsonValue::Double, true },
95 { QGCMapPolyline::jsonPolylineKey, QJsonValue::Array, true },
96 };
97 if (!JsonParsing::validateKeys(complexObject, keyInfoList, errorString)) {
98 _ignoreRecalc = false;
99 return false;
100 }
101
102 QString itemType = complexObject[VisualMissionItem::jsonTypeKey].toString();
103 QString complexType = complexObject[ComplexMissionItem::jsonComplexItemTypeKey].toString();
105 errorString = tr("%1 does not support loading this complex mission item type: %2:%3").arg(qgcApp()->applicationName()).arg(itemType).arg(complexType);
106 _ignoreRecalc = false;
107 return false;
108 }
109
110 int version = complexObject[JsonParsing::jsonVersionKey].toInt();
111 if (version != 2) {
112 errorString = tr("%1 complex item version %2 not supported").arg(jsonComplexItemTypeValue).arg(version);
113 _ignoreRecalc = false;
114 return false;
115 }
116
117 if (!forPresets) {
118 if (!_corridorPolyline.loadFromJson(complexObject, true, errorString)) {
119 _ignoreRecalc = false;
120 return false;
121 }
122 }
123
125
126 if (!_load(complexObject, forPresets, errorString)) {
127 _ignoreRecalc = false;
128 return false;
129 }
130
131 _corridorWidthFact.setRawValue(complexObject[corridorWidthName].toDouble());
132
133 _entryPointLocation = static_cast<EntryPointLocation>(complexObject[_jsonEntryPointKey].toInt());
134
135 _ignoreRecalc = false;
136
138 if (_cameraShots == 0) {
139 // Shot count was possibly not available from plan file
140 _recalcCameraShots();
141 }
142
143 return true;
144}
145
146bool CorridorScanComplexItem::load(const QJsonObject& complexObject, int sequenceNumber, QString& errorString)
147{
148 return _loadWorker(complexObject, sequenceNumber, errorString, false /* forPresets */);
149}
150
152{
153 return _corridorPolyline.count() > 1;
154}
155
156void CorridorScanComplexItem::setCoordinate(const QGeoCoordinate& coordinate)
157{
158 if (!coordinate.isValid() || !_entryCoordinate.isValid() || _corridorPolyline.count() < 2) {
159 return;
160 }
161
162 const double distanceMeters = _entryCoordinate.distanceTo(coordinate);
163 const double azimuthDegrees = _entryCoordinate.azimuthTo(coordinate);
164 const QList<QGeoCoordinate> vertices = _corridorPolyline.coordinateList();
165
166 QList<QGeoCoordinate> translatedVertices;
167 translatedVertices.reserve(vertices.count());
168 for (const QGeoCoordinate& vertex: vertices) {
169 translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees));
170 }
171
172 _corridorPolyline.setPath(translatedVertices);
173}
174
175int CorridorScanComplexItem::_calcTransectCount(void) const
176{
177 double fullWidth = _corridorWidthFact.rawValue().toDouble();
178 if (fullWidth <= 0.0) {
179 return 1;
180 }
181 const double spacing = _calcTransectSpacing();
182 return spacing > 0.0 ? qMin(qCeil(fullWidth / spacing), maxTransectCount) : 1;
183}
184
185void CorridorScanComplexItem::_polylineDirtyChanged(bool dirty)
186{
187 if (dirty) {
188 setDirty(true);
189 }
190}
191
193{
194 int modeAsInt = static_cast<int>(_entryPointLocation);
195
196 if (_calcTransectCount() < 2) {
197 // 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
198 modeAsInt += 2;
199 } else {
200 modeAsInt++;
201 }
202
204 modeAsInt = 0;
205 }
206
207 _entryPointLocation = static_cast<EntryPointLocation>(modeAsInt);
208
210}
211
212void CorridorScanComplexItem::_rebuildCorridorPolygon(void)
213{
214 if (_corridorPolyline.count() < 2) {
216 return;
217 }
218
219 double halfWidth = _corridorWidthFact.rawValue().toDouble() / 2.0;
220
221 QList<QGeoCoordinate> firstSideVertices = _corridorPolyline.offsetPolyline(halfWidth);
222 QList<QGeoCoordinate> secondSideVertices = _corridorPolyline.offsetPolyline(-halfWidth);
223
225
226 QList<QGeoCoordinate> rgCoord;
227 for (const QGeoCoordinate& vertex: firstSideVertices) {
228 rgCoord.append(vertex);
229 }
230 for (int i=secondSideVertices.count() - 1; i >= 0; i--) {
231 rgCoord.append(secondSideVertices[i]);
232 }
234}
235
236void CorridorScanComplexItem::_rebuildTransectsPhase1(void)
237{
238 if (_ignoreRecalc) {
239 return;
240 }
241
242 // If the transects are getting rebuilt then any previsouly loaded mission items are now invalid
244 _loadedMissionItems.clear();
245 _loadedMissionItemsParent->deleteLater();
247 }
248
249 double transectSpacing = _calcTransectSpacing();
250 double fullWidth = _corridorWidthFact.rawValue().toDouble();
251 double halfWidth = fullWidth / 2.0;
252 int transectCount = _calcTransectCount();
253 double normalizedTransectPosition = transectSpacing / 2.0;
254
255 if (_corridorPolyline.count() >= 2) {
256 // First build up the transects all going the same direction
257 //qDebug() << "_rebuildTransectsPhase1";
258 for (int i=0; i<transectCount; i++) {
259 //qDebug() << "start transect";
260 double offsetDistance;
261 if (transectCount == 1) {
262 // Single transect is flown over scan line
263 offsetDistance = 0;
264 } else {
265 // Convert from normalized to absolute transect offset distance
266 offsetDistance = halfWidth - normalizedTransectPosition;
267 }
268
269 // Turn transect into CoordInfo transect
270 QList<TransectStyleComplexItem::CoordInfo_t> transect;
271 QList<QGeoCoordinate> transectCoords = _corridorPolyline.offsetPolyline(offsetDistance);
272 for (int j=1; j<transectCoords.count() - 1; j++) {
273 TransectStyleComplexItem::CoordInfo_t coordInfo = { transectCoords[j], CoordTypeInterior };
274 transect.append(coordInfo);
275 }
276 TransectStyleComplexItem::CoordInfo_t coordInfo = { transectCoords.first(), CoordTypeSurveyEntry };
277 transect.prepend(coordInfo);
278 coordInfo = { transectCoords.last(), CoordTypeSurveyExit };
279 transect.append(coordInfo);
280
281 // Extend the transect ends for turnaround
282 if (_hasTurnaround()) {
283 QGeoCoordinate turnaroundCoord;
285
286 double azimuth = transectCoords[0].azimuthTo(transectCoords[1]);
287 turnaroundCoord = transectCoords[0].atDistanceAndAzimuth(-turnAroundDistance, azimuth);
288 turnaroundCoord.setAltitude(qQNaN());
289 TransectStyleComplexItem::CoordInfo_t turnaroundCoordInfo = { turnaroundCoord, CoordTypeTurnaround };
290 transect.prepend(turnaroundCoordInfo);
291
292 azimuth = transectCoords.last().azimuthTo(transectCoords[transectCoords.count() - 2]);
293 turnaroundCoord = transectCoords.last().atDistanceAndAzimuth(-turnAroundDistance, azimuth);
294 turnaroundCoord.setAltitude(qQNaN());
295 coordInfo = { turnaroundCoord, CoordTypeTurnaround };
296 transect.append(coordInfo);
297 }
298
299#if 0
300 qDebug() << "transect debug";
301 for (const TransectStyleComplexItem::CoordInfo_t& coordInfo: transect) {
302 qDebug() << coordInfo.coordType;
303 }
304#endif
305
306 _transects.append(transect);
307 normalizedTransectPosition += transectSpacing;
308 }
309
310 // Now deal with fixing up the entry point:
311 // 0: Leave alone
312 // 1: Start at same end, opposite side of center
313 // 2: Start at opposite end, same side
314 // 3: Start at opposite end, opposite side
315
316 bool reverseTransects = false;
317 bool reverseVertices = false;
318 switch (_entryPointLocation) {
320 reverseTransects = false;
321 reverseVertices = false;
322 break;
324 reverseTransects = true;
325 reverseVertices = false;
326 break;
328 reverseTransects = false;
329 reverseVertices = true;
330 break;
332 reverseTransects = true;
333 reverseVertices = true;
334 break;
335 }
336 if (reverseTransects) {
337 QList<QList<TransectStyleComplexItem::CoordInfo_t>> reversedTransects;
338 for (const QList<TransectStyleComplexItem::CoordInfo_t>& transect: _transects) {
339 reversedTransects.prepend(transect);
340 }
341 _transects = reversedTransects;
342 }
343 if (reverseVertices) {
344 for (int i=0; i<_transects.count(); i++) {
345 QList<TransectStyleComplexItem::CoordInfo_t> reversedVertices;
346 for (const TransectStyleComplexItem::CoordInfo_t& vertex: _transects[i]) {
347 reversedVertices.prepend(vertex);
348 }
349 _transects[i] = reversedVertices;
350 }
351 }
352
353 // Adjust to lawnmower pattern
354 reverseVertices = false;
355 for (int i=0; i<_transects.count(); i++) {
356 // We must reverse the vertices for every other transect in order to make a lawnmower pattern
357 QList<TransectStyleComplexItem::CoordInfo_t> transectVertices = _transects[i];
358 if (reverseVertices) {
359 reverseVertices = false;
360 QList<TransectStyleComplexItem::CoordInfo_t> reversedVertices;
361 for (int j=transectVertices.count()-1; j>=0; j--) {
362 reversedVertices.append(transectVertices[j]);
363
364 // as we are flying the transect reversed, we also need to swap entry and exit coordinate types
365 if (reversedVertices.last().coordType == CoordTypeSurveyEntry) {
366 reversedVertices.last().coordType = CoordTypeSurveyExit;
367 } else if (reversedVertices.last().coordType == CoordTypeSurveyExit) {
368 reversedVertices.last().coordType = CoordTypeSurveyEntry;
369 }
370 }
371
372 transectVertices = reversedVertices;
373 } else {
374 reverseVertices = true;
375 }
376 _transects[i] = transectVertices;
377 }
378 }
379}
380
381void CorridorScanComplexItem::_recalcCameraShots(void)
382{
384 if (triggerDistance == 0) {
385 _cameraShots = 0;
386 } else {
389 } else {
390 int singleTransectImageCount = qCeil(_corridorPolyline.length() / triggerDistance);
391 _cameraShots = singleTransectImageCount * _calcTransectCount();
392 }
393 }
394 emit cameraShotsChanged();
395}
396
401
406
407double CorridorScanComplexItem::_calcTransectSpacing(void) const
408{
409 double transectSpacing = _cameraCalc.adjustedFootprintSide()->rawValue().toDouble();
410 if (transectSpacing <= 0) {
411 return 0;
412 }
413
414 // Cap spacing so the corridor never generates more than maxTransectCount transects.
415 // The relevant extent is the corridor width (transects run perpendicular to the path).
416 const double corridorWidth = _corridorWidthFact.rawValue().toDouble();
417 if (corridorWidth <= 0.0) {
418 qCWarning(CorridorScanComplexItemLog) << "Corridor width" << corridorWidth << "is invalid, skipping transect count cap";
419 return transectSpacing;
420 }
421 if (transectSpacing < corridorWidth / maxTransectCount) {
422 qCWarning(CorridorScanComplexItemLog) << "Transect spacing" << transectSpacing << "raised to" << corridorWidth / maxTransectCount << "to limit transect count to" << maxTransectCount;
423 transectSpacing = corridorWidth / maxTransectCount;
424 }
425
426 return transectSpacing;
427}
428
429void CorridorScanComplexItem::_updateWizardMode(void)
430{
431 if (_corridorPolyline.isValid() && !_corridorPolyline.traceMode()) {
432 setWizardMode(false);
433 }
434}
#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
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.
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.
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)
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