小程序开发|小程序制作|小程序开发网

搜索

QGIS地图渲染分析

2023-4-27 16:54| 发布者: FY么事| 查看: 473| 评论: 0

摘要: QgsMapCanvas:QGraphicsView|- 成员变量:QGraphicsScene *mScene = nullptr;随构造函数一起新建,管理items。QgsMapCanvasMap *mMap = nullptr;拥有一张图片(一个QImage成员变量),是QGraphicsItem的间接子类,主要

QgsMapCanvas:QGraphicsView

|- 成员变量:

  • QGraphicsScene *mScene = nullptr;
    随构造函数一起新建,管理items。
  • QgsMapCanvasMap *mMap = nullptr;
    拥有一张图片(一个QImage成员变量),是QGraphicsItem的间接子类,主要用于展示地图。
  • QgsMapRendererQImageJob *mJob = nullptr;
    控制实际的渲染。
 QImage img = mJob->renderedImage(); mMap->setContent( img, imageRect( img, mSettings ) );
  • QList< QgsMapRendererQImageJob * > mPreviewJobs;
    预览工作。
  • QgsMapSettings mSettings;
    包含与绘制相关的所有设置:范围、图层(QgsMapLayer)等。

跟踪mScene

1.在QgsMapCanvas构造函数中创建并setSceneRect()设置范围。
2.在析构函数中释放items()和自身。
3.在QgsMapCanvas::resizeEvent()中根据窗口变化更新矩形范围setSceneRect()
4.在QgsMapCanvas::setCanvasColor()中更新场景背景色setBackgroundBrush()

跟踪mMap

1.在构造函数中创建。
2.在QgsMapCanvas::rendererJobFinished()中将渲染好的图片设置到内容中。

 mMap->setContent( img, imageRect( img, mSettings ) );

3.在QgsMapCanvas::previewJobFinished()中添加预览图片。

 mMap->addPreviewImage( job->renderedImage(), job->mapSettings().extent() );

4.在QgsMapCanvas的定时更新任务中,更新渲染好的图片

void QgsMapCanvas::mapUpdateTimeout(){  if ( mJob )  {    const QImage &img = mJob->renderedImage();    mMap->setContent( img, imageRect( img, mSettings ) );  }}

5.在QgsMapCanvas::saveAsImage()中按需提供地图图片

  else //use the map view  {    image = mMap->contentImage().copy();    painter.begin( &image );  }

跟踪mJob

1.在QgsMapCanvas::cancelJobs()中,取消渲染,并释放该对象。

  if ( mJob )  {    whileBlocking( mJob )->cancel();    delete mJob;    mJob = nullptr;  }

2.在QgsMapCanvas::isDrawing()中通过判断该对象是否为空来判断是否正在绘制。

bool QgsMapCanvas::isDrawing(){  return nullptr != mJob;} // isDrawing

3.在QgsMapCanvas::refreshMap()中,实例化该对象(并行、同步),将job的finished信号连接到QgsMapCanvas::rendererJobFinished(),最后调用start()开始渲染。

  // create the renderer job  Q_ASSERT( !mJob );  mJobCanceled = false;  if ( mUseParallelRendering )    mJob = new QgsMapRendererParallelJob( renderSettings );  else    mJob = new QgsMapRendererSequentialJob( renderSettings );  connect( mJob, &QgsMapRendererJob::finished, this, &QgsMapCanvas::rendererJobFinished );  mJob->setCache( mCache );  mJob->setLayerRenderingTimeHints( mLastLayerRenderTime );  mJob->start();
  1. 在QgsMapCanvas::rendererJobFinished()中获取渲染好的QImage对象(mJob->renderedImage())和其他数据(错误mJob->errors()、地图范围mJob->mapSettings().extent()、图层idmJob->mapSettings().layerIds()、渲染时长mJob->renderingTime()等)。
    最后,删除该对象。也就是说mJob在QgsMapCanvas::refreshMap()中被实例化,然后渲染结束后,在QgsMapCanvas::rendererJobFinished()中被释放。
  QImage img = mJob->renderedImage();  ......  // now we are in a slot called from mJob - do not delete it immediately  // so the class is still valid when the execution returns to the class  mJob->deleteLater();  mJob = nullptr;  emit mapCanvasRefreshed()

5.在QgsMapCanvas的定时任务中取出QImage对象。在mJob调用start()后,QgsMapCanvas便启动定时,并在定时任务中调用mJob->renderedImage()获取图像,直到mJob渲染结束。

void QgsMapCanvas::mapUpdateTimeout(){  if ( mJob )  {    const QImage &img = mJob->renderedImage();    mMap->setContent( img, imageRect( img, mSettings ) );  }}

6.在QgsMapCanvas::stopRendering()中停止渲染

void QgsMapCanvas::stopRendering(){  if ( mJob )  {    QgsDebugMsgLevel( QStringLiteral( "CANVAS stop rendering!" ), 2 );    mJobCanceled = true;    disconnect( mJob, &QgsMapRendererJob::finished, this, &QgsMapCanvas::rendererJobFinished );    connect( mJob, &QgsMapRendererQImageJob::finished, mJob, &QgsMapRendererQImageJob::deleteLater );    mJob->cancelWithoutBlocking();    mJob = nullptr;    emit mapRefreshCanceled();  }  stopPreviewJobs();}

地图刷新

1.refreshMap()为私有槽,与定时器mRefreshTimer绑定,该定时器为单次触发,而触发点在refresh()接口中。所有每次refresh()时触发地图刷新。

QgsMapCanvas::QgsMapCanvas(){ ......  mRefreshTimer = new QTimer( this );  mRefreshTimer->setSingleShot( true );  connect( mRefreshTimer, &QTimer::timeout, this, &QgsMapCanvas::refreshMap ); ......}void QgsMapCanvas::refresh(){ ......  // schedule a refresh  mRefreshTimer->start( 1 ); ......}

2.refresh()为公有槽,与定时器mResizeTimer绑定,该定时器为单次触发,在resizeEvent()中调用,以确保窗口大小改变时触发刷新。

void QgsMapCanvas::resizeEvent( QResizeEvent *e ){  QGraphicsView::resizeEvent( e );  mResizeTimer->start( 500 ); // in charge of refreshing canvas ......}

refresh()被多个信号连接并在多处被调用,确保地图得到及时刷新:

  • 椭球体改变:
    connect( QgsProject::instance(), &QgsProject::ellipsoidChanged,             this, [ = ]    {      mSettings.setEllipsoid( QgsProject::instance()->ellipsoid() );      refresh();    } );
  • 转换改变
    connect( QgsProject::instance(), &QgsProject::transformContextChanged,             this, [ = ]    {      mSettings.setTransformContext( QgsProject::instance()->transformContext() );      emit transformContextChanged();      refresh();    } );
  • 设置放大系数
void QgsMapCanvas::setMagnificationFactor( double factor, const QgsPointXY *center ){  ......  refresh();  ......}
  • 设置图层
void QgsMapCanvas::setLayersPrivate( const QList &layers )
  • 设置坐标系
void QgsMapCanvas::setDestinationCrs( const QgsCoordinateReferenceSystem &crs )
  • 地图设置
void QgsMapCanvas::setMapSettingsFlags( Qgis::MapSettingsFlags flags )void QgsMapCanvas::setCanvasColor( const QColor &color )
  • 地图主题改变
void QgsMapCanvas::mapThemeChanged( const QString &theme )void QgsMapCanvas::mapThemeRenamed( const QString &theme, const QString &newTheme )
  • 缩放至四至
void QgsMapCanvas::zoomToFullExtent()void QgsMapCanvas::zoomToProjectExtent()void QgsMapCanvas::zoomToPreviousExtent()void QgsMapCanvas::zoomToNextExtent()void QgsMapCanvas::zoomToFeatureExtent( QgsRectangle &rect )
  • 地图移动
void QgsMapCanvas::panToFeatureIds( QgsVectorLayer *layer, const QgsFeatureIds &ids, bool alwaysRecenter )void QgsMapCanvas::panToSelected( QgsVectorLayer *layer )void QgsMapCanvas::keyPressEvent( QKeyEvent *e )

刷新逻辑:

通过信号或主动调用(地图改变、发生平移、缩放)的方式调用refresh()接口,refresh()使用定时器触发refreshMap(),在refreshMap()中进行渲染调度。
1.停止当前的绘制工作。

  stopRendering(); // if any...  stopPreviewJobs();

2.创建RenderJob(并行/同步),传入renderSettingsrenderSettins中包含渲染地图需要的数据。

  if ( mUseParallelRendering )    mJob = new QgsMapRendererParallelJob( renderSettings );  else    mJob = new QgsMapRendererSequentialJob( renderSettings );

3.连接job的渲染结束信号至槽QgsMapCanvas::renderJobFinished,以便QgsMapCanvas获取渲染结束信号。

  connect( mJob, &QgsMapRendererJob::finished, this, &QgsMapCanvas::rendererJobFinished );  mJob->setCache( mCache );  mJob->setLayerRenderingTimeHints( mLastLayerRenderTime );

4.调用start()接口,开始执行渲染。

  mJob->start();

5.启动mMapUpdateTimer定时器,在mJob渲染过程中,定时去获取渲染图像(可能只渲染了部分)。如果单次绘制任务耗时较长,通过此定时任务也能及时更新出部分渲染好的地图?

  // from now on we can accept refresh requests again  // this must be reset only after the job has been started, because  // some providers (yes, it's you WCS and AMS!) during preparation  // do network requests and start an internal event loop, which may  // end up calling refresh() and would schedule another refresh,  // deleting the one we have just started.  mRefreshScheduled = false;  mMapUpdateTimer.start();  emit renderStarting();
void QgsMapCanvas::mapUpdateTimeout(){  if ( mJob )  {    const QImage &img = mJob->renderedImage();    mMap->setContent( img, imageRect( img, mSettings ) );  }}

渲染

在QgsMapCanvas构造函数中将mMapUpdateTimer的超时信号连接到槽QgsMapCanvas::mapUpdateTimeout(),并设置间隔为250ms.

void QgsMapCanvas::mapUpdateTimeout(){  if ( mJob )  {    const QImage &img = mJob->renderedImage();    mMap->setContent( img, imageRect( img, mSettings ) );  }}

在槽函数mapUpdateTimeout()中,调用QgsMapRendererQImageJob::renderedImage()输出渲染好的Image对象,并赋值给mMap.
QgsMapRendererQImageJob::renderedImage()是个纯虚函数,分别在子类QgsMapRendererParallelJobQgsMapRendererSequentialJob中实现。

QImage QgsMapRendererParallelJob::renderedImage(){  // if status == Idle we are either waiting for the render to start, OR have finished the render completely.  // We can differentiate between those states by checking whether mFinalImage is null -- at the "waiting for  // render to start" state mFinalImage has not yet been created.  const bool jobIsComplete = mStatus == Idle && !mFinalImage.isNull();  if ( !jobIsComplete )    return composeImage( mSettings, mLayerJobs, mLabelJob, mCache );  else    return mFinalImage; // when rendering labels or idle}QImage QgsMapRendererSequentialJob::renderedImage(){  if ( isActive() && mCache )    // this will allow immediate display of cached layers and at the same time updates of the layer being rendered    return composeImage( mSettings, mInternalJob->jobs(), LabelRenderJob() );  else    return mImage;}

从renderedImage()的实现可知,其通过composeImage()生成QImage对象。

QgsMapRenderJob::composeImage()

composeImage()在QgsMapRendererJob中实现。

QImage QgsMapRendererJob::composeImage( const QgsMapSettings &settings,                                        const std::vector &jobs,                                        const LabelRenderJob &labelJob,                                        const QgsMapRendererCache *cache                                      ){  QImage image( settings.deviceOutputSize(), settings.outputImageFormat() );  image.setDevicePixelRatio( settings.devicePixelRatio() );  image.setDotsPerMeterX( static_cast( settings.outputDpi() * 39.37 ) );  image.setDotsPerMeterY( static_cast( settings.outputDpi() * 39.37 ) );  image.fill( settings.backgroundColor().rgba() );  QPainter painter( &image );

composeImage()中创建Image对象时,传入了QgsMapSettings::deviceOutputSize()。

QSize QgsMapSettings::deviceOutputSize() const{  return outputSize() * mDevicePixelRatio;}QSize QgsMapSettings::outputSize() const{  return mSize;}void QgsMapSettings::setOutputSize( QSize size ){  mSize = size;  updateDerived();}

QgsMapSettings::deviceOutputSize()的实现可知,其返回的size是成员变量mSize*mDevicePixelRatio。而mSize的赋值由setOutputSize()实现。
继续回到QgsMapCanvas类,查看其调用QgsMapSettings::setOutputSize()的地方:

  QSize s = viewport()->size();  mSettings.setOutputSize( s );

传入的size大小其实是QGraphicsView的大小。
再回到QgsMapSettings中对mDevicePixelRatio的使用,对其赋值是通过setDevicePixelRatio()实现:


回到QgsMapCanvas中查找对QgsMapSettings::setDevicePixelRatio()的调用

void QgsMapCanvas::updateDevicePixelFromScreen(){  mSettings.setDevicePixelRatio( devicePixelRatio() );  ......}

其值是由QPaintDevice::devicePixelRatio()传入。

  1. 绘制图层。
 for ( const LayerRenderJob &job : jobs ){...... QImage img = layerImageToBeComposed( settings, job, cache );...... painter.drawImage( 0, 0, img );......}

composeImage()中遍历传入的LayerRenderJob,通过layerImageToBeComposed()获取单个job的图像,并依次绘制。
进入layerImageToBeComposed()

QImage QgsMapRendererJob::layerImageToBeComposed(  const QgsMapSettings &settings,  const LayerRenderJob &job,  const QgsMapRendererCache *cache){  if ( job.imageCanBeComposed() )  {    Q_ASSERT( job.img );    return *job.img;  }  else  {    if ( cache && cache->hasAnyCacheImage( job.layerId + QStringLiteral( "_preview" ) ) )    {      return cache->transformedCacheImage( job.layerId + QStringLiteral( "_preview" ), settings.mapToPixel() );    }    else      return QImage();  }}

通过判断job的图像是否可以被组合,如果可以则返回job中的图像,否则,尝试从缓冲中获取或返回空图像。

  1. 绘制Lable图层。
  // IMPORTANT - don't draw labelJob img before the label job is complete,  // as the image is uninitialized and full of garbage before the label job  // commences  if ( labelJob.img && labelJob.complete )  {    painter.setCompositionMode( QPainter::CompositionMode_SourceOver );    painter.setOpacity( 1.0 );    painter.drawImage( 0, 0, *labelJob.img );  }  // when checking for a label cache image, we only look for those which would be drawn between 30% and 300% of the  // original size. We don't want to draw massive pixelated labels on top of everything else, and we also don't need  // to draw tiny unreadable labels... better to draw nothing in this case and wait till the updated label results are ready!  else if ( cache && cache->hasAnyCacheImage( LABEL_PREVIEW_CACHE_ID, 0.3, 3 ) )  {    const QImage labelCacheImage = cache->transformedCacheImage( LABEL_PREVIEW_CACHE_ID, settings.mapToPixel() );    painter.setCompositionMode( QPainter::CompositionMode_SourceOver );    painter.setOpacity( 1.0 );    painter.drawImage( 0, 0, labelCacheImage );  }
  1. 绘制在Label之上的图层。
  // render any layers with the renderAboveLabels flag now  for ( const LayerRenderJob &job : jobs )

QgsMapRendererSequentialJob

  • startPrivate()
......mPainter = new QPainter( &mImage );......mInternalJob = new QgsMapRendererCustomPainterJob( mSettings, mPainter );......mInternalJob->start();

在startPrivate()中,实例化QPainter和QgsMapRendererCustomPainterJob,并调用mInternalJob的start()接口。所以绘制任务的开启委托给了QgsMapRendererCustomPainterJob对象。

  • QgsMapRendererCustomPainterJob::startPrivate()
......mLayerJobs = prepareJobs( mPainter, mLabelingEngineV2.get() );......  if ( mRenderSynchronously )  {    if ( !mPrepareOnly )    {      // do the rendering right now!      doRender();    }    return;  }......

QgsMapRendererCustomPainterJob通过prepareJobs()从mapSetting中提取图层信息,并得到mLayerJobs,之后调用doRender()开启绘制。

  • QgsMapRendererJob::prepareJobs()
    prepareJobs()在父类QgsMapRendererJob中实现。
......  // render all layers in the stack, starting at the base  QListIterator li( mSettings.layers() );  li.toBack();......  while ( li.hasPrevious() )  {    QgsMapLayer *ml = li.previous();    QgsVectorLayer *vl = qobject_cast( ml );......    layerJobs.emplace_back( LayerRenderJob() );    LayerRenderJob &job = layerJobs.back();    job.layer = ml;    job.layerId = ml->id();    job.estimatedRenderingTime = mLayerRenderingTimeHints.value( ml->id(), 0 );......    job.renderer = ml->createMapRenderer( *( job.context() ) );    if ( job.renderer )    {      job.renderer->setLayerRenderingTimeHint( job.estimatedRenderingTime );      job.context()->setFeedback( job.renderer->feedback() );    }......

首先从MapSetting中遍历图层,并将图层信息赋值给LayerRenderJob对象,然后调用maplayer的createMapRenderer()接口实例化渲染对象。

  • QgsMapRendererCustomPainterJob::doRender()
......for ( LayerRenderJob &job : mLayerJobs ){......    job.completed = job.renderer->render();......    if ( ! hasSecondPass && job.img )    {      // If we flattened this layer for alternate blend modes, composite it now      mPainter->setOpacity( job.opacity );      mPainter->drawImage( 0, 0, *job.img );      mPainter->setOpacity( 1.0 );    }}

doRender()中遍历LayerJobs,并且通过LayerRenderJob中的renderer对象调用render()执行渲染。并将渲染好的图像保存在job中。

QsVectorLayerRenderer

以QsVectorLayerRenderer为例:

bool QgsVectorLayerRenderer::render(){......  for ( const std::unique_ptr< QgsFeatureRenderer > &renderer : mRenderers )  {    res = renderInternal( renderer.get() ) && res;  }......}bool QgsVectorLayerRenderer::renderInternal( QgsFeatureRenderer *renderer ){......  QgsFeatureIterator fit = mSource->getFeatures( featureRequest );  // Attach an interruption checker so that iterators that have potentially  // slow fetchFeature() implementations, such as in the WFS provider, can  // check it, instead of relying on just the mContext.renderingStopped() check  // in drawRenderer()  fit.setInterruptionChecker( mFeedback.get() );  if ( ( renderer->capabilities() & QgsFeatureRenderer::SymbolLevels ) && renderer->usingSymbolLevels() )    drawRendererLevels( renderer, fit );  else    drawRenderer( renderer, fit );......}void QgsVectorLayerRenderer::drawRenderer( QgsFeatureRenderer *renderer, QgsFeatureIterator &fit ){......  QgsFeature fet;  while ( fit.nextFeature( fet ) )  {      // render feature      bool rendered = false;      if ( !context.testFlag( Qgis::RenderContextFlag::SkipSymbolRendering ) )      {        rendered = renderer->renderFeature( fet, context, -1, sel, drawMarker );      }      else      {        rendered = renderer->willRenderFeature( fet, context );      }     ......  }}

QgsVectorLayerRenderer::render()中遍历得到QgsFeatureRenderer,再调用QgsVectorLayerRenderer::renderInternal(),在renderInternal()里遍历要素,使用要素渲染器执行要素渲染gsFeatureRenderer::renderFeature()

总结

QgsMapRendererSequentialJob将start()任务交给QgsMapRendererCustomPainterJob,然后遍历图层,依次调用layerJob的渲染器(render)进行渲染。以矢量图层(QgsVectorLayerRenderer)渲染为例,遍历要素,并调用要素的渲染器(QgsFeatureRenderer),最后交给QgsSymbol,对要素进行符号化渲染。接口调用伪代码如下所示:

QgsMapRendererSequentialJob::start(){  QgsMapRendererCustomPainterJob::start()  {    QgsMapRendererCustomPainterJob::doRender()    {      for(LayerRenderJob job: layerJobs)      {        job.renderer->render()        {           for(QgsFeature feature : features)          {              QgsFeatureRenderer::renderFeature()              {                  QgsSymbol::renderFeature();              }          }        }      }    }  }}

QgsMapRendererParallelJob

  • startPrivate()
  const bool canUseLabelCache = prepareLabelCache();  mLayerJobs = prepareJobs( nullptr, mLabelingEngineV2.get() );  mLabelJob = prepareLabelingJob( nullptr, mLabelingEngineV2.get(), canUseLabelCache );  mSecondPassLayerJobs = prepareSecondPassJobs( mLayerJobs, mLabelJob );  // start async job  connect( &mFutureWatcher, &QFutureWatcher::finished, this, &QgsMapRendererParallelJob::renderLayersFinished );  mFuture = QtConcurrent::map( mLayerJobs, renderLayerStatic );  mFutureWatcher.setFuture( mFuture );

QgsMapRendererParallelJob在startPrivate()中使用QtConcurrent来进行并行计算。单个计算由void QgsMapRendererParallelJob::renderLayerStatic( LayerRenderJob &job )执行。

void QgsMapRendererParallelJob::renderLayerStatic( LayerRenderJob &job ){......    job.completed = job.renderer->render();......}

renderLayerStatic()中也同QgsMapRendererCustomPainterJob一样调用layerJob的renderer进行实际渲染。

Layers

~Layers: QgsMapLayer : QObject

MapCanvasMap : QgsMapCanvasItem : QGraphicsItem
重写paint(),利用QPainter::drawImage

ps:
图元unit : QGraphicsItem
图层Layer:QGraphicsItemGroup
地图:Map:QGraphcisScene:w


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

鲜花

握手

雷人

路过

鸡蛋

最新评论

返回顶部