QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
CorridorScanComplexItem.cc
Go to the documentation of this file.
2#include "JsonHelper.h"
3#include "SettingsManager.h"
4#include "AppSettings.h"
6#include "QGCApplication.h"
8
9#include <QtCore/QJsonArray>
10
11QGC_LOGGING_CATEGORY(CorridorScanComplexItemLog, "Plan.CorridorScanComplexItemL")
12
13const QString CorridorScanComplexItem::name(CorridorScanComplexItem::tr("Corridor Scan"));
14
15CorridorScanComplexItem::CorridorScanComplexItem(PlanMasterController* masterController, bool flyView, const QString& kmlOrShpFile)
16 : TransectStyleComplexItem (masterController, flyView, settingsGroup)
17 , _entryPointLocation (EntryPointDefaultOrder)
18 , _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/CorridorScan.SettingsGroup.json"), this))
19 , _corridorWidthFact (settingsGroup, _metaDataMap[corridorWidthName])
20{
21 _editorQml = "qrc:/qml/QGroundControl/Controls/CorridorScanEditor.qml";
22
23 // We override the altitude to the mission default
24 if (_cameraCalc.isManualCamera() || !_cameraCalc.valueSetIsDistance()->rawValue().toBool()) {
25 _cameraCalc.distanceToSurface()->setRawValue(SettingsManager::instance()->appSettings()->defaultMissionItemAltitude()->rawValue());
26 }
27
28 connect(&_corridorWidthFact, &Fact::valueChanged, this, &CorridorScanComplexItem::_setDirty);
29 connect(&_corridorPolyline, &QGCMapPolyline::pathChanged, this, &CorridorScanComplexItem::_setDirty);
30
31 connect(&_corridorPolyline, &QGCMapPolyline::dirtyChanged, this, &CorridorScanComplexItem::_polylineDirtyChanged);
32
33 connect(&_corridorPolyline, &QGCMapPolyline::pathChanged, this, &CorridorScanComplexItem::_rebuildCorridorPolygon);
34 connect(&_corridorWidthFact, &Fact::valueChanged, this, &CorridorScanComplexItem::_rebuildCorridorPolygon);
35
36 connect(&_corridorPolyline, &QGCMapPolyline::isValidChanged, this, &CorridorScanComplexItem::_updateWizardMode);
37 connect(&_corridorPolyline, &QGCMapPolyline::traceModeChanged, this, &CorridorScanComplexItem::_updateWizardMode);
38
39 if (!kmlOrShpFile.isEmpty()) {
40 _corridorPolyline.loadKMLOrSHPFile(kmlOrShpFile);
41 _corridorPolyline.setDirty(false);
42 }
43 setDirty(false);
44}
45
46void CorridorScanComplexItem::save(QJsonArray& planItems)
47{
48 QJsonObject saveObject;
49
50 _saveCommon(saveObject);
51 planItems.append(saveObject);
52}
53
54void CorridorScanComplexItem::savePreset(const QString& presetName)
55{
56 QJsonObject saveObject;
57
58 _saveCommon(saveObject);
59 _savePresetJson(presetName, saveObject);
60}
61
62void CorridorScanComplexItem::_saveCommon(QJsonObject& saveObject)
63{
65
66 saveObject[JsonHelper::jsonVersionKey] = 2;
69 saveObject[corridorWidthName] = _corridorWidthFact.rawValue().toDouble();
70 saveObject[_jsonEntryPointKey] = static_cast<int>(_entryPointLocation);
71
72 _corridorPolyline.saveToJson(saveObject);
73}
74
75void CorridorScanComplexItem::loadPreset(const QString& presetName)
76{
77 QString errorString;
78
79 QJsonObject presetObject = _loadPresetJson(presetName);
80 if (!_loadWorker(presetObject, 0, errorString, true /* forPresets */)) {
81 qgcApp()->showAppMessage(QStringLiteral("Internal Error: Preset load failed. Name: %1 Error: %2").arg(presetName).arg(errorString));
82 }
84}
85
86bool CorridorScanComplexItem::_loadWorker(const QJsonObject& complexObject, int sequenceNumber, QString& errorString, bool forPresets)
87{
88 _ignoreRecalc = !forPresets;
89
90 QList<JsonHelper::KeyValidateInfo> keyInfoList = {
91 { JsonHelper::jsonVersionKey, QJsonValue::Double, true },
92 { VisualMissionItem::jsonTypeKey, QJsonValue::String, true },
93 { ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true },
94 { corridorWidthName, QJsonValue::Double, true },
95 { _jsonEntryPointKey, QJsonValue::Double, true },
96 { QGCMapPolyline::jsonPolylineKey, QJsonValue::Array, true },
97 };
98 if (!JsonHelper::validateKeys(complexObject, keyInfoList, errorString)) {
99 _ignoreRecalc = false;
100 return false;
101 }
102
103 QString itemType = complexObject[VisualMissionItem::jsonTypeKey].toString();
104 QString complexType = complexObject[ComplexMissionItem::jsonComplexItemTypeKey].toString();
106 errorString = tr("%1 does not support loading this complex mission item type: %2:%3").arg(qgcApp()->applicationName()).arg(itemType).arg(complexType);
107 _ignoreRecalc = false;
108 return false;
109 }
110
111 int version = complexObject[JsonHelper::jsonVersionKey].toInt();
112 if (version != 2) {
113 errorString = tr("%1 complex item version %2 not supported").arg(jsonComplexItemTypeValue).arg(version);
114 _ignoreRecalc = false;
115 return false;
116 }
117
118 if (!forPresets) {
119 if (!_corridorPolyline.loadFromJson(complexObject, true, errorString)) {
120 _ignoreRecalc = false;
121 return false;
122 }
123 }
124
126
127 if (!_load(complexObject, forPresets, errorString)) {
128 _ignoreRecalc = false;
129 return false;
130 }
131
132 _corridorWidthFact.setRawValue(complexObject[corridorWidthName].toDouble());
133
134 _entryPointLocation = static_cast<EntryPointLocation>(complexObject[_jsonEntryPointKey].toInt());
135
136 _ignoreRecalc = false;
137
139 if (_cameraShots == 0) {
140 // Shot count was possibly not available from plan file
141 _recalcCameraShots();
142 }
143
144 return true;
145}
146
147bool CorridorScanComplexItem::load(const QJsonObject& complexObject, int sequenceNumber, QString& errorString)
148{
149 return _loadWorker(complexObject, sequenceNumber, errorString, false /* forPresets */);
150}
151
153{
154 return _corridorPolyline.count() > 1;
155}
156
157void CorridorScanComplexItem::setCoordinate(const QGeoCoordinate& coordinate)
158{
159 if (!coordinate.isValid() || !_entryCoordinate.isValid() || _corridorPolyline.count() < 2) {
160 return;
161 }
162
163 const double distanceMeters = _entryCoordinate.distanceTo(coordinate);
164 const double azimuthDegrees = _entryCoordinate.azimuthTo(coordinate);
165 const QList<QGeoCoordinate> vertices = _corridorPolyline.coordinateList();
166
167 QList<QGeoCoordinate> translatedVertices;
168 translatedVertices.reserve(vertices.count());
169 for (const QGeoCoordinate& vertex: vertices) {
170 translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees));
171 }
172
173 _corridorPolyline.setPath(translatedVertices);
174}
175
176int CorridorScanComplexItem::_calcTransectCount(void) const
177{
178 double fullWidth = _corridorWidthFact.rawValue().toDouble();
179 return fullWidth > 0.0 ? qCeil(fullWidth / _calcTransectSpacing()) : 1;
180}
181
182void CorridorScanComplexItem::_polylineDirtyChanged(bool dirty)
183{
184 if (dirty) {
185 setDirty(true);
186 }
187}
188
190{
191 int modeAsInt = static_cast<int>(_entryPointLocation);
192
193 if (_calcTransectCount() < 2) {
194 // 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
195 modeAsInt += 2;
196 } else {
197 modeAsInt++;
198 }
199
200 if (modeAsInt > EntryPointStartOppositeEndOppositeSide) {
201 modeAsInt = 0;
202 }
203
204 _entryPointLocation = static_cast<EntryPointLocation>(modeAsInt);
205
207}
208
209void CorridorScanComplexItem::_rebuildCorridorPolygon(void)
210{
211 if (_corridorPolyline.count() < 2) {
212 _surveyAreaPolygon.clear();
213 return;
214 }
215
216 double halfWidth = _corridorWidthFact.rawValue().toDouble() / 2.0;
217
218 QList<QGeoCoordinate> firstSideVertices = _corridorPolyline.offsetPolyline(halfWidth);
219 QList<QGeoCoordinate> secondSideVertices = _corridorPolyline.offsetPolyline(-halfWidth);
220
221 _surveyAreaPolygon.clear();
222
223 QList<QGeoCoordinate> rgCoord;
224 for (const QGeoCoordinate& vertex: firstSideVertices) {
225 rgCoord.append(vertex);
226 }
227 for (int i=secondSideVertices.count() - 1; i >= 0; i--) {
228 rgCoord.append(secondSideVertices[i]);
229 }
231}
232
233void CorridorScanComplexItem::_rebuildTransectsPhase1(void)
234{
235 if (_ignoreRecalc) {
236 return;
237 }
238
239 // If the transects are getting rebuilt then any previsouly loaded mission items are now invalid
241 _loadedMissionItems.clear();
242 _loadedMissionItemsParent->deleteLater();
244 }
245
246 double transectSpacing = _calcTransectSpacing();
247 double fullWidth = _corridorWidthFact.rawValue().toDouble();
248 double halfWidth = fullWidth / 2.0;
249 int transectCount = _calcTransectCount();
250 double normalizedTransectPosition = transectSpacing / 2.0;
251
252 if (_corridorPolyline.count() >= 2) {
253 // First build up the transects all going the same direction
254 //qDebug() << "_rebuildTransectsPhase1";
255 for (int i=0; i<transectCount; i++) {
256 //qDebug() << "start transect";
257 double offsetDistance;
258 if (transectCount == 1) {
259 // Single transect is flown over scan line
260 offsetDistance = 0;
261 } else {
262 // Convert from normalized to absolute transect offset distance
263 offsetDistance = halfWidth - normalizedTransectPosition;
264 }
265
266 // Turn transect into CoordInfo transect
267 QList<TransectStyleComplexItem::CoordInfo_t> transect;
268 QList<QGeoCoordinate> transectCoords = _corridorPolyline.offsetPolyline(offsetDistance);
269 for (int j=1; j<transectCoords.count() - 1; j++) {
270 TransectStyleComplexItem::CoordInfo_t coordInfo = { transectCoords[j], CoordTypeInterior };
271 transect.append(coordInfo);
272 }
273 TransectStyleComplexItem::CoordInfo_t coordInfo = { transectCoords.first(), CoordTypeSurveyEntry };
274 transect.prepend(coordInfo);
275 coordInfo = { transectCoords.last(), CoordTypeSurveyExit };
276 transect.append(coordInfo);
277
278 // Extend the transect ends for turnaround
279 if (_hasTurnaround()) {
280 QGeoCoordinate turnaroundCoord;
281 double turnAroundDistance = _turnAroundDistanceFact.rawValue().toDouble();
282
283 double azimuth = transectCoords[0].azimuthTo(transectCoords[1]);
284 turnaroundCoord = transectCoords[0].atDistanceAndAzimuth(-turnAroundDistance, azimuth);
285 turnaroundCoord.setAltitude(qQNaN());
286 TransectStyleComplexItem::CoordInfo_t turnaroundCoordInfo = { turnaroundCoord, CoordTypeTurnaround };
287 transect.prepend(turnaroundCoordInfo);
288
289 azimuth = transectCoords.last().azimuthTo(transectCoords[transectCoords.count() - 2]);
290 turnaroundCoord = transectCoords.last().atDistanceAndAzimuth(-turnAroundDistance, azimuth);
291 turnaroundCoord.setAltitude(qQNaN());
292 coordInfo = { turnaroundCoord, CoordTypeTurnaround };
293 transect.append(coordInfo);
294 }
295
296#if 0
297 qDebug() << "transect debug";
298 for (const TransectStyleComplexItem::CoordInfo_t& coordInfo: transect) {
299 qDebug() << coordInfo.coordType;
300 }
301#endif
302
303 _transects.append(transect);
304 normalizedTransectPosition += transectSpacing;
305 }
306
307 // Now deal with fixing up the entry point:
308 // 0: Leave alone
309 // 1: Start at same end, opposite side of center
310 // 2: Start at opposite end, same side
311 // 3: Start at opposite end, opposite side
312
313 bool reverseTransects = false;
314 bool reverseVertices = false;
315 switch (_entryPointLocation) {
316 case EntryPointDefaultOrder:
317 reverseTransects = false;
318 reverseVertices = false;
319 break;
320 case EntryPointStartSameEndOppositeSide:
321 reverseTransects = true;
322 reverseVertices = false;
323 break;
324 case EntryPointStartOppositeEndSameSide:
325 reverseTransects = false;
326 reverseVertices = true;
327 break;
328 case EntryPointStartOppositeEndOppositeSide:
329 reverseTransects = true;
330 reverseVertices = true;
331 break;
332 }
333 if (reverseTransects) {
334 QList<QList<TransectStyleComplexItem::CoordInfo_t>> reversedTransects;
335 for (const QList<TransectStyleComplexItem::CoordInfo_t>& transect: _transects) {
336 reversedTransects.prepend(transect);
337 }
338 _transects = reversedTransects;
339 }
340 if (reverseVertices) {
341 for (int i=0; i<_transects.count(); i++) {
342 QList<TransectStyleComplexItem::CoordInfo_t> reversedVertices;
343 for (const TransectStyleComplexItem::CoordInfo_t& vertex: _transects[i]) {
344 reversedVertices.prepend(vertex);
345 }
346 _transects[i] = reversedVertices;
347 }
348 }
349
350 // Adjust to lawnmower pattern
351 reverseVertices = false;
352 for (int i=0; i<_transects.count(); i++) {
353 // We must reverse the vertices for every other transect in order to make a lawnmower pattern
354 QList<TransectStyleComplexItem::CoordInfo_t> transectVertices = _transects[i];
355 if (reverseVertices) {
356 reverseVertices = false;
357 QList<TransectStyleComplexItem::CoordInfo_t> reversedVertices;
358 for (int j=transectVertices.count()-1; j>=0; j--) {
359 reversedVertices.append(transectVertices[j]);
360
361 // as we are flying the transect reversed, we also need to swap entry and exit coordinate types
362 if (reversedVertices.last().coordType == CoordTypeSurveyEntry) {
363 reversedVertices.last().coordType = CoordTypeSurveyExit;
364 } else if (reversedVertices.last().coordType == CoordTypeSurveyExit) {
365 reversedVertices.last().coordType = CoordTypeSurveyEntry;
366 }
367 }
368
369 transectVertices = reversedVertices;
370 } else {
371 reverseVertices = true;
372 }
373 _transects[i] = transectVertices;
374 }
375 }
376}
377
378void CorridorScanComplexItem::_recalcCameraShots(void)
379{
380 double triggerDistance = _cameraCalc.adjustedFootprintFrontal()->rawValue().toDouble();
381 if (triggerDistance == 0) {
382 _cameraShots = 0;
383 } else {
384 if (_cameraTriggerInTurnAroundFact.rawValue().toBool()) {
386 } else {
387 int singleTransectImageCount = qCeil(_corridorPolyline.length() / triggerDistance);
388 _cameraShots = singleTransectImageCount * _calcTransectCount();
389 }
390 }
391 emit cameraShotsChanged();
392}
393
398
400{
401 return _vehicleSpeed == 0 ? 0 : _cameraCalc.adjustedFootprintFrontal()->rawValue().toDouble() / _vehicleSpeed;
402}
403
404double CorridorScanComplexItem::_calcTransectSpacing(void) const
405{
406 double transectSpacing = _cameraCalc.adjustedFootprintSide()->rawValue().toDouble();
407 if (transectSpacing < _minimumTransectSpacingMeters) {
408 // We can't let spacing get too small otherwise we will end up with too many transects.
409 // So we limit the spacing to be above a small increment and below that value we set to huge spacing
410 // which will cause a single transect to be added instead of having things blow up.
411 transectSpacing = _forceLargeTransectSpacingMeters;
412 }
413
414 return transectSpacing;
415}
416
417void CorridorScanComplexItem::_updateWizardMode(void)
418{
419 if (_corridorPolyline.isValid() && !_corridorPolyline.traceMode()) {
420 setWizardMode(false);
421 }
422}
#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)
static constexpr const char * jsonComplexItemTypeValue
ReadyForSaveState readyForSaveState(void) const final
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.
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
static constexpr double _minimumTransectSpacingMeters
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
ReadyForSaveState readyForSaveState(void) const override
bool _load(const QJsonObject &complexObject, bool forPresets, QString &errorString)
static constexpr double _forceLargeTransectSpacingMeters
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
constexpr const char * jsonVersionKey
Definition JsonHelper.h:109
bool validateKeys(const QJsonObject &jsonObject, const QList< KeyValidateInfo > &keyInfo, QString &errorString)