Skip to content

Instantly share code, notes, and snippets.

@signedav
Last active February 24, 2023 11:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save signedav/120f6c6aae753f662b93570f5d3429da to your computer and use it in GitHub Desktop.
Save signedav/120f6c6aae753f662b93570f5d3429da to your computer and use it in GitHub Desktop.
Legend Server Bug Research

Bring it home

1. Commit

Fix inconcequent use of DPI at generating the WMS legend

  • Consider DPI of QgsWmsRenderContext what is the OGC default (0.28 mm per pixel) or the passed WMS parameter - this is done by creating the QgsRenderContext by the mapSettings (with BBOX) or applying the dotsPerMm to the scaleFactor.

  • Additionally the image size needs to be calculated according to the QgsRenderContext now, what means it needs to be generated before.

  • The QPainter needs to be applied after to the context (since it's not passed by creating the context anymore).

  QImage *QgsRenderer::getLegendGraphics( QgsLayerTreeModel &model )
  {
    // get layers
    std::unique_ptr<QgsWmsRestorer> restorer;
    restorer.reset( new QgsWmsRestorer( mContext ) );

    // configure layers
    QList<QgsMapLayer *> layers = mContext.layersToRender();
    configureLayers( layers );

    // init renderer
    QgsLegendSettings settings = legendSettings();
    QgsLegendRenderer renderer( &model, settings );

    // create context
    QgsRenderContext context;
    if ( !mWmsParameters.bbox().isEmpty() )
    {
        QgsMapSettings mapSettings;
        mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
        std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
        configureMapSettings( tmp.get(), mapSettings );
        context = QgsRenderContext::fromMapSettings(mapSettings);
    } else {
      context.setScaleFactor( mContext.dotsPerMm() );
      const double mmPerMapUnit = 1 / QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mProject );
      context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );
    }

    // create image according to context
    std::unique_ptr<QImage> image;
    const qreal dpmm = mContext.dotsPerMm();
    const QSizeF minSize = renderer.minimumSize(&context);
    const QSize size( static_cast<int>( minSize.width() * dpmm ), static_cast<int>( minSize.height() * dpmm ) );
    if ( !mContext.isValidWidthHeight( size.width(), size.height() ) )
    {
      throw QgsServerException( QStringLiteral( "Legend image is too large" ) );
    }
    image.reset( createImage( size ) );

    // configure painter and addapt to the context
    QPainter painter( image.get() );

    context.setPainter(&painter);
    if ( painter.renderHints() & QPainter::SmoothPixmapTransform )
      context.setFlag( Qgis::RenderContextFlag::HighQualityImageTransforms, true );
    if ( painter.renderHints() & QPainter::LosslessImageRendering )
      context.setFlag( Qgis::RenderContextFlag::LosslessImageRendering, true );

    context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );
    QgsScopedRenderContextScaleToMm scaleContext( context );

    // rendering
    renderer.drawLegend( context );
    painter.end();

    return image.release();
  }

2. Commit

Fix DPI on getLegendGraphic concerning a RULE. It should not take the context settings according to the mapSettings since it NEVER has a BBOX (since of RULE)

    // create context
    QgsRenderContext context = QgsRenderContext::fromQPainter( painter.get() );
    context.setScaleFactor( mContext.dotsPerMm() );
    const double mmPerMapUnit = 1 / QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mProject );
    context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );
    QgsDistanceArea distanceArea = QgsDistanceArea();
    distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
    distanceArea.setEllipsoid( geoNone() );
    context.setDistanceArea( distanceArea );
    ctx.context = &context;

-> PUSH

3. Commit

fixing all the tests...

4. Commit

Append distance area when using defaultMapUnitsPerMm to consider the correct CRS. This fixes #50366

    } else {
      context.setScaleFactor( mContext.dotsPerMm() );
      const double mmPerMapUnit = 1 / QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mProject );
      context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );
      QgsDistanceArea distanceArea;
      distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
      distanceArea.setEllipsoid( geoNone() );
      context.setDistanceArea(distanceArea);
    }
  • new tests...

5. Commit

  • images to test

Notes

Note: Apparently in QGIS code dots are equal pixels...

Another Note It's working with EPSG:4326

http://localhost:8000/?MAP=/home/dave/dev/qgis/QGIS/tests/testdata/qgis_server/test_project_scaledsymbols.qgs&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=testlayer&CRS=EPSG:4326&srcheight=550&srcwidth=850&BBOX=25.3,-22.1,64.5,38.5

URLS for to the testfiles:

4326 ohne BBOX

http://localhost:8000/?MAP=/home/dave/dev/qgis/QGIS/tests/testdata/qgis_server/test_project_meters_at_scaledsymbols.qgs&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=testlayer&CRS=EPSG:4326

4326 mit BBOX

http://localhost:8000/?MAP=/home/dave/dev/qgis/QGIS/tests/testdata/qgis_server/test_project_meters_at_scaledsymbols.qgs&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=testlayer&CRS=EPSG:4326&SRCHEIGHT=2550&SRCWIDTH=3850&BBOX=44.89945254864102964,8.20044117721021948,44.90400902275693085,8.20936038559772285

4326 mit Rule

http://localhost:8000/?MAP=/home/dave/dev/qgis/QGIS/tests/testdata/qgis_server/test_project_meters_at_scaledsymbols.qgs&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=testlayer&CRS=EPSG:4326&WIDTH=50&HEIGHT=50&RULE=two

2056 ohne BBOX

http://localhost:8000/?MAP=/home/dave/dev/qgis/QGIS/tests/testdata/qgis_server/test_project_meters_at_scaledsymbols_2056.qgs&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=testlayer_2056&CRS=EPSG:2056

2056 mit BBOX

http://localhost:8000/?MAP=/home/dave/dev/qgis/QGIS/tests/testdata/qgis_server/test_project_meters_at_scaledsymbols_2056.qgs&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=testlayer_2056&CRS=EPSG:2056&BBOX=2662610.7,1268841.8,2663010.5,1269000.05&WIDTH=400&HEIGHT=400&SRCHEIGHT=1100&SRCWIDTH=1700

2056 mit RULE

http://localhost:8000/?MAP=/home/dave/dev/qgis/QGIS/tests/testdata/qgis_server/test_project_meters_at_scaledsymbols_2056.qgs&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=testlayer_2056&CRS=EPSG:2056&WIDTH=50&HEIGHT=50&RULE=test

URLS for you dave:

Map von 2056

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServer.qgz&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&CRS=EPSG:2056&WIDTH=2000&HEIGHT=2000&BBOX=2684045.08,1134480.69,2706655.46,1148786.83&LAYERS=Lines

2056 mit BBOX

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServer.qgz&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=Lines&SRCWIDTH=2000&SRCHEIGHT=2000&BBOX=2684045.08,1134480.69,2706655.46,1148786.83&CRS=EPSG:2056

2056 ohne BBOX

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServer.qgz&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=Lines&CRS=EPSG:2056

2056 mit Rule

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServerRule.qgz&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetLegendGraphics&LAYERS=Lines&WIDTH=50&HEIGHT=50&RULE=r1&CRS=EPSG:2056

Map von 4326

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServer4326.qgz&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&CRS=EPSG:4326&WIDTH=1000&HEIGHT=500&BBOX=46.305431,8.779962,46.396553,8.905068&LAYERS=Line

4326 mit BBOX

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServer4326.qgz&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=Line&WIDTH=1000&HEIGHT=500&BBOX=46.305431,8.779962,46.396553,8.905068&CRS=EPSG:4326

4326 ohne BBOX

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServer4326.qgz&SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=Line&CRS=EPSG:4326

4326 mit Rule

http://localhost:8000/?MAP=/home/dave/qgis_projects/LegendeServer/LegendeServerRule4326.qgz&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetLegendGraphics&LAYERS=Line&WIDTH=50&HEIGHT=50&RULE=r1&CRS=EPSG:4326

Short

The issue is that we do not set the CRS on the distanceArea of the QgsRenderContext and with that the default one is used (EPSG:4326) using Degrees instead of Meters as MapUnits, what gives us suuupersmall symbols on Meter-based CRS...

This can be solved by setting the corrects CRS, like e.g.

    const QgsCoordinateTransformContext cordcontext;
    distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), cordcontext );
    distanceArea.setEllipsoid( geoNone() );
    context.setDistanceArea(distanceArea);

But not sure if we can do that...

TL:DR - Backgroundinfo

  1. Default Scale for Legend is converted to mU/mm on saving the settings:

    double defaultMapUnitsPerMm = mWMSDefaultMapUnitScale->scale() / QgsUnitTypes::fromUnitToUnitFactor( QgsProject::instance()->crs().mapUnits(), QgsUnitTypes::DistanceMillimeters );
    

    This is stored then to the setting: "WMSDefaultMapUnitsPerMm"

    Means on 1:39542 we have 39.542 mU/mm

  2. Now we need to set it in QgsLegendSettings QgsRenderer::legendSettings():

    Without BBOX:

    The wmsDefaultMapUnitsPerMm is used.

    defaultMapUnitsPerPixel = QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mContext.project() ) / mContext.dotsPerMm();
    

    With this we have 39.542 mU/mm divided with e.g. 3.57 leading to 11.0718 mU/pixel.

    With BBOX:

    (we tried to make a more or less same bbox with the same scale)

    mapSettings.mapUnitsPerPixel() what is in our case 11.3052 mU/pixel

    Both cases: After that it's set here QgsLegendSettings::setMapUnitsPerPixel where other fancy calculations are done:

    mMmPerMapUnit = 1 / mapUnitsPerPixel / ( mDpi / 25.4 );
    

    Where mDpi is 96 (per default).

    Leads to mm/mU 0.023897048 (on 11.0718 mU/pixel) and to mm/mU 0.023403684 (on 11.3052 mU/pixel).

    And in QgsLegendSettings::mapUnitsPerPixel() it's calculated back to mU/px, but this is never done. Instead we do in QgsRenderer::getLegendGraphics:

    //setting the scale factor
    context.setRendererScale( settings.mapScale() );
    //calculating map units per pixel (again)
    context.setMapToPixel( QgsMapToPixel( 1 / ( settings.mmPerMapUnit() * context.scaleFactor() ) ) );
    

    With BBOX:

    And since we have a scale of 40375.7 this leads to a scale factor of 3.58268 this leads to:

    QgsMapToPixel( 1 / ( settings.mmPerMapUnit() * context.scaleFactor() ) ) => QgsMapToPixel( 1 / ( 0.023403684 * 3.58268 ) ) `

    (While in QgsMapToPixel should be passed mU/px)

    And with this we are back again on our 11.3052 mU/pixel.

    We end up here QgsSymbolLegendNode::drawSymbol where we do this:

    const double size = markerSymbol->size( *context ) / context->scaleFactor();
    

    And have this size (with and height) now: 2.10239e-05 (0.0000210239)

    Then we read dotsPerMM = context->scaleFactor(); again what is 3.58268.

    And in the end we calculate the with and the hight like this: std::round( width * dotsPerMM ) )

    Means we are on 0 now...

    And no legend symbol with both:

    image

  3. Where do we go on...

    It seems that this value here is way to small:

    And have this size (with and height) now: 2.10239e-05 (0.0000210239)

    We get it here here QgsSymbolLegendNode::drawSymbol where we do this:

    const double size = markerSymbol->size( *context ) / context->scaleFactor();
    

    In the size() we call QgsRenderContext::convertToPainterUnits and there we do this:

    size = convertMetersToMapUnits( size );
    

    Now this should not change the size. Because our map unit is meters. I thought. But apparantely not. We end up here QgsRenderContext::convertMetersToMapUnits with QgsUnitTypes::DistanceDegrees as mDistanceArea.sourceCrs().mapUnits() because the default SRS is EPSG:4326 set in the construtor of QgsDistanceArea::QgsDistanceArea().

    What we need to do now? Set somewhere the correct CRS in the QgsDistanceArea. Maybe in getLegendGraphics...

The Fix

In QImage *QgsRenderer::getLegendGraphics( QgsLayerTreeModel &model ):

    image.reset( createImage( size ) );

    // configure painter
    QPainter painter( image.get() );

    QgsRenderContext context = QgsRenderContext::fromQPainter(&painter);
    context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );
    QgsScopedRenderContextScaleToMm scaleContext( context );

    QgsDistanceArea distanceArea  = QgsDistanceArea();

    if ( !mWmsParameters.bbox().isEmpty() )
    {
          QgsMapSettings mapSettings;
          mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
          std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
          configureMapSettings( tmp.get(), mapSettings );
          context.setRendererScale( mapSettings.scale() );
          context.setMapToPixel( mapSettings.mapToPixel() );

          distanceArea.setSourceCrs( QgsCoordinateReferenceSystem(  mapSettings.destinationCrs() ), mapSettings.transformContext() );
          distanceArea.setEllipsoid( mapSettings.ellipsoid() );
    }else{
        const double defaultMapUnitsPerPixel = QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mContext.project() ) / mContext.dotsPerMm();
        const double mmPerMapUnit = 1 / defaultMapUnitsPerPixel / ( 96 / 25.4 );
        context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );

        distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
        distanceArea.setEllipsoid( geoNone() );
    }
    context.setDistanceArea(distanceArea);

    // rendering
    qDebug() << "let's draw a legend "<< size.height();
    renderer.drawLegend( context );
    painter.end();

    return image.release();

And in QImage *QgsRenderer::getLegendGraphics( QgsLayerTreeModelLegendNode &nodeModel ):

    // configure painter
    const qreal dpmm = mContext.dotsPerMm();
    QPainter painter( image.get() );
    painter.setRenderHint( QPainter::Antialiasing, true );
    painter.scale( dpmm, dpmm );

    // rendering
    QgsLegendSettings settings = legendSettings();
    QgsLayerTreeModelLegendNode::ItemContext ctx;

    QgsRenderContext context = QgsRenderContext::fromQPainter(&painter);
    context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );
    QgsScopedRenderContextScaleToMm scaleContext( context );

    QgsDistanceArea distanceArea  = QgsDistanceArea();
    if ( !mWmsParameters.bbox().isEmpty() )
    {
          QgsMapSettings mapSettings;
          mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
          std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
          configureMapSettings( tmp.get(), mapSettings );
          context.setRendererScale( mapSettings.scale() );
          context.setMapToPixel( mapSettings.mapToPixel() );

          distanceArea.setSourceCrs( QgsCoordinateReferenceSystem(  mapSettings.destinationCrs() ), mapSettings.transformContext() );
          distanceArea.setEllipsoid( mapSettings.ellipsoid() );
    }else
    {
        const double defaultMapUnitsPerPixel = QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mContext.project() ) / mContext.dotsPerMm();
        const double mmPerMapUnit = 1 / defaultMapUnitsPerPixel / ( 96 / 25.4 );
        context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );

        distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ),  mProject->transformContext() );
        distanceArea.setEllipsoid( geoNone() );
    }
    context.setDistanceArea(distanceArea);
    ctx.context = &context;

    ctx.painter = &painter;
    qDebug() << "let's draw a symbol "<< size.height();
    nodeModel.drawSymbol( settings, &ctx, size.height() / dpmm );
    painter.end();

    return image.release();

Approach with tmpContext

This approach fixes sizes:

  QImage *QgsRenderer::getLegendGraphics( QgsLayerTreeModel &model )
  {
    // get layers
    std::unique_ptr<QgsWmsRestorer> restorer;
    restorer.reset( new QgsWmsRestorer( mContext ) );

    // configure layers
    QList<QgsMapLayer *> layers = mContext.layersToRender();
    configureLayers( layers );

    // init renderer
    QgsLegendSettings settings = legendSettings();
    QgsLegendRenderer renderer( &model, settings );

    // tmp context to calculate the size
    QgsRenderContext tmpContext = QgsRenderContext::fromQPainter(nullptr);
    
    QgsDistanceArea distanceArea  = QgsDistanceArea();
    if ( !mWmsParameters.bbox().isEmpty() )
    {
          QgsMapSettings mapSettings;
          mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
          std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
          configureMapSettings( tmp.get(), mapSettings );
          tmpContext.setRendererScale( mapSettings.scale() );
          tmpContext.setMapToPixel( mapSettings.mapToPixel() );

          distanceArea.setSourceCrs( QgsCoordinateReferenceSystem(  mapSettings.destinationCrs() ), mapSettings.transformContext() );
          distanceArea.setEllipsoid( mapSettings.ellipsoid() );
    }else{
        const double defaultMapUnitsPerPixel = QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mContext.project() ) / mContext.dotsPerMm();
        const double mmPerMapUnit = 1 / defaultMapUnitsPerPixel / ( 96 / 25.4 );
        tmpContext.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * tmpContext.scaleFactor() ) ) );

        distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
        distanceArea.setEllipsoid( geoNone() );
    }
    tmpContext.setDistanceArea(distanceArea);

    // create image
    std::unique_ptr<QImage> image;
    const qreal dpmm = mContext.dotsPerMm();
    const QSizeF minSize = renderer.minimumSize(&tmpContext);
    const QSize size( static_cast<int>( minSize.width() * dpmm ), static_cast<int>( minSize.height() * dpmm ) );
    if ( !mContext.isValidWidthHeight( size.width(), size.height() ) )
    {
      throw QgsServerException( QStringLiteral( "Legend image is too large" ) );
    }
    image.reset( createImage( size ) );

    // configure painter
    QPainter painter( image.get() );

    QgsRenderContext context = QgsRenderContext::fromQPainter(&painter);
    context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );

    QgsScopedRenderContextScaleToMm scaleContext( context );

    if ( !mWmsParameters.bbox().isEmpty() )
    {
          QgsMapSettings mapSettings;
          mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
          std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
          configureMapSettings( tmp.get(), mapSettings );
          context.setRendererScale( mapSettings.scale() );
          context.setMapToPixel( mapSettings.mapToPixel() );

          distanceArea.setSourceCrs( QgsCoordinateReferenceSystem(  mapSettings.destinationCrs() ), mapSettings.transformContext() );
          distanceArea.setEllipsoid( mapSettings.ellipsoid() );
    }else{
        const double defaultMapUnitsPerPixel = QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mContext.project() ) / mContext.dotsPerMm();
        const double mmPerMapUnit = 1 / defaultMapUnitsPerPixel / ( 96 / 25.4 );
        context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );

        distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
        distanceArea.setEllipsoid( geoNone() );
    }
    context.setDistanceArea(distanceArea);

    // rendering
    renderer.drawLegend( context );
    painter.end();

    return image.release();
  }

Finetuned approach with tmpContext (without tmpContext but initializing context before QPainter)

Approach with applyPainter (discarted)

Trying to apply painter after the QgsRenderContext is evaluated. But this is apparently not a good sollution, since it leads to weird scaling.


  QImage *QgsRenderer::getLegendGraphics( QgsLayerTreeModel &model )
  {
    // get layers
    std::unique_ptr<QgsWmsRestorer> restorer;
    restorer.reset( new QgsWmsRestorer( mContext ) );

    // configure layers
    QList<QgsMapLayer *> layers = mContext.layersToRender();
    configureLayers( layers );

    // init renderer
    QgsLegendSettings settings = legendSettings();
    QgsLegendRenderer renderer( &model, settings );

    QgsRenderContext context;
    context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );

    QgsDistanceArea distanceArea  = QgsDistanceArea();

    if ( !mWmsParameters.bbox().isEmpty() )
    {
          QgsMapSettings mapSettings;
          mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
          std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
          configureMapSettings( tmp.get(), mapSettings );
          context.setRendererScale( mapSettings.scale() );
          context.setMapToPixel( mapSettings.mapToPixel() );

          distanceArea.setSourceCrs( QgsCoordinateReferenceSystem(  mapSettings.destinationCrs() ), mapSettings.transformContext() );
          distanceArea.setEllipsoid( mapSettings.ellipsoid() );
    }else{
        const double defaultMapUnitsPerPixel = QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mContext.project() ) / mContext.dotsPerMm();
        const double mmPerMapUnit = 1 / defaultMapUnitsPerPixel / ( 96 / 25.4 );
        context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );

        distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
        distanceArea.setEllipsoid( geoNone() );
    }
    context.setDistanceArea(distanceArea);

    // apply painter
    // create image
    std::unique_ptr<QImage> image;
    const qreal dpmm = mContext.dotsPerMm();
    const QSizeF minSize = renderer.minimumSize(&context);
    const QSize size( static_cast<int>( minSize.width() * dpmm ), static_cast<int>( minSize.height() * dpmm ) );
    if ( !mContext.isValidWidthHeight( size.width(), size.height() ) )
    {
      throw QgsServerException( QStringLiteral( "Legend image is too large" ) );
    }
    image.reset( createImage( size ) );

    // configure painter
    QPainter painter( image.get() );

    context.applyPainter(&painter);
    QgsScopedRenderContextScaleToMm scaleContext( context );

    // rendering
    renderer.drawLegend( context );
    painter.end();

    return image.release();
  }

PR Text

Issue the CRS (and with it the Map Units) have not been considered on GetLegendGraphics requests what leaded to invisible small symbol sizes on using Meters on Scale. It worked for EPSG:4326 since it was the default value, but not for CRS using Meters (e.g. 2056).

It uses the MapUnits from QgsRenderContext::distanceArea() what means this should be set. Since QgsLegendSettings is marked as deprecated I set it from the mapSettings (if BBOX provided) or from the mWmsParameters.crs() (if no BBOX provided).

Since the painter's image size should now be calculated according to the QgsRenderContext it needs to be defined before and consider a nullptr-painter.

I am not sure if this is the appropriate way to go. Happy for feedback.

  • Not sure if it should take themapSettings.transformContext().sourceCrs insetad of mapSettings.destinationCrs. -> See here it's done the same way: /home/dave/dev/qgis/QGIS/src/core/qgsrendercontext.cpp:263
  • check out difference between mProject and context.project() and use everywhere the same. -> it's the same

After failing tests

This context.setMapToPixel( mapSettings.mapToPixel() ); leads to verschobene Labels.

Und zwar aus folgendem Grund: mapSettings.mapToPixel() verwendet wohl mMapUnitsPerPixel 0.0476364 währenddem mit den LegendSettings anhand von mMapUnitsPerPixel 0.0519604 gerechnet wird. Zweiteres wird so berechnet: 1 / 5.55423 * 3.465 (wobei die Rechnung so ist 1 / ( settings.mmPerMapUnit() * tmpcontext.scaleFactor() )) Und jetzt kommt's. context.setScaleFactor( 3.465 ); //assume 88 dpi as standard value (88 dpi sind 3.465 dpmm) - währenddem in mapSettings offenbar von 96 dpi ausgegangen wird (und somit mit 3.7795 dpmm gerechnet wird)...

Frage 1: Weshalb 96 - weshalb 88?

Frage 2: Was ist richtig? Ist richtig so wie Map oder so wie bisher?

DistanceArea und setRendererScale( mapSettings.scale() ) macht keinen Unterschied.

"Hoselupf" leads to different size...

QgsScopedRenderContextScaleToMm scaleContext( context ) muss nach dem setzen des Painters gemacht werden aber offenbar vor dem setMapToPixel.

This Works:


  QImage *QgsRenderer::getLegendGraphics( QgsLayerTreeModel &model )
  {
    // get layers
    std::unique_ptr<QgsWmsRestorer> restorer;
    restorer.reset( new QgsWmsRestorer( mContext ) );

    // configure layers
    QList<QgsMapLayer *> layers = mContext.layersToRender();
    configureLayers( layers );

    // init renderer
    QgsLegendSettings settings = legendSettings();
    QgsLegendRenderer renderer( &model, settings );

    QgsRenderContext tmpcontext = QgsRenderContext::fromQPainter( nullptr );
    tmpcontext.setFlag( Qgis::RenderContextFlag::Antialiasing, true );

    QgsDistanceArea distanceArea  = QgsDistanceArea();
    if ( !mWmsParameters.bbox().isEmpty() )
    {
      QgsMapSettings mapSettings;
      mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
      std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
      configureMapSettings( tmp.get(), mapSettings );

      tmpcontext.setRendererScale( mapSettings.scale() );

      qDebug()<< "map units per pixel from mapsettings "<< mapSettings.mapUnitsPerPixel();

      qDebug()<< "vs map units per pixel from calculation "<<1 / ( settings.mmPerMapUnit() * tmpcontext.scaleFactor() )<<" means 1 / "<<settings.mmPerMapUnit()<<" * "<< tmpcontext.scaleFactor();
      tmpcontext.setMapToPixel( QgsMapToPixel( 1 / ( settings.mmPerMapUnit() * tmpcontext.scaleFactor() ) ) );
      //context.setMapToPixel( mapSettings.mapToPixel() );

      distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mapSettings.destinationCrs() ), mapSettings.transformContext() );
      distanceArea.setEllipsoid( mapSettings.ellipsoid() );
    }
    else
    {
      const double mmPerMapUnit = 1 / QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mProject );
      tmpcontext.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * tmpcontext.scaleFactor() ) ) );

      distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
      distanceArea.setEllipsoid( geoNone() );
    }
    tmpcontext.setDistanceArea( distanceArea );

    tmpcontext.setFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering, true );
    // create image with size according to the context
    std::unique_ptr<QImage> image;
    const qreal dpmm = mContext.dotsPerMm();
    const QSizeF minSize = renderer.minimumSize( &tmpcontext );
    const QSize size( static_cast<int>( minSize.width() * dpmm ), static_cast<int>( minSize.height() * dpmm ) );
    if ( !mContext.isValidWidthHeight( size.width(), size.height() ) )
    {
      throw QgsServerException( QStringLiteral( "Legend image is too large" ) );
    }
    image.reset( createImage( size ) );


    // set painter and scale context
    QPainter painter( image.get() );

    QgsRenderContext context = QgsRenderContext::fromQPainter( &painter );
    context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );

    QgsScopedRenderContextScaleToMm scaleContext( context );

    if ( !mWmsParameters.bbox().isEmpty() )
    {
      QgsMapSettings mapSettings;
      mapSettings.setFlag( Qgis::MapSettingsFlag::RenderBlocking );
      std::unique_ptr<QImage> tmp( createImage( mContext.mapSize( false ) ) );
      configureMapSettings( tmp.get(), mapSettings );

      context.setRendererScale( mapSettings.scale() );

      context.setMapToPixel( QgsMapToPixel( 1 / ( settings.mmPerMapUnit() * context.scaleFactor() ) ) );
      //context.setMapToPixel( mapSettings.mapToPixel() );

      distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mapSettings.destinationCrs() ), mapSettings.transformContext() );
      distanceArea.setEllipsoid( mapSettings.ellipsoid() );
    }
    else
    {
      const double mmPerMapUnit = 1 / QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mProject );
      context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );

      distanceArea.setSourceCrs( QgsCoordinateReferenceSystem( mWmsParameters.crs() ), mProject->transformContext() );
      distanceArea.setEllipsoid( geoNone() );
    }
    context.setDistanceArea( distanceArea );

    // rendering
    renderer.drawLegend( context );
    painter.end();

    return image.release();
  }

and This works:


  QImage *QgsRenderer::getLegendGraphics( QgsLayerTreeModel &model )
  {
    // get layers
    std::unique_ptr<QgsWmsRestorer> restorer;
    restorer.reset( new QgsWmsRestorer( mContext ) );

    // configure layers
    QList<QgsMapLayer *> layers = mContext.layersToRender();
    configureLayers( layers );

    // init renderer
    QgsLegendSettings settings = legendSettings();
    QgsLegendRenderer renderer( &model, settings );

    // create image
    std::unique_ptr<QImage> image;
    const qreal dpmm = mContext.dotsPerMm();
    const QSizeF minSize = renderer.minimumSize();
    const QSize size( static_cast<int>( minSize.width() * dpmm ), static_cast<int>( minSize.height() * dpmm ) );
    if ( !mContext.isValidWidthHeight( size.width(), size.height() ) )
    {
      throw QgsServerException( QStringLiteral( "Legend image is too large" ) );
    }
    image.reset( createImage( size ) );

    // configure painter
    QPainter painter( image.get() );
    QgsRenderContext context = QgsRenderContext::fromQPainter( nullptr );
    context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );

    context.setPainter(&painter);
    if ( painter.device() )
    {
      context.setScaleFactor( painter.device()->logicalDpiX() / 25.4 );
    }
    else
    {
      context.setScaleFactor( 3.465 ); //assume 88 dpi as standard value
    }

    if ( painter.renderHints() & QPainter::Antialiasing )
      context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );
    if ( painter.renderHints() & QPainter::SmoothPixmapTransform )
      context.setFlag( Qgis::RenderContextFlag::HighQualityImageTransforms, true );
    if ( painter.renderHints() & QPainter::LosslessImageRendering )
      context.setFlag( Qgis::RenderContextFlag::LosslessImageRendering, true );

    QgsScopedRenderContextScaleToMm scaleContext( context );
    
    // QGIS 4.0 -- take from real render context instead
    Q_NOWARN_DEPRECATED_PUSH
    context.setRendererScale( settings.mapScale() );
    context.setMapToPixel( QgsMapToPixel( 1 / ( settings.mmPerMapUnit() * context.scaleFactor() ) ) );
    Q_NOWARN_DEPRECATED_POP

    // rendering
    renderer.drawLegend( context );
    painter.end();

    return image.release();
  }

More notes

I'm a little stuck here. Issue was that the wmsDefaultMapUnitsPerMm on meter based CRS did not work, since the QgsRenderContext had no CRS. My goal is to apply it, and since it needs (a) calculate the image size according to the context and (b) create the context according to the painter, this needed a little reconstruction.

Current status is messy (and uses deprecated legendSettings). On cleaning up, I'm stuck at a point concerning the DPI/DPMM.

As far as I can see it we have:

  1. The DPI from QgsWmsRenderContext -> used then for the QPainter -> and used for the context. This is (when not passed by parameter) ~91 (calculated from OGC_PX_M = 0.00028).
  2. The DPI that is set on default when passing no QPainter to QgsRenderContext::fromQPainter what is 88.
  3. MapSettings provides a DPI from 96.

Which one should be used? I assume the one from the QgsWmsRenderContext since we set it on the QImage.

  1. Issue was that the CRS (and with it the Map Units) have not been considered on GetLegendGraphics requests what leaded to invisible small symbol sizes on using Meters on Scale. It worked for EPSG:4326 since it was the default value, but not for CRS using Meters (e.g. 2056).

  2. It uses the MapUnits from QgsRenderContext::distanceArea() what means this should be set. Since QgsLegendSettings is marked as deprecated I set it from the mapSettings (if BBOX provided) or from the mWmsParameters.crs() (if no BBOX provided).

  3. Since the painter's image size should now be calculated according to the QgsRenderContext it needs to be defined before and consider a nullptr-painter.


/*
* When using legend setting we did this: setMapUnitsPerPixel( mapSettings.mapUnitsPerPixel() ) where it multiplied it with 96 as dpi
* We would like to do this:
*       context.setMapToPixel( mapSettings.mapToPixel() );
* but this leads to problems (most possible because of different DPI)
*
* When using legend setting we did this: setMapUnitsPerPixel( defaultMapUnitsPerPixel ) where it multiplied it with 96 as dpi
* We would like to do this:
*       const double mmPerMapUnit = 1 / QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( *mProject );
*       context.setMapToPixel( QgsMapToPixel( 1 / ( mmPerMapUnit * context.scaleFactor() ) ) );
* but this leads to problems (most possible because of different DPI)
*/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment