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

MerginMaps / input / 6033717279

31 Aug 2023 06:12AM UTC coverage: 62.1% (+0.04%) from 62.056%
6033717279

Pull #2779

github

PeterPetrik
fix mapcanvas memory leak
Pull Request #2779: fix mapcanvas memory leak

7560 of 12174 relevant lines covered (62.1%)

101.99 hits per line

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

48.71
/app/map/inputmapcanvasmap.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
/*
11
 * The source code forked from https://github.com/qgis/QGIS on 25th Nov 2022
12
 * File: qgsquickmapcanvasmap.cpp by (C) 2014 by Matthias Kuhn
13
 */
14

15
#include <QQuickWindow>
16
#include <QSGSimpleTextureNode>
17
#include <QScreen>
18

19
#include "qgis.h"
20
#include "qgsexpressioncontextutils.h"
21
#include "qgsmaplayertemporalproperties.h"
22
#include "qgsmaprenderercache.h"
23
#include "qgsmaprendererparalleljob.h"
24
#include "qgsmessagelog.h"
25
#include "qgspallabeling.h"
26
#include "qgsproject.h"
27
#include "qgsannotationlayer.h"
28
#include "qgsvectorlayer.h"
29
#include "qgslabelingresults.h"
30

31
#include "inputmapcanvasmap.h"
32
#include "inputmapsettings.h"
33

34

35
InputMapCanvasMap::InputMapCanvasMap( QQuickItem *parent )
24✔
36
  : QQuickItem( parent )
37
  , mMapSettings( std::make_unique<InputMapSettings>() )
24✔
38
  , mCache( std::make_unique<QgsMapRendererCache>() )
24✔
39
{
40
  connect( this, &QQuickItem::windowChanged, this, &InputMapCanvasMap::onWindowChanged );
24✔
41
  connect( &mRefreshTimer, &QTimer::timeout, this, [ = ] { refreshMap(); } );
26✔
42
  connect( &mMapUpdateTimer, &QTimer::timeout, this, &InputMapCanvasMap::renderJobUpdated );
24✔
43

44
  connect( mMapSettings.get(), &InputMapSettings::extentChanged, this, &InputMapCanvasMap::onExtentChanged );
24✔
45
  connect( mMapSettings.get(), &InputMapSettings::layersChanged, this, &InputMapCanvasMap::onLayersChanged );
24✔
46
  connect( mMapSettings.get(), &InputMapSettings::temporalStateChanged, this, &InputMapCanvasMap::onTemporalStateChanged );
24✔
47

48
  connect( this, &InputMapCanvasMap::renderStarting, this, &InputMapCanvasMap::isRenderingChanged );
24✔
49
  connect( this, &InputMapCanvasMap::mapCanvasRefreshed, this, &InputMapCanvasMap::isRenderingChanged );
24✔
50

51
  mMapUpdateTimer.setSingleShot( false );
24✔
52
  mMapUpdateTimer.setInterval( 250 );
24✔
53
  mRefreshTimer.setSingleShot( true );
24✔
54
  setTransformOrigin( QQuickItem::TopLeft );
24✔
55
  setFlags( QQuickItem::ItemHasContents );
24✔
56
}
24✔
57

58
InputMapCanvasMap::~InputMapCanvasMap()
24✔
59
{
60
  stopRendering();
24✔
61
}
24✔
62

63
InputMapSettings *InputMapCanvasMap::mapSettings() const
24✔
64
{
65
  return mMapSettings.get();
24✔
66
}
67

68
void InputMapCanvasMap::zoom( QPointF center, qreal scale )
1✔
69
{
70
  QgsRectangle extent = mMapSettings->extent();
1✔
71
  QgsPoint oldCenter( extent.center() );
1✔
72
  QgsPoint mousePos( mMapSettings->screenToCoordinate( center ) );
1✔
73

74
  QgsPointXY newCenter( mousePos.x() + ( ( oldCenter.x() - mousePos.x() ) * scale ),
1✔
75
                        mousePos.y() + ( ( oldCenter.y() - mousePos.y() ) * scale ) );
2✔
76

77
  // same as zoomWithCenter (no coordinate transformations are needed)
78
  extent.scale( scale, &newCenter );
1✔
79
  mMapSettings->setExtent( extent );
1✔
80
}
1✔
81

82
void InputMapCanvasMap::pan( QPointF oldPos, QPointF newPos )
×
83
{
84
  QgsPoint start = mMapSettings->screenToCoordinate( oldPos.toPoint() );
×
85
  QgsPoint end = mMapSettings->screenToCoordinate( newPos.toPoint() );
×
86

87
  double dx = end.x() - start.x();
×
88
  double dy = end.y() - start.y();
×
89

90
  // modify the extent
91
  QgsRectangle extent = mMapSettings->extent();
×
92

93
  extent.setXMinimum( extent.xMinimum() + dx );
×
94
  extent.setXMaximum( extent.xMaximum() + dx );
×
95
  extent.setYMaximum( extent.yMaximum() + dy );
×
96
  extent.setYMinimum( extent.yMinimum() + dy );
×
97

98
  mMapSettings->setExtent( extent );
×
99
}
×
100

101
void InputMapCanvasMap::refreshMap()
2✔
102
{
103
  stopRendering(); // if any...
2✔
104

105
  QgsMapSettings mapSettings = mMapSettings->mapSettings();
2✔
106
  if ( !mapSettings.hasValidSettings() )
2✔
107
    return;
×
108

109
  //build the expression context
110
  QgsExpressionContext expressionContext;
2✔
111
  expressionContext << QgsExpressionContextUtils::globalScope()
2✔
112
                    << QgsExpressionContextUtils::mapSettingsScope( mapSettings );
2✔
113

114
  QgsProject *project = mMapSettings->project();
2✔
115
  if ( project )
2✔
116
  {
117
    expressionContext << QgsExpressionContextUtils::projectScope( project );
×
118

119
    mapSettings.setLabelingEngineSettings( project->labelingEngineSettings() );
×
120

121
    // render main annotation layer above all other layers
122
    QList<QgsMapLayer *> allLayers = mapSettings.layers();
×
123
    allLayers.insert( 0, project->mainAnnotationLayer() );
×
124
    mapSettings.setLayers( allLayers );
×
125
  }
×
126

127
  mapSettings.setExpressionContext( expressionContext );
2✔
128

129
  // enables on-the-fly simplification of geometries to spend less time rendering
130
  mapSettings.setFlag( Qgis::MapSettingsFlag::UseRenderingOptimization );
2✔
131
  // with incremental rendering - enables updates of partially rendered layers (good for WMTS, XYZ layers)
132
  mapSettings.setFlag( Qgis::MapSettingsFlag::RenderPartialOutput, mIncrementalRendering );
2✔
133

134
  // create the renderer job
135
  Q_ASSERT( !mJob );
2✔
136
  mJob = new QgsMapRendererParallelJob( mapSettings );
2✔
137

138
  if ( mIncrementalRendering )
2✔
139
    mMapUpdateTimer.start();
×
140

141
  connect( mJob, &QgsMapRendererJob::renderingLayersFinished, this, &InputMapCanvasMap::renderJobUpdated );
2✔
142
  connect( mJob, &QgsMapRendererJob::finished, this, &InputMapCanvasMap::renderJobFinished );
2✔
143
  mJob->setCache( mCache.get() );
2✔
144

145
  mJob->start();
2✔
146

147
  if ( !mSilentRefresh )
2✔
148
  {
149
    emit renderStarting();
2✔
150
  }
151
}
2✔
152

153
void InputMapCanvasMap::renderJobUpdated()
2✔
154
{
155
  if ( !mJob )
2✔
156
    return;
×
157

158
  mImage = mJob->renderedImage();
2✔
159
  mImageMapSettings = mJob->mapSettings();
2✔
160
  mDirty = true;
2✔
161
  // Temporarily freeze the canvas, we only need to reset the geometry but not trigger a repaint
162
  bool freeze = mFreeze;
2✔
163
  mFreeze = true;
2✔
164
  updateTransform();
2✔
165
  mFreeze = freeze;
2✔
166

167
  update();
2✔
168
}
169

170
void InputMapCanvasMap::renderJobFinished()
2✔
171
{
172
  if ( !mJob )
2✔
173
    return;
×
174

175
  const QgsMapRendererJob::Errors errors = mJob->errors();
2✔
176
  for ( const QgsMapRendererJob::Error &error : errors )
2✔
177
  {
178
    QgsMessageLog::logMessage( QStringLiteral( "%1 :: %2" ).arg( error.layerID, error.message ), QStringLiteral( "Rendering" ) );
×
179
  }
180

181
  // take labeling results before emitting renderComplete, so labeling map tools
182
  // connected to signal work with correct results
183
  delete mLabelingResults;
2✔
184
  mLabelingResults = mJob->takeLabelingResults();
2✔
185

186
  mImage = mJob->renderedImage();
2✔
187
  mImageMapSettings = mJob->mapSettings();
2✔
188

189
  // now we are in a slot called from mJob - do not delete it immediately
190
  // so the class is still valid when the execution returns to the class
191
  mJob->deleteLater();
2✔
192
  mJob = nullptr;
2✔
193
  mDirty = true;
2✔
194
  mMapUpdateTimer.stop();
2✔
195

196
  // Temporarily freeze the canvas, we only need to reset the geometry but not trigger a repaint
197
  bool freeze = mFreeze;
2✔
198
  mFreeze = true;
2✔
199
  updateTransform();
2✔
200
  mFreeze = freeze;
2✔
201

202
  update();
2✔
203
  if ( !mSilentRefresh )
2✔
204
  {
205
    emit mapCanvasRefreshed();
2✔
206
  }
207
  else
208
  {
209
    mSilentRefresh = false;
×
210
  }
211

212
  if ( mDeferredRefreshPending )
2✔
213
  {
214
    mDeferredRefreshPending = false;
×
215
    mSilentRefresh = true;
×
216
    refresh();
×
217
  }
218
}
2✔
219

220
void InputMapCanvasMap::layerRepaintRequested( bool deferred )
16✔
221
{
222
  if ( mMapSettings->outputSize().isNull() )
16✔
223
    return; // the map image size has not been set yet
×
224

225
  if ( !mFreeze )
16✔
226
  {
227
    if ( deferred )
16✔
228
    {
229
      if ( !mJob )
×
230
      {
231
        mSilentRefresh = true;
×
232
        refresh();
×
233
      }
234
      else
235
      {
236
        mDeferredRefreshPending = true;
×
237
      }
238
    }
239
    else
240
    {
241
      refresh();
16✔
242
    }
243
  }
244
}
245

246
void InputMapCanvasMap::onWindowChanged( QQuickWindow *window )
×
247
{
248
  if ( mWindow == window )
×
249
    return;
×
250

251
  if ( mWindow )
×
252
    disconnect( mWindow, &QQuickWindow::screenChanged, this, &InputMapCanvasMap::onScreenChanged );
×
253

254
  if ( window )
×
255
  {
256
    connect( window, &QQuickWindow::screenChanged, this, &InputMapCanvasMap::onScreenChanged );
×
257
    onScreenChanged( window->screen() );
×
258
  }
259

260
  mWindow = window;
×
261
}
262

263
void InputMapCanvasMap::onScreenChanged( QScreen *screen )
×
264
{
265
  if ( screen )
×
266
  {
267
    if ( screen->devicePixelRatio() > 0 )
×
268
    {
269
      mMapSettings->setDevicePixelRatio( screen->devicePixelRatio() );
×
270
    }
271
    mMapSettings->setOutputDpi( screen->physicalDotsPerInch() );
×
272
  }
273
}
×
274

275
void InputMapCanvasMap::onExtentChanged()
45✔
276
{
277
  updateTransform();
45✔
278

279
  // And trigger a new rendering job
280
  refresh();
45✔
281
}
45✔
282

283

284
void InputMapCanvasMap::onTemporalStateChanged()
×
285
{
286
  clearTemporalCache();
×
287

288
  // And trigger a new rendering job
289
  refresh();
×
290
}
×
291

292
void InputMapCanvasMap::updateTransform()
49✔
293
{
294
  QgsRectangle imageExtent = mImageMapSettings.visibleExtent();
49✔
295
  QgsRectangle newExtent = mMapSettings->mapSettings().visibleExtent();
49✔
296
  setScale( imageExtent.width() / newExtent.width() );
49✔
297

298
  QgsPointXY pixelPt = mMapSettings->coordinateToScreen( QgsPoint( imageExtent.xMinimum(), imageExtent.yMaximum() ) );
49✔
299
  setX( pixelPt.x() );
49✔
300
  setY( pixelPt.y() );
49✔
301
}
49✔
302

303
int InputMapCanvasMap::mapUpdateInterval() const
×
304
{
305
  return mMapUpdateTimer.interval();
×
306
}
307

308
void InputMapCanvasMap::setMapUpdateInterval( int mapUpdateInterval )
×
309
{
310
  if ( mMapUpdateTimer.interval() == mapUpdateInterval )
×
311
    return;
×
312

313
  mMapUpdateTimer.setInterval( mapUpdateInterval );
×
314

315
  emit mapUpdateIntervalChanged();
×
316
}
317

318
bool InputMapCanvasMap::incrementalRendering() const
×
319
{
320
  return mIncrementalRendering;
×
321
}
322

323
void InputMapCanvasMap::setIncrementalRendering( bool incrementalRendering )
×
324
{
325
  if ( incrementalRendering == mIncrementalRendering )
×
326
    return;
×
327

328
  mIncrementalRendering = incrementalRendering;
×
329
  emit incrementalRenderingChanged();
×
330
}
331

332
bool InputMapCanvasMap::freeze() const
×
333
{
334
  return mFreeze;
×
335
}
336

337
void InputMapCanvasMap::setFreeze( bool freeze )
×
338
{
339
  if ( freeze == mFreeze )
×
340
    return;
×
341

342
  mFreeze = freeze;
×
343

344
  if ( mFreeze )
×
345
    stopRendering();
×
346
  else
347
    refresh();
×
348

349
  emit freezeChanged();
×
350
}
351

352
bool InputMapCanvasMap::isRendering() const
×
353
{
354
  return mJob;
×
355
}
356

357
QSGNode *InputMapCanvasMap::updatePaintNode( QSGNode *oldNode, QQuickItem::UpdatePaintNodeData * )
×
358
{
359
  if ( mDirty )
×
360
  {
361
    delete oldNode;
×
362
    oldNode = nullptr;
×
363
    mDirty = false;
×
364
  }
365

366
  QSGSimpleTextureNode *node = static_cast<QSGSimpleTextureNode *>( oldNode );
×
367
  if ( !node )
×
368
  {
369
    node = new QSGSimpleTextureNode();
×
370
    QSGTexture *texture = window()->createTextureFromImage( mImage );
×
371
    node->setTexture( texture );
×
372
    node->setOwnsTexture( true );
×
373
  }
374

375
  QRectF rect( boundingRect() );
×
376
  QSizeF size = mImage.size();
×
377
  if ( !size.isEmpty() )
×
378
    size /= mMapSettings->devicePixelRatio();
×
379

380
  // Check for resizes that change the w/h ratio
381
  if ( !rect.isEmpty() && !size.isEmpty() && !qgsDoubleNear( rect.width() / rect.height(), ( size.width() ) / static_cast<double>( size.height() ), 3 ) )
×
382
  {
383
    if ( qgsDoubleNear( rect.height(), mImage.height() ) )
×
384
    {
385
      rect.setHeight( rect.width() / size.width() * size.height() );
×
386
    }
387
    else
388
    {
389
      rect.setWidth( rect.height() / size.height() * size.width() );
×
390
    }
391
  }
392

393
  node->setRect( rect );
×
394

395
  return node;
×
396
}
397

398
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
399
void InputMapCanvasMap::geometryChanged( const QRectF &newGeometry, const QRectF &oldGeometry )
400
{
401
  QQuickItem::geometryChanged( newGeometry, oldGeometry );
402
#else
403
void InputMapCanvasMap::geometryChange( const QRectF &newGeometry, const QRectF &oldGeometry )
72✔
404
{
405
  QQuickItem::geometryChange( newGeometry, oldGeometry );
72✔
406
#endif
407
  if ( newGeometry.size() != oldGeometry.size() )
72✔
408
  {
409
    mMapSettings->setOutputSize( newGeometry.size().toSize() );
×
410
    refresh();
×
411
  }
412
}
72✔
413

414
void InputMapCanvasMap::onLayersChanged()
23✔
415
{
416
  if ( mMapSettings->extent().isEmpty() )
23✔
417
    zoomToFullExtent();
19✔
418

419
  for ( const QMetaObject::Connection &conn : std::as_const( mLayerConnections ) )
23✔
420
  {
421
    disconnect( conn );
×
422
  }
423
  mLayerConnections.clear();
23✔
424

425
  const QList<QgsMapLayer *> layers = mMapSettings->layers();
23✔
426
  for ( QgsMapLayer *layer : layers )
160✔
427
  {
428
    mLayerConnections << connect( layer, &QgsMapLayer::repaintRequested, this, &InputMapCanvasMap::layerRepaintRequested );
137✔
429
  }
430

431
  refresh();
23✔
432
}
23✔
433

434
void InputMapCanvasMap::destroyJob( QgsMapRendererJob *job )
×
435
{
436
  job->cancel();
×
437
  job->deleteLater();
×
438
}
×
439

440
void InputMapCanvasMap::stopRendering()
26✔
441
{
442
  if ( mJob )
26✔
443
  {
444
    mMapUpdateTimer.stop();
×
445

446
    disconnect( mJob, &QgsMapRendererJob::renderingLayersFinished, this, &InputMapCanvasMap::renderJobUpdated );
×
447
    disconnect( mJob, &QgsMapRendererJob::finished, this, &InputMapCanvasMap::renderJobFinished );
×
448

449
    if ( !mJob->isActive() )
×
450
      mJob->deleteLater();
×
451
    else
452
      connect( mJob, &QgsMapRendererJob::finished, mJob, &QObject::deleteLater );
×
453

454
    mJob->cancelWithoutBlocking();
×
455
    mJob = nullptr;
×
456
  }
457
}
26✔
458

459
void InputMapCanvasMap::zoomToFullExtent()
19✔
460
{
461
  QgsRectangle extent;
19✔
462
  const QList<QgsMapLayer *> layers = mMapSettings->layers();
19✔
463
  for ( QgsMapLayer *layer : layers )
152✔
464
  {
465
    if ( mMapSettings->destinationCrs() != layer->crs() )
133✔
466
    {
467
      QgsCoordinateTransform transform( layer->crs(), mMapSettings->destinationCrs(), mMapSettings->transformContext() );
×
468
      try
469
      {
470
        extent.combineExtentWith( transform.transformBoundingBox( layer->extent() ) );
×
471
      }
472
      catch ( const QgsCsException &exp )
×
473
      {
474
        // Ignore extent if it can't be transformed
475
      }
×
476
    }
×
477
    else
478
    {
479
      extent.combineExtentWith( layer->extent() );
133✔
480
    }
481
  }
482
  mMapSettings->setExtent( extent );
19✔
483

484
  refresh();
19✔
485
}
19✔
486

487
void InputMapCanvasMap::refresh()
103✔
488
{
489
  if ( mMapSettings->outputSize().isNull() )
103✔
490
    return; // the map image size has not been set yet
53✔
491

492
  if ( !mFreeze )
50✔
493
    mRefreshTimer.start( 1 );
50✔
494
}
495

496
void InputMapCanvasMap::clearCache()
×
497
{
498
  if ( mCache )
×
499
    mCache->clear();
×
500
}
×
501

502
void InputMapCanvasMap::clearTemporalCache()
×
503
{
504
  if ( mCache )
×
505
  {
506
    bool invalidateLabels = false;
×
507
    const QList<QgsMapLayer *> layerList = mMapSettings->mapSettings().layers();
×
508
    for ( QgsMapLayer *layer : layerList )
×
509
    {
510
      if ( layer->temporalProperties() && layer->temporalProperties()->isActive() )
×
511
      {
512
        if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
×
513
        {
514
          if ( vl->labelsEnabled() || vl->diagramsEnabled() )
×
515
            invalidateLabels = true;
×
516
        }
517

518
        if ( layer->temporalProperties()->flags() & QgsTemporalProperty::FlagDontInvalidateCachedRendersWhenRangeChanges )
×
519
          continue;
×
520

521
        mCache->invalidateCacheForLayer( layer );
×
522
      }
523
    }
524

525
    if ( invalidateLabels )
×
526
    {
527
      mCache->clearCacheImage( QStringLiteral( "_labels_" ) );
×
528
      mCache->clearCacheImage( QStringLiteral( "_preview_labels_" ) );
×
529
    }
530
  }
×
531
}
×
532

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