• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MerginMaps / input / 6336876697

28 Sep 2023 09:10AM UTC coverage: 62.146% (+0.3%) from 61.875%
6336876697

Pull #2813

github

PeterPetrik
fix std::bad_function in tests
Pull Request #2813: add missing CRS to project load errors

7616 of 12255 relevant lines covered (62.15%)

102.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.87
/app/activeproject.cpp
1
/***************************************************************************
2
 *                                                                         *
3
 *   This program is free software; you can redistribute it and/or modify  *
4
 *   it under the terms of the GNU General Public License as published by  *
5
 *   the Free Software Foundation; either version 2 of the License, or     *
6
 *   (at your option) any later version.                                   *
7
 *                                                                         *
8
 ***************************************************************************/
9

10
#include <QDebug>
11
#include <QStandardPaths>
12
#include <QTimer>
13

14
#include "qgsvectorlayer.h"
15
#include "qgslayertree.h"
16
#include "qgslayertreemodel.h"
17
#include "qgslayertreelayer.h"
18
#include "qgslayertreegroup.h"
19
#include "qgsmapthemecollection.h"
20

21
#include "activeproject.h"
22
#include "coreutils.h"
23

24
#ifdef ANDROID
25
#include "position/tracking/androidtrackingbroadcast.h"
26
#endif
27

28
const QString ActiveProject::LOADING_FLAG_FILE_PATH = QString( "%1/.input_loading_project" ).arg( QStandardPaths::standardLocations( QStandardPaths::TempLocation ).first() );
29

30
ActiveProject::ActiveProject( AppSettings &appSettings
21✔
31
                              , ActiveLayer &activeLayer
32
                              , LayersProxyModel &recordingLayerPM
33
                              , LocalProjectsManager &localProjectsManager
34
                              , QObject *parent ) :
21✔
35

36
  QObject( parent )
37
  , mAppSettings( appSettings )
21✔
38
  , mActiveLayer( activeLayer )
21✔
39
  , mRecordingLayerPM( recordingLayerPM )
21✔
40
  , mLocalProjectsManager( localProjectsManager )
21✔
41
  , mProjectLoadingLog( "" )
21✔
42
{
43
  // we used to have our own QgsProject instance, but unfortunately few pieces of qgis_core
44
  // still work with QgsProject::instance() singleton hardcoded (e.g. vector layer's feature
45
  // iterator uses it for virtual fields, causing minor bugs with expressions)
46
  // so for the time being let's just stick to using the singleton until qgis_core is completely fixed
47
  mQgsProject = QgsProject::instance();
21✔
48

49
  // listen to local project removal event to invalidate mProject
50
  QObject::connect(
21✔
51
    &mLocalProjectsManager,
21✔
52
    &LocalProjectsManager::aboutToRemoveLocalProject,
53
    this, [this]( const LocalProject & project )
51✔
54
  {
55
    if ( project.id() == mLocalProject.id() )
50✔
56
    {
57
      load( QLatin1String() );
1✔
58
    }
59
  } );
50✔
60

61
  // listen to metadata changes of opened LocalProject (e.g. local version update or namespace update)
62
  QObject::connect(
21✔
63
    &mLocalProjectsManager,
21✔
64
    &LocalProjectsManager::localProjectDataChanged,
65
    this, [this]( const LocalProject & project )
122✔
66
  {
67
    if ( project.projectDir == mLocalProject.projectDir )
122✔
68
    {
69
      mLocalProject = project;
×
70
      emit localProjectChanged( mLocalProject );
×
71
    }
72
  } );
122✔
73

74
  setAutosyncEnabled( mAppSettings.autosyncAllowed() );
21✔
75

76
  QObject::connect( &mAppSettings, &AppSettings::autosyncAllowedChanged, this, &ActiveProject::setAutosyncEnabled );
21✔
77
}
21✔
78

79
ActiveProject::~ActiveProject() = default;
21✔
80

81
QgsProject *ActiveProject::qgsProject()
1✔
82
{
83
  return mQgsProject;
1✔
84
}
85

86
LocalProject ActiveProject::localProject()
2✔
87
{
88
  return mLocalProject;
2✔
89
}
90

91
QString ActiveProject::projectFullName() const
55✔
92
{
93
  return mLocalProject.fullName();
55✔
94
}
95

96
bool ActiveProject::load( const QString &filePath )
6✔
97
{
98
  return forceLoad( filePath, false );
6✔
99
}
100

101
bool ActiveProject::forceLoad( const QString &filePath, bool force )
6✔
102
{
103
  CoreUtils::log( QStringLiteral( "Project loading" ), filePath + " " + ( force ? "true" : "false" ) );
12✔
104

105
  // clear autosync
106
  setAutosyncEnabled( false );
6✔
107

108
  // clear position tracking broadcast listeners
109
#ifdef ANDROID
110
  disconnect( &AndroidTrackingBroadcast::getInstance() );
111
  AndroidTrackingBroadcast::unregisterBroadcast();
112
#endif
113

114
  // Just clear project if empty
115
  if ( filePath.isEmpty() )
6✔
116
  {
117
    emit projectWillBeReloaded();
1✔
118

119
    whileBlocking( &mActiveLayer )->resetActiveLayer();
1✔
120
    mLocalProject = LocalProject();
1✔
121
    mQgsProject->clear();
1✔
122

123
    emit localProjectChanged( mLocalProject );
1✔
124
    emit projectReloaded( mQgsProject );
1✔
125

126
    return true;
1✔
127
  }
128

129
  if ( !force )
5✔
130
    emit loadingStarted();
5✔
131

132
  QFile flagFile( LOADING_FLAG_FILE_PATH );
5✔
133
  flagFile.open( QIODevice::WriteOnly );
5✔
134
  flagFile.close();
5✔
135

136
  mProjectLoadingLog.clear();
5✔
137

138
  QString logFilePath = CoreUtils::logFilename();
5✔
139
  qint64 alreadyAppendedCharsCount = 0;
5✔
140

141
  {
142
    QFile file( logFilePath );
5✔
143
    alreadyAppendedCharsCount = file.size();
5✔
144
  }
5✔
145

146
  // Give some time to other (GUI) processes before loading a project in the main thread
147
  QEventLoop loop;
5✔
148
  QTimer t;
5✔
149
  t.connect( &t, &QTimer::timeout, &loop, &QEventLoop::quit );
5✔
150
  t.start( 10 );
5✔
151
  loop.exec();
5✔
152

153
  bool res = true;
5✔
154
  if ( mQgsProject->fileName() != filePath || force )
5✔
155
  {
156
    emit projectWillBeReloaded();
5✔
157
    mActiveLayer.resetActiveLayer();
5✔
158

159
    res = mQgsProject->read( filePath );
5✔
160
    if ( !res )
5✔
161
    {
162
      QString error = mQgsProject->error();
1✔
163
      CoreUtils::log( QStringLiteral( "Project loading" ), QStringLiteral( "Could not read project file: " ) + error );
2✔
164

165
      mLocalProject = LocalProject();
1✔
166
      if ( mMapSettings )
1✔
167
      {
168
        QList< QgsMapLayer * > layers;
×
169
        mMapSettings->setLayers( layers );
×
170
      }
×
171
      mQgsProject->clear();
1✔
172

173
      emit localProjectChanged( mLocalProject );
1✔
174
      emit projectReloaded( mQgsProject );
1✔
175

176
      emit projectReadingFailed( error );
1✔
177
      return res;
1✔
178
    }
1✔
179

180
    mLocalProject = mLocalProjectsManager.projectFromProjectFilePath( filePath );
4✔
181

182
    if ( !mLocalProject.isValid() )
4✔
183
    {
184
      CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Could not find project in local projects: " ) + filePath );
×
185
    }
186

187
    updateMapTheme();
4✔
188
    updateRecordingLayers();
4✔
189
    updateActiveLayer();
4✔
190
    updateMapSettingsLayers();
4✔
191

192
    emit localProjectChanged( mLocalProject );
4✔
193
    emit projectReloaded( mQgsProject );
4✔
194
    emit positionTrackingSupportedChanged();
4✔
195
  }
196

197
  bool foundErrorsInLoadedProject = validateProject();
4✔
198

199
  flagFile.remove();
4✔
200
  if ( !force )
4✔
201
  {
202
    emit loadingFinished();
4✔
203

204
    if ( foundErrorsInLoadedProject )
4✔
205
    {
206
      QFile file( logFilePath );
1✔
207
      if ( file.open( QIODevice::ReadOnly ) )
1✔
208
      {
209
        file.seek( alreadyAppendedCharsCount );
1✔
210
        QByteArray neededLogFileData = file.readAll();
1✔
211
        mProjectLoadingLog = QString::fromStdString( neededLogFileData.toStdString() );
1✔
212
        file.close();
1✔
213
      }
1✔
214
      emit qgisLogChanged();
1✔
215
      emit loadingErrorFound();
1✔
216
    }
1✔
217
  }
218

219
  if ( mAppSettings.autosyncAllowed() )
4✔
220
  {
221
    setAutosyncEnabled( true );
1✔
222
  }
223

224
  // in case tracking is running, we want to show the UI
225
#ifdef ANDROID
226
  if ( positionTrackingSupported() )
227
  {
228
    connect(
229
      &AndroidTrackingBroadcast::getInstance(),
230
      &AndroidTrackingBroadcast::aliveResponse,
231
      this,
232
      [this]( bool isAlive )
233
    {
234
      if ( isAlive )
235
      {
236
        emit startPositionTracking();
237
      }
238
    },
239
    Qt::SingleShotConnection
240
    );
241

242
    // note: order matters in the following calls
243
    AndroidTrackingBroadcast::registerBroadcast();
244
    AndroidTrackingBroadcast::sendAliveRequestAsync();
245
  }
246
#endif
247

248
  return res;
4✔
249
}
5✔
250

251
bool ActiveProject::validateProject()
4✔
252
{
253
  Q_ASSERT( mQgsProject );
4✔
254

255
  bool errorsFound = false;
4✔
256

257
  // A. Per project validations
258
  // A.1. Project CRS
259
  if ( !mQgsProject->crs().isValid() )
4✔
260
  {
261
    errorsFound = true;
1✔
262
    CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Invalid canvas CRS" ) );
2✔
263
    emit reportIssue( tr( "General" ), tr( "Project has invalid CRS assigned. Map and tools have undefined behaviour!" ) );
1✔
264
  }
265

266
  // B. Per-Layer validations
267
  QMap<QString, QgsMapLayer *> projectLayers = mQgsProject->mapLayers();
4✔
268
  for ( QgsMapLayer *layer : projectLayers )
25✔
269
  {
270
    // B.1. Layer Validity
271
    if ( !layer->isValid() )
21✔
272
    {
273
      errorsFound = true;
1✔
274
      CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Invalid layer %1" ).arg( layer->name() ) );
2✔
275
      emit reportIssue( tr( "Layer" ) + ": " + layer->name(), tr( "Unable to load source " ) + ": " + layer->publicSource() );
1✔
276
    }
277
    else
278
    {
279
      // B.2. Layer CRS
280
      if ( layer->isSpatial() && !layer->crs().isValid() )
20✔
281
      {
282
        errorsFound = true;
1✔
283
        CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Invalid layer CRS %1" ).arg( layer->name() ) );
2✔
284
        emit reportIssue( tr( "Layer" ) + ": " + layer->name(), tr( "Layer has invalid CRS assigned. Recording tools have undefined behaviour." ) );
1✔
285
      }
286
    }
287
  }
288

289
  return errorsFound;
4✔
290
}
4✔
291

292
bool ActiveProject::reloadProject( QString projectDir )
×
293
{
294
  if ( mQgsProject->homePath() == projectDir )
×
295
  {
296
    // store current project extent
297
    QgsRectangle extent;
×
298
    if ( mMapSettings )
×
299
      extent = mMapSettings->extent();
×
300

301
    bool result = forceLoad( mQgsProject->fileName(), true );
×
302

303
    // restore extent
304
    if ( mMapSettings && !extent.isNull() )
×
305
      mMapSettings->setExtent( extent );
×
306

307
    return result;
×
308
  }
309
  return false;
×
310
}
311

312
void ActiveProject::setAutosyncEnabled( bool enabled )
30✔
313
{
314
  if ( enabled )
30✔
315
  {
316
    if ( mAutosyncController )
2✔
317
    {
318
      mAutosyncController->disconnect();
×
319
      mAutosyncController.reset();
×
320
    }
321

322
    mAutosyncController = std::make_unique<AutosyncController>( mQgsProject );
2✔
323

324
    connect( mAutosyncController.get(), &AutosyncController::projectChangeDetected, this, &ActiveProject::requestSync );
2✔
325
  }
326
  else
327
  {
328
    if ( mAutosyncController )
28✔
329
    {
330
      mAutosyncController->disconnect();
2✔
331
    }
332
    mAutosyncController.reset();
28✔
333
  }
334
}
30✔
335

336
void ActiveProject::requestSync()
1✔
337
{
338
  emit syncActiveProject( mLocalProject );
1✔
339
}
1✔
340

341
void ActiveProject::setMapSettings( InputMapSettings *mapSettings )
×
342
{
343
  if ( mMapSettings == mapSettings )
×
344
    return;
×
345

346
  mMapSettings = mapSettings;
×
347
  updateMapSettingsLayers();
×
348

349
  emit mapSettingsChanged();
×
350
}
351

352
void ActiveProject::updateMapSettingsLayers() const
4✔
353
{
354
  if ( !mQgsProject || !mMapSettings ) return;
4✔
355

356
  QgsLayerTree *root = mQgsProject->layerTreeRoot();
×
357

358
  // Get list of all visible and valid layers in the project
359
  QList< QgsMapLayer * > visibleLayers;
×
360
  foreach ( QgsLayerTreeLayer *nodeLayer, root->findLayers() )
×
361
  {
362
    if ( nodeLayer->isVisible() )
×
363
    {
364
      QgsMapLayer *layer = nodeLayer->layer();
×
365
      if ( layer && layer->isValid() )
×
366
      {
367
        visibleLayers << layer;
×
368
      }
369
    }
370
  }
×
371

372
  mMapSettings->setLayers( visibleLayers );
×
373
  mMapSettings->setTransformContext( mQgsProject->transformContext() );
×
374
}
×
375

376
InputMapSettings *ActiveProject::mapSettings() const
×
377
{
378
  return mMapSettings;
×
379
}
380

381
AutosyncController *ActiveProject::autosyncController() const
2✔
382
{
383
  if ( mAutosyncController )
2✔
384
  {
385
    return mAutosyncController.get();
1✔
386
  }
387

388
  return nullptr;
1✔
389
}
390

391
bool ActiveProject::layerVisible( QgsMapLayer *layer )
4✔
392
{
393
  if ( !mQgsProject || !layer )
4✔
394
  {
395
    return false;
4✔
396
  }
397

398
  QgsLayerTree *root = mQgsProject->layerTreeRoot();
×
399

400
  if ( !root )
×
401
  {
402
    return false;
×
403
  }
404

405
  QgsLayerTreeLayer *layerTree = root->findLayer( layer );
×
406

407
  if ( !layerTree )
×
408
  {
409
    return false;
×
410
  }
411

412
  return layerTree->isVisible();
×
413
}
414

415
QString ActiveProject::projectLoadingLog() const
×
416
{
417
  return mProjectLoadingLog;
×
418
}
419

420
void ActiveProject::updateMapTheme()
4✔
421
{
422
  if ( !mQgsProject )
4✔
423
  {
424
    return;
×
425
  }
426

427
  QgsLayerTree *root = mQgsProject->layerTreeRoot();
4✔
428
  QgsMapThemeCollection *collection = mQgsProject->mapThemeCollection();
4✔
429

430
  if ( !root || !collection )
4✔
431
  {
432
    return;
×
433
  }
434

435
  QgsLayerTreeModel model( root );
4✔
436

437
  QString themeCandidateName;
4✔
438
  QStringList mapThemes = collection->mapThemes();
4✔
439

440
  QgsMapThemeCollection::MapThemeRecord themeCandidate = collection->createThemeFromCurrentState( root, &model );
4✔
441

442
  for ( const QString &themeName : mapThemes )
4✔
443
  {
444
    QgsMapThemeCollection::MapThemeRecord themeIterator = collection->mapThemeState( themeName );
×
445

446
    if ( themeCandidate == themeIterator )
×
447
    {
448
      themeCandidateName = themeName;
×
449
      break;
×
450
    }
451
  }
×
452

453
  setMapTheme( themeCandidateName );
4✔
454
}
4✔
455

456
void ActiveProject::setMapTheme( const QString &themeName )
4✔
457
{
458
  if ( mMapTheme == themeName )
4✔
459
  {
460
    return;
4✔
461
  }
462

463
  if ( !mQgsProject )
×
464
  {
465
    return;
×
466
  }
467

468
  mMapTheme = themeName;
×
469

470
  if ( !mMapTheme.isEmpty() )
×
471
  {
472
    QgsLayerTree *root = mQgsProject->layerTreeRoot();
×
473
    QgsMapThemeCollection *collection = mQgsProject->mapThemeCollection();
×
474

475
    if ( !root || !collection )
×
476
    {
477
      return;
×
478
    }
479

480
    QgsLayerTreeModel model( root );
×
481
    collection->applyTheme( mMapTheme, root, &model );
×
482
  }
×
483

484
  emit mapThemeChanged( mMapTheme );
×
485

486
  updateRecordingLayers(); // <- worth to decouple similar to map themes model decoupling
×
487
  updateActiveLayer();
×
488
  updateMapSettingsLayers();
×
489
}
490

491
void ActiveProject::updateActiveLayer()
4✔
492
{
493
  if ( !layerVisible( mActiveLayer.layer() ) )
4✔
494
  {
495
    QgsMapLayer *defaultLayer = mRecordingLayerPM.layerFromLayerName( mAppSettings.defaultLayer() );
4✔
496

497
    if ( !defaultLayer )
4✔
498
    {
499
      defaultLayer = mRecordingLayerPM.firstUsableLayer();
4✔
500
    }
501

502
    setActiveLayer( defaultLayer );
4✔
503
  }
504
}
4✔
505

506
void ActiveProject::updateRecordingLayers()
4✔
507
{
508
  mRecordingLayerPM.refreshData();
4✔
509
}
4✔
510

511
bool ActiveProject::isProjectLoaded() const
2✔
512
{
513
  return mQgsProject && !mQgsProject->fileName().isEmpty();
2✔
514
}
515

516
void ActiveProject::setActiveLayer( QgsMapLayer *layer ) const
4✔
517
{
518
  if ( !layer || !layer->isValid() )
4✔
519
  {
520
    mActiveLayer.resetActiveLayer();
×
521
  }
522
  else
523
  {
524
    mActiveLayer.setActiveLayer( layer );
4✔
525
    mAppSettings.setDefaultLayer( mActiveLayer.layerName() );
4✔
526
  }
527
}
4✔
528

529
void ActiveProject::switchLayerTreeNodeVisibility( QgsLayerTreeNode *node )
×
530
{
531
  if ( !node )
×
532
    return;
×
533

534
  node->setItemVisibilityChecked( !node->isVisible() );
×
535

536
  updateMapTheme();
×
537
  updateRecordingLayers(); // <- worth to decouple similar to map themes model decoupling
×
538
  updateActiveLayer();
×
539
  updateMapSettingsLayers();
×
540
}
541

542
const QString &ActiveProject::mapTheme() const
×
543
{
544
  return mMapTheme;
×
545
}
546

547
bool ActiveProject::positionTrackingSupported() const
2✔
548
{
549
  if ( !isProjectLoaded() )
2✔
550
  {
551
    return false;
×
552
  }
553

554
  return mQgsProject->readBoolEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PositionTracking/Enabled" ), false );
6✔
555
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc