mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-17 00:04:02 -04:00
414 lines
15 KiB
C++
414 lines
15 KiB
C++
/***************************************************************************
|
|
qgsdatadefinedsizelegend.cpp
|
|
--------------------------------------
|
|
Date : June 2017
|
|
Copyright : (C) 2017 by Martin Dobias
|
|
Email : wonder dot sk at gmail dot com
|
|
***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "qgsdatadefinedsizelegend.h"
|
|
|
|
#include "qgsproperty.h"
|
|
#include "qgspropertytransformer.h"
|
|
#include "qgssymbollayerutils.h"
|
|
#include "qgsxmlutils.h"
|
|
|
|
|
|
QgsDataDefinedSizeLegend::QgsDataDefinedSizeLegend( const QgsDataDefinedSizeLegend &other )
|
|
: mType( other.mType )
|
|
, mTitleLabel( other.mTitleLabel )
|
|
, mSizeClasses( other.mSizeClasses )
|
|
, mSymbol( other.mSymbol.get() ? other.mSymbol->clone() : nullptr )
|
|
, mSizeScaleTransformer( other.mSizeScaleTransformer.get() ? new QgsSizeScaleTransformer( *other.mSizeScaleTransformer ) : nullptr )
|
|
, mVAlign( other.mVAlign )
|
|
, mFont( other.mFont )
|
|
, mTextColor( other.mTextColor )
|
|
, mTextAlignment( other.mTextAlignment )
|
|
{
|
|
}
|
|
|
|
QgsDataDefinedSizeLegend &QgsDataDefinedSizeLegend::operator=( const QgsDataDefinedSizeLegend &other )
|
|
{
|
|
if ( this != &other )
|
|
{
|
|
mType = other.mType;
|
|
mTitleLabel = other.mTitleLabel;
|
|
mSizeClasses = other.mSizeClasses;
|
|
mSymbol.reset( other.mSymbol.get() ? other.mSymbol->clone() : nullptr );
|
|
mSizeScaleTransformer.reset( other.mSizeScaleTransformer.get() ? new QgsSizeScaleTransformer( *other.mSizeScaleTransformer ) : nullptr );
|
|
mVAlign = other.mVAlign;
|
|
mFont = other.mFont;
|
|
mTextColor = other.mTextColor;
|
|
mTextAlignment = other.mTextAlignment;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
void QgsDataDefinedSizeLegend::setSymbol( QgsMarkerSymbol *symbol )
|
|
{
|
|
mSymbol.reset( symbol );
|
|
}
|
|
|
|
QgsMarkerSymbol *QgsDataDefinedSizeLegend::symbol() const
|
|
{
|
|
return mSymbol.get();
|
|
}
|
|
|
|
void QgsDataDefinedSizeLegend::setSizeScaleTransformer( QgsSizeScaleTransformer *transformer )
|
|
{
|
|
mSizeScaleTransformer.reset( transformer );
|
|
}
|
|
|
|
QgsSizeScaleTransformer *QgsDataDefinedSizeLegend::sizeScaleTransformer() const
|
|
{
|
|
return mSizeScaleTransformer.get();
|
|
}
|
|
|
|
|
|
void QgsDataDefinedSizeLegend::updateFromSymbolAndProperty( const QgsMarkerSymbol *symbol, const QgsProperty &ddSize )
|
|
{
|
|
mSymbol.reset( symbol->clone() );
|
|
mSymbol->setDataDefinedSize( QgsProperty() ); // original symbol may have had data-defined size associated
|
|
|
|
const QgsSizeScaleTransformer *sizeTransformer = dynamic_cast< const QgsSizeScaleTransformer * >( ddSize.transformer() );
|
|
mSizeScaleTransformer.reset( sizeTransformer ? sizeTransformer->clone() : nullptr );
|
|
|
|
if ( mTitleLabel.isEmpty() )
|
|
mTitleLabel = ddSize.propertyType() == QgsProperty::ExpressionBasedProperty ? ddSize.expressionString() : ddSize.field();
|
|
|
|
// automatically generate classes if no classes are defined
|
|
if ( sizeTransformer && mSizeClasses.isEmpty() )
|
|
{
|
|
mSizeClasses.clear();
|
|
Q_FOREACH ( double v, QgsSymbolLayerUtils::prettyBreaks( sizeTransformer->minValue(), sizeTransformer->maxValue(), 4 ) )
|
|
{
|
|
mSizeClasses << SizeClass( v, QString::number( v ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsLegendSymbolList QgsDataDefinedSizeLegend::legendSymbolList() const
|
|
{
|
|
QgsLegendSymbolList lst;
|
|
if ( !mTitleLabel.isEmpty() )
|
|
{
|
|
QgsLegendSymbolItem title( nullptr, mTitleLabel, QString() );
|
|
lst << title;
|
|
}
|
|
|
|
if ( mType == LegendCollapsed )
|
|
{
|
|
QgsLegendSymbolItem i;
|
|
i.setDataDefinedSizeLegendSettings( new QgsDataDefinedSizeLegend( *this ) );
|
|
lst << i;
|
|
return lst;
|
|
}
|
|
else if ( mType == LegendSeparated )
|
|
{
|
|
Q_FOREACH ( const SizeClass &cl, mSizeClasses )
|
|
{
|
|
QgsLegendSymbolItem si( mSymbol.get(), cl.label, QString() );
|
|
QgsMarkerSymbol *s = static_cast<QgsMarkerSymbol *>( si.symbol() );
|
|
s->setSize( cl.size );
|
|
lst << si;
|
|
}
|
|
}
|
|
return lst;
|
|
}
|
|
|
|
|
|
void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, QSize *outputSize, int *labelXOffset ) const
|
|
{
|
|
if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
|
|
{
|
|
if ( outputSize )
|
|
*outputSize = QSize();
|
|
if ( labelXOffset )
|
|
*labelXOffset = 0;
|
|
return;
|
|
}
|
|
|
|
// parameters that could be configurable
|
|
double hLengthLineMM = 2; // extra horizontal space to be occupied by callout line
|
|
double hSpaceLineTextMM = 1; // horizontal space between end of the line and start of the text
|
|
|
|
std::unique_ptr<QgsMarkerSymbol> s( mSymbol->clone() );
|
|
|
|
QList<SizeClass> classes = mSizeClasses;
|
|
|
|
// optionally scale size values if transformer is defined
|
|
if ( mSizeScaleTransformer )
|
|
{
|
|
for ( auto it = classes.begin(); it != classes.end(); ++it )
|
|
it->size = mSizeScaleTransformer->size( it->size );
|
|
}
|
|
|
|
// make sure we draw bigger symbols first
|
|
std::sort( classes.begin(), classes.end(), []( const SizeClass & a, const SizeClass & b ) { return a.size > b.size; } );
|
|
|
|
int hLengthLine = std::round( context.convertToPainterUnits( hLengthLineMM, QgsUnitTypes::RenderMillimeters ) );
|
|
int hSpaceLineText = std::round( context.convertToPainterUnits( hSpaceLineTextMM, QgsUnitTypes::RenderMillimeters ) );
|
|
int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
|
|
|
|
// get font metrics - we need a temporary image just to get the metrics right for the given DPI
|
|
QImage tmpImg( QSize( 1, 1 ), QImage::Format_ARGB32_Premultiplied );
|
|
tmpImg.setDotsPerMeterX( dpm );
|
|
tmpImg.setDotsPerMeterY( dpm );
|
|
QFontMetrics fm( mFont, &tmpImg );
|
|
int textHeight = fm.height();
|
|
int leading = fm.leading();
|
|
int minTextDistY = textHeight + leading;
|
|
|
|
//
|
|
// determine layout of the rendered elements
|
|
//
|
|
|
|
// find out how wide the text will be
|
|
int maxTextWidth = 0;
|
|
Q_FOREACH ( const SizeClass &c, classes )
|
|
{
|
|
int w = fm.width( c.label );
|
|
if ( w > maxTextWidth )
|
|
maxTextWidth = w;
|
|
}
|
|
|
|
// find out size of the largest symbol
|
|
double largestSize = classes.at( 0 ).size;
|
|
double outputLargestSize = context.convertToPainterUnits( largestSize, s->sizeUnit(), s->sizeMapUnitScale() );
|
|
|
|
// find out top Y coordinate for individual symbol sizes
|
|
QList<int> symbolTopY;
|
|
Q_FOREACH ( const SizeClass &c, classes )
|
|
{
|
|
double outputSymbolSize = context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() );
|
|
switch ( mVAlign )
|
|
{
|
|
case AlignCenter:
|
|
symbolTopY << std::round( outputLargestSize / 2 - outputSymbolSize / 2 );
|
|
break;
|
|
case AlignBottom:
|
|
symbolTopY << std::round( outputLargestSize - outputSymbolSize );
|
|
break;
|
|
}
|
|
}
|
|
|
|
// determine Y coordinate of texts: ideally they should be at the same level as symbolTopY
|
|
// but we need to avoid overlapping texts, so adjust the vertical positions
|
|
int middleIndex = 0; // classes.count() / 2; // will get the ideal position
|
|
QList<int> textCenterY;
|
|
int lastY = symbolTopY[middleIndex];
|
|
textCenterY << lastY;
|
|
for ( int i = middleIndex + 1; i < classes.count(); ++i )
|
|
{
|
|
int symbolY = symbolTopY[i];
|
|
if ( symbolY - lastY < minTextDistY )
|
|
symbolY = lastY + minTextDistY;
|
|
textCenterY << symbolY;
|
|
lastY = symbolY;
|
|
}
|
|
|
|
int textTopY = textCenterY.first() - textHeight / 2;
|
|
int textBottomY = textCenterY.last() + textHeight / 2;
|
|
int totalTextHeight = textBottomY - textTopY;
|
|
|
|
int fullWidth = outputLargestSize + hLengthLine + hSpaceLineText + maxTextWidth;
|
|
int fullHeight = std::max( static_cast< int >( std::round( outputLargestSize ) ) - textTopY, totalTextHeight );
|
|
|
|
if ( outputSize )
|
|
*outputSize = QSize( fullWidth, fullHeight );
|
|
if ( labelXOffset )
|
|
*labelXOffset = outputLargestSize + hLengthLine + hSpaceLineText;
|
|
|
|
if ( !context.painter() )
|
|
return; // only layout
|
|
|
|
//
|
|
// drawing
|
|
//
|
|
|
|
QPainter *p = context.painter();
|
|
|
|
p->save();
|
|
p->translate( 0, -textTopY );
|
|
|
|
// draw symbols first so that they do not cover
|
|
Q_FOREACH ( const SizeClass &c, classes )
|
|
{
|
|
s->setSize( c.size );
|
|
|
|
double outputSymbolSize = context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() );
|
|
double tx = ( outputLargestSize - outputSymbolSize ) / 2;
|
|
|
|
p->save();
|
|
switch ( mVAlign )
|
|
{
|
|
case AlignCenter:
|
|
p->translate( tx, ( outputLargestSize - outputSymbolSize ) / 2 );
|
|
break;
|
|
case AlignBottom:
|
|
p->translate( tx, outputLargestSize - outputSymbolSize );
|
|
break;
|
|
}
|
|
s->drawPreviewIcon( p, QSize( outputSymbolSize, outputSymbolSize ) );
|
|
p->restore();
|
|
}
|
|
|
|
p->setPen( mTextColor );
|
|
p->setFont( mFont );
|
|
|
|
int i = 0;
|
|
Q_FOREACH ( const SizeClass &c, classes )
|
|
{
|
|
// line from symbol to the text
|
|
p->drawLine( outputLargestSize / 2, symbolTopY[i], outputLargestSize + hLengthLine, textCenterY[i] );
|
|
|
|
// draw label
|
|
QRect rect( outputLargestSize + hLengthLine + hSpaceLineText, textCenterY[i] - textHeight / 2,
|
|
maxTextWidth, textHeight );
|
|
p->drawText( rect, mTextAlignment, c.label );
|
|
i++;
|
|
}
|
|
|
|
p->restore();
|
|
}
|
|
|
|
|
|
QImage QgsDataDefinedSizeLegend::collapsedLegendImage( QgsRenderContext &context, const QColor &backgroundColor, double paddingMM ) const
|
|
{
|
|
if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
|
|
return QImage();
|
|
|
|
// find out the size first
|
|
QSize contentSize;
|
|
drawCollapsedLegend( context, &contentSize );
|
|
|
|
int padding = std::round( context.convertToPainterUnits( paddingMM, QgsUnitTypes::RenderMillimeters ) );
|
|
int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
|
|
|
|
QImage img( contentSize.width() + padding * 2, contentSize.height() + padding * 2, QImage::Format_ARGB32_Premultiplied );
|
|
img.setDotsPerMeterX( dpm );
|
|
img.setDotsPerMeterY( dpm );
|
|
img.fill( backgroundColor );
|
|
|
|
QPainter painter( &img );
|
|
painter.setRenderHint( QPainter::Antialiasing, true );
|
|
|
|
painter.translate( padding, padding ); // so we do not need to care about padding at all
|
|
|
|
// now do the rendering
|
|
QPainter *oldPainter = context.painter();
|
|
context.setPainter( &painter );
|
|
drawCollapsedLegend( context );
|
|
context.setPainter( oldPainter );
|
|
|
|
painter.end();
|
|
return img;
|
|
}
|
|
|
|
QgsDataDefinedSizeLegend *QgsDataDefinedSizeLegend::readXml( const QDomElement &elem, const QgsReadWriteContext &context )
|
|
{
|
|
if ( elem.isNull() )
|
|
return nullptr;
|
|
QgsDataDefinedSizeLegend *ddsLegend = new QgsDataDefinedSizeLegend;
|
|
ddsLegend->setLegendType( elem.attribute( QStringLiteral( "type" ) ) == QLatin1String( "collapsed" ) ? LegendCollapsed : LegendSeparated );
|
|
ddsLegend->setVerticalAlignment( elem.attribute( QStringLiteral( "valign" ) ) == QLatin1String( "center" ) ? AlignCenter : AlignBottom );
|
|
ddsLegend->setTitle( elem.attribute( QStringLiteral( "title" ) ) );
|
|
|
|
QDomElement elemSymbol = elem.firstChildElement( QStringLiteral( "symbol" ) );
|
|
if ( !elemSymbol.isNull() )
|
|
{
|
|
ddsLegend->setSymbol( QgsSymbolLayerUtils::loadSymbol<QgsMarkerSymbol>( elemSymbol, context ) );
|
|
}
|
|
|
|
QgsSizeScaleTransformer *transformer = nullptr;
|
|
QDomElement elemTransformer = elem.firstChildElement( QStringLiteral( "transformer" ) );
|
|
if ( !elemTransformer.isNull() )
|
|
{
|
|
transformer = new QgsSizeScaleTransformer;
|
|
transformer->loadVariant( QgsXmlUtils::readVariant( elemTransformer ) );
|
|
}
|
|
ddsLegend->setSizeScaleTransformer( transformer );
|
|
|
|
QDomElement elemTextStyle = elem.firstChildElement( QStringLiteral( "text-style" ) );
|
|
if ( !elemTextStyle.isNull() )
|
|
{
|
|
QDomElement elemFont = elemTextStyle.firstChildElement( QStringLiteral( "font" ) );
|
|
if ( !elemFont.isNull() )
|
|
{
|
|
ddsLegend->setFont( QFont( elemFont.attribute( QStringLiteral( "family" ) ), elemFont.attribute( QStringLiteral( "size" ) ).toInt(),
|
|
elemFont.attribute( QStringLiteral( "weight" ) ).toInt(), elemFont.attribute( QStringLiteral( "italic" ) ).toInt() ) );
|
|
}
|
|
ddsLegend->setTextColor( QgsSymbolLayerUtils::decodeColor( elemTextStyle.attribute( QStringLiteral( "color" ) ) ) );
|
|
ddsLegend->setTextAlignment( static_cast<Qt::AlignmentFlag>( elemTextStyle.attribute( QStringLiteral( "align" ) ).toInt() ) );
|
|
}
|
|
|
|
QDomElement elemClasses = elem.firstChildElement( QStringLiteral( "classes" ) );
|
|
if ( !elemClasses.isNull() )
|
|
{
|
|
QList<SizeClass> classes;
|
|
QDomElement elemClass = elemClasses.firstChildElement( QStringLiteral( "class" ) );
|
|
while ( !elemClass.isNull() )
|
|
{
|
|
classes << SizeClass( elemClass.attribute( QStringLiteral( "size" ) ).toDouble(), elemClass.attribute( QStringLiteral( "label" ) ) );
|
|
elemClass = elemClass.nextSiblingElement();
|
|
}
|
|
ddsLegend->setClasses( classes );
|
|
}
|
|
|
|
return ddsLegend;
|
|
}
|
|
|
|
void QgsDataDefinedSizeLegend::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
|
|
{
|
|
QDomDocument doc = elem.ownerDocument();
|
|
|
|
elem.setAttribute( QStringLiteral( "type" ), mType == LegendCollapsed ? "collapsed" : "separated" );
|
|
elem.setAttribute( QStringLiteral( "valign" ), mVAlign == AlignCenter ? "center" : "bottom" );
|
|
elem.setAttribute( QStringLiteral( "title" ), mTitleLabel );
|
|
|
|
if ( mSymbol )
|
|
{
|
|
QDomElement elemSymbol = QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "source" ), mSymbol.get(), doc, context );
|
|
elem.appendChild( elemSymbol );
|
|
}
|
|
|
|
if ( mSizeScaleTransformer )
|
|
{
|
|
QDomElement elemTransformer = QgsXmlUtils::writeVariant( mSizeScaleTransformer->toVariant(), doc );
|
|
elemTransformer.setTagName( QStringLiteral( "transformer" ) );
|
|
elem.appendChild( elemTransformer );
|
|
}
|
|
|
|
QDomElement elemFont = doc.createElement( QStringLiteral( "font" ) );
|
|
elemFont.setAttribute( QStringLiteral( "family" ), mFont.family() );
|
|
elemFont.setAttribute( QStringLiteral( "size" ), mFont.pointSize() );
|
|
elemFont.setAttribute( QStringLiteral( "weight" ), mFont.weight() );
|
|
elemFont.setAttribute( QStringLiteral( "italic" ), mFont.italic() );
|
|
|
|
QDomElement elemTextStyle = doc.createElement( QStringLiteral( "text-style" ) );
|
|
elemTextStyle.setAttribute( QStringLiteral( "color" ), QgsSymbolLayerUtils::encodeColor( mTextColor ) );
|
|
elemTextStyle.setAttribute( QStringLiteral( "align" ), static_cast<int>( mTextAlignment ) );
|
|
elemTextStyle.appendChild( elemFont );
|
|
elem.appendChild( elemTextStyle );
|
|
|
|
if ( !mSizeClasses.isEmpty() )
|
|
{
|
|
QDomElement elemClasses = doc.createElement( QStringLiteral( "classes" ) );
|
|
Q_FOREACH ( const SizeClass &sc, mSizeClasses )
|
|
{
|
|
QDomElement elemClass = doc.createElement( QStringLiteral( "class" ) );
|
|
elemClass.setAttribute( QStringLiteral( "size" ), sc.size );
|
|
elemClass.setAttribute( QStringLiteral( "label" ), sc.label );
|
|
elemClasses.appendChild( elemClass );
|
|
}
|
|
elem.appendChild( elemClasses );
|
|
}
|
|
}
|