mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-04 00:04:03 -04:00
1605 lines
49 KiB
C++
1605 lines
49 KiB
C++
/***************************************************************************
|
|
qgscategorizedsymbolrenderer.cpp
|
|
---------------------
|
|
begin : November 2009
|
|
copyright : (C) 2009 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 <algorithm>
|
|
|
|
#include "qgscategorizedsymbolrenderer.h"
|
|
|
|
#include "qgsdatadefinedsizelegend.h"
|
|
#include "qgssymbol.h"
|
|
#include "qgssymbollayerutils.h"
|
|
#include "qgscolorramp.h"
|
|
#include "qgscolorrampimpl.h"
|
|
#include "qgsgraduatedsymbolrenderer.h"
|
|
#include "qgspointdisplacementrenderer.h"
|
|
#include "qgsinvertedpolygonrenderer.h"
|
|
#include "qgspainteffect.h"
|
|
#include "qgssymbollayer.h"
|
|
#include "qgsfeature.h"
|
|
#include "qgsvectorlayer.h"
|
|
#include "qgslogger.h"
|
|
#include "qgsproperty.h"
|
|
#include "qgsstyle.h"
|
|
#include "qgsfieldformatter.h"
|
|
#include "qgsfieldformatterregistry.h"
|
|
#include "qgsapplication.h"
|
|
#include "qgsexpressioncontextutils.h"
|
|
#include "qgsstyleentityvisitor.h"
|
|
#include "qgsembeddedsymbolrenderer.h"
|
|
#include "qgsmarkersymbol.h"
|
|
#include "qgsexpressionnodeimpl.h"
|
|
#include "qgssldexportcontext.h"
|
|
|
|
#include <QDomDocument>
|
|
#include <QDomElement>
|
|
#include <QSettings> // for legend
|
|
#include <QRegularExpression>
|
|
#include <QUuid>
|
|
|
|
QgsRendererCategory::QgsRendererCategory( const QVariant &value, QgsSymbol *symbol, const QString &label, bool render, const QString &uuid )
|
|
: mValue( value )
|
|
, mSymbol( symbol )
|
|
, mLabel( label )
|
|
, mRender( render )
|
|
{
|
|
mUuid = !uuid.isEmpty() ? uuid : QUuid::createUuid().toString();
|
|
}
|
|
|
|
QgsRendererCategory::QgsRendererCategory( const QgsRendererCategory &cat )
|
|
: mValue( cat.mValue )
|
|
, mSymbol( cat.mSymbol ? cat.mSymbol->clone() : nullptr )
|
|
, mLabel( cat.mLabel )
|
|
, mRender( cat.mRender )
|
|
, mUuid( cat.mUuid )
|
|
{
|
|
}
|
|
|
|
QgsRendererCategory &QgsRendererCategory::operator=( QgsRendererCategory cat )
|
|
{
|
|
mValue = cat.mValue;
|
|
mSymbol.reset( cat.mSymbol ? cat.mSymbol->clone() : nullptr );
|
|
mLabel = cat.mLabel;
|
|
mRender = cat.mRender;
|
|
mUuid = cat.mUuid;
|
|
return *this;
|
|
}
|
|
|
|
QgsRendererCategory::~QgsRendererCategory() = default;
|
|
|
|
QString QgsRendererCategory::uuid() const
|
|
{
|
|
return mUuid;
|
|
}
|
|
|
|
QVariant QgsRendererCategory::value() const
|
|
{
|
|
return mValue;
|
|
}
|
|
|
|
QgsSymbol *QgsRendererCategory::symbol() const
|
|
{
|
|
return mSymbol.get();
|
|
}
|
|
|
|
QString QgsRendererCategory::label() const
|
|
{
|
|
return mLabel;
|
|
}
|
|
|
|
bool QgsRendererCategory::renderState() const
|
|
{
|
|
return mRender;
|
|
}
|
|
|
|
void QgsRendererCategory::setValue( const QVariant &value )
|
|
{
|
|
mValue = value;
|
|
}
|
|
|
|
void QgsRendererCategory::setSymbol( QgsSymbol *s )
|
|
{
|
|
if ( mSymbol.get() != s ) mSymbol.reset( s );
|
|
}
|
|
|
|
void QgsRendererCategory::setLabel( const QString &label )
|
|
{
|
|
mLabel = label;
|
|
}
|
|
|
|
void QgsRendererCategory::setRenderState( bool render )
|
|
{
|
|
mRender = render;
|
|
}
|
|
|
|
QString QgsRendererCategory::dump() const
|
|
{
|
|
return QStringLiteral( "%1::%2::%3:%4\n" ).arg( mValue.toString(), mLabel, mSymbol->dump() ).arg( mRender );
|
|
}
|
|
|
|
void QgsRendererCategory::toSld( QDomDocument &doc, QDomElement &element, QVariantMap props ) const
|
|
{
|
|
if ( !mSymbol.get() || props.value( QStringLiteral( "attribute" ), QString() ).toString().isEmpty() )
|
|
return;
|
|
|
|
QString attrName = props[ QStringLiteral( "attribute" )].toString();
|
|
|
|
QgsSldExportContext context;
|
|
context.setExtraProperties( props );
|
|
toSld( doc, element, attrName, context );
|
|
}
|
|
|
|
bool QgsRendererCategory::toSld( QDomDocument &doc, QDomElement &element, const QString &classAttribute, QgsSldExportContext &context ) const
|
|
{
|
|
if ( !mSymbol.get() || classAttribute.isEmpty() )
|
|
return false;
|
|
|
|
QString attrName = classAttribute;
|
|
|
|
// try to determine if attribute name is actually a field reference or expression.
|
|
// If it's a field reference, we need to quote it.
|
|
// Because we don't have access to the layer or fields here, we treat a parser error
|
|
// as just an unquoted field name (eg a field name with spaces)
|
|
const QgsExpression attrExpression = QgsExpression( attrName );
|
|
if ( attrExpression.hasParserError() )
|
|
{
|
|
attrName = QgsExpression::quotedColumnRef( attrName );
|
|
}
|
|
else if ( attrExpression.isField() )
|
|
{
|
|
attrName = QgsExpression::quotedColumnRef(
|
|
qgis::down_cast<const QgsExpressionNodeColumnRef *>( attrExpression.rootNode() )->name()
|
|
);
|
|
}
|
|
|
|
QDomElement ruleElem = doc.createElement( QStringLiteral( "se:Rule" ) );
|
|
|
|
QDomElement nameElem = doc.createElement( QStringLiteral( "se:Name" ) );
|
|
nameElem.appendChild( doc.createTextNode( mLabel ) );
|
|
ruleElem.appendChild( nameElem );
|
|
|
|
QDomElement descrElem = doc.createElement( QStringLiteral( "se:Description" ) );
|
|
QDomElement titleElem = doc.createElement( QStringLiteral( "se:Title" ) );
|
|
QString descrStr = QStringLiteral( "%1 is '%2'" ).arg( attrName, mValue.toString() );
|
|
titleElem.appendChild( doc.createTextNode( !mLabel.isEmpty() ? mLabel : descrStr ) );
|
|
descrElem.appendChild( titleElem );
|
|
ruleElem.appendChild( descrElem );
|
|
|
|
// create the ogc:Filter for the range
|
|
QString filterFunc;
|
|
if ( mValue.userType() == QMetaType::Type::QVariantList )
|
|
{
|
|
const QVariantList list = mValue.toList();
|
|
if ( list.size() == 1 )
|
|
{
|
|
filterFunc = QStringLiteral( "%1 = %2" ).arg( attrName, QgsExpression::quotedValue( list.at( 0 ) ) );
|
|
}
|
|
else
|
|
{
|
|
QStringList valuesList;
|
|
valuesList.reserve( list.size() );
|
|
for ( const QVariant &v : list )
|
|
{
|
|
valuesList << QgsExpression::quotedValue( v );
|
|
}
|
|
filterFunc = QStringLiteral( "%1 IN (%2)" ).arg( attrName,
|
|
valuesList.join( ',' ) );
|
|
}
|
|
}
|
|
else if ( QgsVariantUtils::isNull( mValue ) || mValue.toString().isEmpty() )
|
|
{
|
|
filterFunc = QStringLiteral( "ELSE" );
|
|
}
|
|
else
|
|
{
|
|
filterFunc = QStringLiteral( "%1 = %2" ).arg( attrName, QgsExpression::quotedValue( mValue ) );
|
|
}
|
|
|
|
QgsSymbolLayerUtils::createFunctionElement( doc, ruleElem, filterFunc, context );
|
|
|
|
// add the mix/max scale denoms if we got any from the callers
|
|
const QVariantMap oldProps = context.extraProperties();
|
|
QVariantMap props = oldProps;
|
|
QgsSymbolLayerUtils::applyScaleDependency( doc, ruleElem, props );
|
|
context.setExtraProperties( props );
|
|
mSymbol->toSld( doc, ruleElem, context );
|
|
context.setExtraProperties( oldProps );
|
|
if ( !QgsSymbolLayerUtils::hasSldSymbolizer( ruleElem ) )
|
|
{
|
|
// symbol could not be converted to SLD, or is an "empty" symbol. In this case we do not generate a rule, as
|
|
// SLD spec requires a Symbolizer element to be present
|
|
return false;
|
|
}
|
|
|
|
element.appendChild( ruleElem );
|
|
return true;
|
|
}
|
|
|
|
///////////////////
|
|
|
|
QgsCategorizedSymbolRenderer::QgsCategorizedSymbolRenderer( const QString &attrName, const QgsCategoryList &categories )
|
|
: QgsFeatureRenderer( QStringLiteral( "categorizedSymbol" ) )
|
|
, mAttrName( attrName )
|
|
{
|
|
//important - we need a deep copy of the categories list, not a shared copy. This is required because
|
|
//QgsRendererCategory::symbol() is marked const, and so retrieving the symbol via this method does not
|
|
//trigger a detachment and copy of mCategories BUT that same method CAN be used to modify a symbol in place
|
|
for ( const QgsRendererCategory &cat : categories )
|
|
{
|
|
if ( !cat.symbol() )
|
|
{
|
|
QgsDebugError( QStringLiteral( "invalid symbol in a category! ignoring..." ) );
|
|
}
|
|
mCategories << cat;
|
|
}
|
|
}
|
|
|
|
Qgis::FeatureRendererFlags QgsCategorizedSymbolRenderer::flags() const
|
|
{
|
|
Qgis::FeatureRendererFlags res;
|
|
QgsCategoryList::const_iterator catIt = mCategories.constBegin();
|
|
for ( ; catIt != mCategories.constEnd(); ++catIt )
|
|
{
|
|
if ( QgsSymbol *catSymbol = catIt->symbol() )
|
|
{
|
|
if ( catSymbol->flags().testFlag( Qgis::SymbolFlag::AffectsLabeling ) )
|
|
res.setFlag( Qgis::FeatureRendererFlag::AffectsLabeling );
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
QgsCategorizedSymbolRenderer::~QgsCategorizedSymbolRenderer() = default;
|
|
|
|
void QgsCategorizedSymbolRenderer::rebuildHash()
|
|
{
|
|
mSymbolHash.clear();
|
|
|
|
for ( const QgsRendererCategory &cat : std::as_const( mCategories ) )
|
|
{
|
|
const QVariant val = cat.value();
|
|
if ( val.userType() == QMetaType::Type::QVariantList )
|
|
{
|
|
const QVariantList list = val.toList();
|
|
for ( const QVariant &v : list )
|
|
{
|
|
mSymbolHash.insert( v.toString(), ( cat.renderState() || mCounting ) ? cat.symbol() : nullptr );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mSymbolHash.insert( val.toString(), ( cat.renderState() || mCounting ) ? cat.symbol() : nullptr );
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsSymbol *QgsCategorizedSymbolRenderer::skipRender()
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
QgsSymbol *QgsCategorizedSymbolRenderer::symbolForValue( const QVariant &value ) const
|
|
{
|
|
bool found = false;
|
|
return symbolForValue( value, found );
|
|
}
|
|
|
|
QgsSymbol *QgsCategorizedSymbolRenderer::symbolForValue( const QVariant &value, bool &foundMatchingSymbol ) const
|
|
{
|
|
foundMatchingSymbol = false;
|
|
|
|
// TODO: special case for int, double
|
|
QHash<QString, QgsSymbol *>::const_iterator it = mSymbolHash.constFind( QgsVariantUtils::isNull( value ) ? QString() : value.toString() );
|
|
if ( it == mSymbolHash.constEnd() )
|
|
{
|
|
if ( mSymbolHash.isEmpty() )
|
|
{
|
|
QgsDebugError( QStringLiteral( "there are no hashed symbols!!!" ) );
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( "attribute value not found: " + value.toString(), 3 );
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
foundMatchingSymbol = true;
|
|
|
|
return *it;
|
|
}
|
|
|
|
QgsSymbol *QgsCategorizedSymbolRenderer::symbolForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
|
|
{
|
|
return originalSymbolForFeature( feature, context );
|
|
}
|
|
|
|
QVariant QgsCategorizedSymbolRenderer::valueForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
|
|
{
|
|
QgsAttributes attrs = feature.attributes();
|
|
QVariant value;
|
|
if ( mAttrNum == -1 )
|
|
{
|
|
Q_ASSERT( mExpression );
|
|
|
|
value = mExpression->evaluate( &context.expressionContext() );
|
|
}
|
|
else
|
|
{
|
|
value = attrs.value( mAttrNum );
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
QgsSymbol *QgsCategorizedSymbolRenderer::originalSymbolForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
|
|
{
|
|
QVariant value = valueForFeature( feature, context );
|
|
|
|
bool foundCategory = false;
|
|
// find the right symbol for the category
|
|
QgsSymbol *symbol = symbolForValue( value, foundCategory );
|
|
|
|
if ( !foundCategory )
|
|
{
|
|
// if no symbol found, use default symbol
|
|
return symbolForValue( QVariant( "" ), foundCategory );
|
|
}
|
|
|
|
return symbol;
|
|
}
|
|
|
|
|
|
int QgsCategorizedSymbolRenderer::categoryIndexForValue( const QVariant &val )
|
|
{
|
|
for ( int i = 0; i < mCategories.count(); i++ )
|
|
{
|
|
if ( mCategories[i].value() == val )
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int QgsCategorizedSymbolRenderer::categoryIndexForLabel( const QString &val )
|
|
{
|
|
int idx = -1;
|
|
for ( int i = 0; i < mCategories.count(); i++ )
|
|
{
|
|
if ( mCategories[i].label() == val )
|
|
{
|
|
if ( idx != -1 )
|
|
return -1;
|
|
else
|
|
idx = i;
|
|
}
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::updateCategoryValue( int catIndex, const QVariant &value )
|
|
{
|
|
if ( catIndex < 0 || catIndex >= mCategories.size() )
|
|
return false;
|
|
mCategories[catIndex].setValue( value );
|
|
return true;
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::updateCategorySymbol( int catIndex, QgsSymbol *symbol )
|
|
{
|
|
if ( catIndex < 0 || catIndex >= mCategories.size() )
|
|
return false;
|
|
mCategories[catIndex].setSymbol( symbol );
|
|
return true;
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::updateCategoryLabel( int catIndex, const QString &label )
|
|
{
|
|
if ( catIndex < 0 || catIndex >= mCategories.size() )
|
|
return false;
|
|
mCategories[catIndex].setLabel( label );
|
|
return true;
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::updateCategoryRenderState( int catIndex, bool render )
|
|
{
|
|
if ( catIndex < 0 || catIndex >= mCategories.size() )
|
|
return false;
|
|
mCategories[catIndex].setRenderState( render );
|
|
return true;
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::addCategory( const QgsRendererCategory &cat )
|
|
{
|
|
if ( !cat.symbol() )
|
|
{
|
|
QgsDebugError( QStringLiteral( "invalid symbol in a category! ignoring..." ) );
|
|
return;
|
|
}
|
|
|
|
mCategories.append( cat );
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::deleteCategory( int catIndex )
|
|
{
|
|
if ( catIndex < 0 || catIndex >= mCategories.size() )
|
|
return false;
|
|
|
|
mCategories.removeAt( catIndex );
|
|
return true;
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::deleteAllCategories()
|
|
{
|
|
mCategories.clear();
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::moveCategory( int from, int to )
|
|
{
|
|
if ( from < 0 || from >= mCategories.size() || to < 0 || to >= mCategories.size() ) return;
|
|
mCategories.move( from, to );
|
|
}
|
|
|
|
bool valueLessThan( const QgsRendererCategory &c1, const QgsRendererCategory &c2 )
|
|
{
|
|
return qgsVariantLessThan( c1.value(), c2.value() );
|
|
}
|
|
bool valueGreaterThan( const QgsRendererCategory &c1, const QgsRendererCategory &c2 )
|
|
{
|
|
return qgsVariantGreaterThan( c1.value(), c2.value() );
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::sortByValue( Qt::SortOrder order )
|
|
{
|
|
if ( order == Qt::AscendingOrder )
|
|
{
|
|
std::sort( mCategories.begin(), mCategories.end(), valueLessThan );
|
|
}
|
|
else
|
|
{
|
|
std::sort( mCategories.begin(), mCategories.end(), valueGreaterThan );
|
|
}
|
|
}
|
|
|
|
bool labelLessThan( const QgsRendererCategory &c1, const QgsRendererCategory &c2 )
|
|
{
|
|
return QString::localeAwareCompare( c1.label(), c2.label() ) < 0;
|
|
}
|
|
|
|
bool labelGreaterThan( const QgsRendererCategory &c1, const QgsRendererCategory &c2 )
|
|
{
|
|
return QString::localeAwareCompare( c1.label(), c2.label() ) > 0;
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::sortByLabel( Qt::SortOrder order )
|
|
{
|
|
if ( order == Qt::AscendingOrder )
|
|
{
|
|
std::sort( mCategories.begin(), mCategories.end(), labelLessThan );
|
|
}
|
|
else
|
|
{
|
|
std::sort( mCategories.begin(), mCategories.end(), labelGreaterThan );
|
|
}
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::startRender( QgsRenderContext &context, const QgsFields &fields )
|
|
{
|
|
QgsFeatureRenderer::startRender( context, fields );
|
|
|
|
mCounting = context.rendererScale() == 0.0;
|
|
|
|
// make sure that the hash table is up to date
|
|
rebuildHash();
|
|
|
|
// find out classification attribute index from name
|
|
mAttrNum = fields.lookupField( mAttrName );
|
|
if ( mAttrNum == -1 )
|
|
{
|
|
mExpression.reset( new QgsExpression( mAttrName ) );
|
|
mExpression->prepare( &context.expressionContext() );
|
|
}
|
|
|
|
for ( const QgsRendererCategory &cat : std::as_const( mCategories ) )
|
|
{
|
|
cat.symbol()->startRender( context, fields );
|
|
}
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::stopRender( QgsRenderContext &context )
|
|
{
|
|
QgsFeatureRenderer::stopRender( context );
|
|
|
|
for ( const QgsRendererCategory &cat : std::as_const( mCategories ) )
|
|
{
|
|
cat.symbol()->stopRender( context );
|
|
}
|
|
mExpression.reset();
|
|
}
|
|
|
|
QSet<QString> QgsCategorizedSymbolRenderer::usedAttributes( const QgsRenderContext &context ) const
|
|
{
|
|
QSet<QString> attributes;
|
|
|
|
// mAttrName can contain either attribute name or an expression.
|
|
// Sometimes it is not possible to distinguish between those two,
|
|
// e.g. "a - b" can be both a valid attribute name or expression.
|
|
// Since we do not have access to fields here, try both options.
|
|
attributes << mAttrName;
|
|
|
|
QgsExpression testExpr( mAttrName );
|
|
if ( !testExpr.hasParserError() )
|
|
attributes.unite( testExpr.referencedColumns() );
|
|
|
|
QgsCategoryList::const_iterator catIt = mCategories.constBegin();
|
|
for ( ; catIt != mCategories.constEnd(); ++catIt )
|
|
{
|
|
QgsSymbol *catSymbol = catIt->symbol();
|
|
if ( catSymbol )
|
|
{
|
|
attributes.unite( catSymbol->usedAttributes( context ) );
|
|
}
|
|
}
|
|
return attributes;
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::filterNeedsGeometry() const
|
|
{
|
|
QgsExpression testExpr( mAttrName );
|
|
if ( !testExpr.hasParserError() )
|
|
{
|
|
QgsExpressionContext context;
|
|
context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( nullptr ) ); // unfortunately no layer access available!
|
|
testExpr.prepare( &context );
|
|
return testExpr.needsGeometry();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QString QgsCategorizedSymbolRenderer::dump() const
|
|
{
|
|
QString s = QStringLiteral( "CATEGORIZED: idx %1\n" ).arg( mAttrName );
|
|
for ( int i = 0; i < mCategories.count(); i++ )
|
|
s += mCategories[i].dump();
|
|
return s;
|
|
}
|
|
|
|
QgsCategorizedSymbolRenderer *QgsCategorizedSymbolRenderer::clone() const
|
|
{
|
|
QgsCategorizedSymbolRenderer *r = new QgsCategorizedSymbolRenderer( mAttrName, mCategories );
|
|
if ( mSourceSymbol )
|
|
r->setSourceSymbol( mSourceSymbol->clone() );
|
|
if ( mSourceColorRamp )
|
|
{
|
|
r->setSourceColorRamp( mSourceColorRamp->clone() );
|
|
}
|
|
r->setDataDefinedSizeLegend( mDataDefinedSizeLegend ? new QgsDataDefinedSizeLegend( *mDataDefinedSizeLegend ) : nullptr );
|
|
|
|
copyRendererData( r );
|
|
return r;
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const
|
|
{
|
|
QgsSldExportContext context;
|
|
context.setExtraProperties( props );
|
|
toSld( doc, element, context );
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::toSld( QDomDocument &doc, QDomElement &element, QgsSldExportContext &context ) const
|
|
{
|
|
const QVariantMap oldProps = context.extraProperties();
|
|
QVariantMap newProps = oldProps;
|
|
newProps[ QStringLiteral( "attribute" )] = mAttrName;
|
|
context.setExtraProperties( newProps );
|
|
|
|
// create a Rule for each range
|
|
bool result = true;
|
|
for ( QgsCategoryList::const_iterator it = mCategories.constBegin(); it != mCategories.constEnd(); ++it )
|
|
{
|
|
if ( !it->toSld( doc, element, mAttrName, context ) )
|
|
result = false;
|
|
}
|
|
context.setExtraProperties( oldProps );
|
|
return result;
|
|
}
|
|
|
|
QString QgsCategorizedSymbolRenderer::filter( const QgsFields &fields )
|
|
{
|
|
int attrNum = fields.lookupField( mAttrName );
|
|
bool isExpression = ( attrNum == -1 );
|
|
|
|
bool hasDefault = false;
|
|
bool defaultActive = false;
|
|
bool allActive = true;
|
|
bool noneActive = true;
|
|
|
|
//we need to build lists of both inactive and active values, as either list may be required
|
|
//depending on whether the default category is active or not
|
|
QString activeValues;
|
|
QString inactiveValues;
|
|
|
|
for ( const QgsRendererCategory &cat : std::as_const( mCategories ) )
|
|
{
|
|
if ( cat.value() == "" || QgsVariantUtils::isNull( cat.value() ) )
|
|
{
|
|
hasDefault = true;
|
|
defaultActive = cat.renderState();
|
|
}
|
|
|
|
noneActive = noneActive && !cat.renderState();
|
|
allActive = allActive && cat.renderState();
|
|
|
|
const bool isList = cat.value().userType() == QMetaType::Type::QVariantList;
|
|
QString value = QgsExpression::quotedValue( cat.value(), static_cast<QMetaType::Type>( cat.value().userType() ) );
|
|
|
|
if ( !cat.renderState() )
|
|
{
|
|
if ( value != "" )
|
|
{
|
|
if ( isList )
|
|
{
|
|
const QVariantList list = cat.value().toList();
|
|
for ( const QVariant &v : list )
|
|
{
|
|
if ( !inactiveValues.isEmpty() )
|
|
inactiveValues.append( ',' );
|
|
|
|
inactiveValues.append( QgsExpression::quotedValue( v, isExpression ? static_cast<QMetaType::Type>( v.userType() ) : fields.at( attrNum ).type() ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !inactiveValues.isEmpty() )
|
|
inactiveValues.append( ',' );
|
|
|
|
inactiveValues.append( value );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( value != "" )
|
|
{
|
|
if ( isList )
|
|
{
|
|
const QVariantList list = cat.value().toList();
|
|
for ( const QVariant &v : list )
|
|
{
|
|
if ( !activeValues.isEmpty() )
|
|
activeValues.append( ',' );
|
|
|
|
activeValues.append( QgsExpression::quotedValue( v, isExpression ? static_cast<QMetaType::Type>( v.userType() ) : fields.at( attrNum ).type() ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !activeValues.isEmpty() )
|
|
activeValues.append( ',' );
|
|
|
|
activeValues.append( value );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QString attr = isExpression ? mAttrName : QStringLiteral( "\"%1\"" ).arg( mAttrName );
|
|
|
|
if ( allActive && hasDefault )
|
|
{
|
|
return QString();
|
|
}
|
|
else if ( noneActive )
|
|
{
|
|
return QStringLiteral( "FALSE" );
|
|
}
|
|
else if ( defaultActive )
|
|
{
|
|
return QStringLiteral( "(%1) NOT IN (%2) OR (%1) IS NULL" ).arg( attr, inactiveValues );
|
|
}
|
|
else
|
|
{
|
|
return QStringLiteral( "(%1) IN (%2)" ).arg( attr, activeValues );
|
|
}
|
|
}
|
|
|
|
QgsSymbolList QgsCategorizedSymbolRenderer::symbols( QgsRenderContext &context ) const
|
|
{
|
|
Q_UNUSED( context )
|
|
QgsSymbolList lst;
|
|
lst.reserve( mCategories.count() );
|
|
for ( const QgsRendererCategory &cat : mCategories )
|
|
{
|
|
lst.append( cat.symbol() );
|
|
}
|
|
return lst;
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::accept( QgsStyleEntityVisitorInterface *visitor ) const
|
|
{
|
|
for ( const QgsRendererCategory &cat : mCategories )
|
|
{
|
|
QgsStyleSymbolEntity entity( cat.symbol() );
|
|
if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity, cat.value().toString(), cat.label() ) ) )
|
|
return false;
|
|
}
|
|
|
|
if ( mSourceColorRamp )
|
|
{
|
|
QgsStyleColorRampEntity entity( mSourceColorRamp.get() );
|
|
if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity ) ) )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QgsFeatureRenderer *QgsCategorizedSymbolRenderer::create( QDomElement &element, const QgsReadWriteContext &context )
|
|
{
|
|
QDomElement symbolsElem = element.firstChildElement( QStringLiteral( "symbols" ) );
|
|
if ( symbolsElem.isNull() )
|
|
return nullptr;
|
|
|
|
QDomElement catsElem = element.firstChildElement( QStringLiteral( "categories" ) );
|
|
if ( catsElem.isNull() )
|
|
return nullptr;
|
|
|
|
QgsSymbolMap symbolMap = QgsSymbolLayerUtils::loadSymbols( symbolsElem, context );
|
|
QgsCategoryList cats;
|
|
|
|
// Value from string (long, ulong, double and string)
|
|
const auto valueFromString = []( const QString & value, const QString & valueType ) -> QVariant
|
|
{
|
|
if ( valueType == QLatin1String( "double" ) )
|
|
{
|
|
bool ok;
|
|
const auto val { value.toDouble( &ok ) };
|
|
if ( ok )
|
|
{
|
|
return val;
|
|
}
|
|
}
|
|
else if ( valueType == QLatin1String( "ulong" ) )
|
|
{
|
|
bool ok;
|
|
const auto val { value.toULongLong( &ok ) };
|
|
if ( ok )
|
|
{
|
|
return val;
|
|
}
|
|
}
|
|
else if ( valueType == QLatin1String( "long" ) )
|
|
{
|
|
bool ok;
|
|
const auto val { value.toLongLong( &ok ) };
|
|
if ( ok )
|
|
{
|
|
return val;
|
|
}
|
|
}
|
|
else if ( valueType == QLatin1String( "bool" ) )
|
|
{
|
|
if ( value.toLower() == QLatin1String( "false" ) )
|
|
return false;
|
|
if ( value.toLower() == QLatin1String( "true" ) )
|
|
return true;
|
|
}
|
|
else if ( valueType == QLatin1String( "NULL" ) )
|
|
{
|
|
// This is the default ("fallback") category
|
|
return QVariant();
|
|
}
|
|
return value;
|
|
};
|
|
|
|
QDomElement catElem = catsElem.firstChildElement();
|
|
int i = 0;
|
|
QSet<QString> usedUuids;
|
|
while ( !catElem.isNull() )
|
|
{
|
|
if ( catElem.tagName() == QLatin1String( "category" ) )
|
|
{
|
|
QVariant value;
|
|
if ( catElem.hasAttribute( QStringLiteral( "value" ) ) )
|
|
{
|
|
value = valueFromString( catElem.attribute( QStringLiteral( "value" ) ), catElem.attribute( QStringLiteral( "type" ), QString() ) ) ;
|
|
}
|
|
else
|
|
{
|
|
QVariantList values;
|
|
QDomElement valElem = catElem.firstChildElement();
|
|
while ( !valElem.isNull() )
|
|
{
|
|
if ( valElem.tagName() == QLatin1String( "val" ) )
|
|
{
|
|
values << valueFromString( valElem.attribute( QStringLiteral( "value" ) ), valElem.attribute( QStringLiteral( "type" ), QString() ) );
|
|
}
|
|
valElem = valElem.nextSiblingElement();
|
|
}
|
|
if ( !values.isEmpty() )
|
|
value = values;
|
|
}
|
|
QString symbolName = catElem.attribute( QStringLiteral( "symbol" ) );
|
|
QString label = catElem.attribute( QStringLiteral( "label" ) );
|
|
bool render = catElem.attribute( QStringLiteral( "render" ) ) != QLatin1String( "false" );
|
|
QString uuid = catElem.attribute( QStringLiteral( "uuid" ), QString::number( i++ ) );
|
|
while ( usedUuids.contains( uuid ) )
|
|
{
|
|
uuid = QUuid::createUuid().toString();
|
|
}
|
|
if ( symbolMap.contains( symbolName ) )
|
|
{
|
|
QgsSymbol *symbol = symbolMap.take( symbolName );
|
|
cats.append( QgsRendererCategory( value, symbol, label, render, uuid ) );
|
|
usedUuids << uuid;
|
|
}
|
|
}
|
|
catElem = catElem.nextSiblingElement();
|
|
}
|
|
|
|
QString attrName = element.attribute( QStringLiteral( "attr" ) );
|
|
|
|
QgsCategorizedSymbolRenderer *r = new QgsCategorizedSymbolRenderer( attrName, cats );
|
|
|
|
// delete symbols if there are any more
|
|
QgsSymbolLayerUtils::clearSymbolMap( symbolMap );
|
|
|
|
// try to load source symbol (optional)
|
|
QDomElement sourceSymbolElem = element.firstChildElement( QStringLiteral( "source-symbol" ) );
|
|
if ( !sourceSymbolElem.isNull() )
|
|
{
|
|
QgsSymbolMap sourceSymbolMap = QgsSymbolLayerUtils::loadSymbols( sourceSymbolElem, context );
|
|
if ( sourceSymbolMap.contains( QStringLiteral( "0" ) ) )
|
|
{
|
|
r->setSourceSymbol( sourceSymbolMap.take( QStringLiteral( "0" ) ) );
|
|
}
|
|
QgsSymbolLayerUtils::clearSymbolMap( sourceSymbolMap );
|
|
}
|
|
|
|
// try to load color ramp (optional)
|
|
QDomElement sourceColorRampElem = element.firstChildElement( QStringLiteral( "colorramp" ) );
|
|
if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( QStringLiteral( "name" ) ) == QLatin1String( "[source]" ) )
|
|
{
|
|
r->setSourceColorRamp( QgsSymbolLayerUtils::loadColorRamp( sourceColorRampElem ).release() );
|
|
}
|
|
|
|
QDomElement rotationElem = element.firstChildElement( QStringLiteral( "rotation" ) );
|
|
if ( !rotationElem.isNull() && !rotationElem.attribute( QStringLiteral( "field" ) ).isEmpty() )
|
|
{
|
|
for ( const QgsRendererCategory &cat : r->mCategories )
|
|
{
|
|
convertSymbolRotation( cat.symbol(), rotationElem.attribute( QStringLiteral( "field" ) ) );
|
|
}
|
|
if ( r->mSourceSymbol )
|
|
{
|
|
convertSymbolRotation( r->mSourceSymbol.get(), rotationElem.attribute( QStringLiteral( "field" ) ) );
|
|
}
|
|
}
|
|
|
|
QDomElement sizeScaleElem = element.firstChildElement( QStringLiteral( "sizescale" ) );
|
|
if ( !sizeScaleElem.isNull() && !sizeScaleElem.attribute( QStringLiteral( "field" ) ).isEmpty() )
|
|
{
|
|
for ( const QgsRendererCategory &cat : r->mCategories )
|
|
{
|
|
convertSymbolSizeScale( cat.symbol(),
|
|
QgsSymbolLayerUtils::decodeScaleMethod( sizeScaleElem.attribute( QStringLiteral( "scalemethod" ) ) ),
|
|
sizeScaleElem.attribute( QStringLiteral( "field" ) ) );
|
|
}
|
|
if ( r->mSourceSymbol && r->mSourceSymbol->type() == Qgis::SymbolType::Marker )
|
|
{
|
|
convertSymbolSizeScale( r->mSourceSymbol.get(),
|
|
QgsSymbolLayerUtils::decodeScaleMethod( sizeScaleElem.attribute( QStringLiteral( "scalemethod" ) ) ),
|
|
sizeScaleElem.attribute( QStringLiteral( "field" ) ) );
|
|
}
|
|
}
|
|
|
|
QDomElement ddsLegendSizeElem = element.firstChildElement( QStringLiteral( "data-defined-size-legend" ) );
|
|
if ( !ddsLegendSizeElem.isNull() )
|
|
{
|
|
r->mDataDefinedSizeLegend.reset( QgsDataDefinedSizeLegend::readXml( ddsLegendSizeElem, context ) );
|
|
}
|
|
|
|
// TODO: symbol levels
|
|
return r;
|
|
}
|
|
|
|
QDomElement QgsCategorizedSymbolRenderer::save( QDomDocument &doc, const QgsReadWriteContext &context )
|
|
{
|
|
// clazy:skip
|
|
QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
|
|
rendererElem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "categorizedSymbol" ) );
|
|
rendererElem.setAttribute( QStringLiteral( "attr" ), mAttrName );
|
|
|
|
// String for type
|
|
// We just need string, bool, and three numeric types: double, ulong and long for unsigned, signed and float/double
|
|
const auto stringForType = []( const QMetaType::Type type ) -> QString
|
|
{
|
|
if ( type == QMetaType::Type::QChar || type == QMetaType::Type::Int || type == QMetaType::Type::LongLong )
|
|
{
|
|
return QStringLiteral( "long" );
|
|
}
|
|
else if ( type == QMetaType::Type::UInt || type == QMetaType::Type::ULongLong )
|
|
{
|
|
return QStringLiteral( "ulong" );
|
|
}
|
|
else if ( type == QMetaType::Type::Double )
|
|
{
|
|
return QStringLiteral( "double" ) ;
|
|
}
|
|
else if ( type == QMetaType::Type::Bool )
|
|
{
|
|
return QStringLiteral( "bool" );
|
|
}
|
|
else // Default: string
|
|
{
|
|
return QStringLiteral( "string" );
|
|
}
|
|
};
|
|
|
|
// categories
|
|
if ( !mCategories.isEmpty() )
|
|
{
|
|
int i = 0;
|
|
QgsSymbolMap symbols;
|
|
QDomElement catsElem = doc.createElement( QStringLiteral( "categories" ) );
|
|
QgsCategoryList::const_iterator it = mCategories.constBegin();
|
|
for ( ; it != mCategories.constEnd(); ++it )
|
|
{
|
|
const QgsRendererCategory &cat = *it;
|
|
QString symbolName = QString::number( i );
|
|
symbols.insert( symbolName, cat.symbol() );
|
|
|
|
QDomElement catElem = doc.createElement( QStringLiteral( "category" ) );
|
|
if ( cat.value().userType() == QMetaType::Type::QVariantList )
|
|
{
|
|
const QVariantList list = cat.value().toList();
|
|
for ( const QVariant &v : list )
|
|
{
|
|
QDomElement valueElem = doc.createElement( QStringLiteral( "val" ) );
|
|
valueElem.setAttribute( QStringLiteral( "value" ), v.toString() );
|
|
valueElem.setAttribute( QStringLiteral( "type" ), stringForType( static_cast<QMetaType::Type>( v.userType() ) ) );
|
|
catElem.appendChild( valueElem );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( QgsVariantUtils::isNull( cat.value() ) )
|
|
{
|
|
// We need to save NULL value as specific kind, it is the default ("fallback") category
|
|
catElem.setAttribute( QStringLiteral( "value" ), "NULL" );
|
|
catElem.setAttribute( QStringLiteral( "type" ), "NULL" );
|
|
}
|
|
else
|
|
{
|
|
catElem.setAttribute( QStringLiteral( "value" ), cat.value().toString() );
|
|
catElem.setAttribute( QStringLiteral( "type" ), stringForType( static_cast<QMetaType::Type>( cat.value().userType() ) ) );
|
|
}
|
|
}
|
|
catElem.setAttribute( QStringLiteral( "symbol" ), symbolName );
|
|
catElem.setAttribute( QStringLiteral( "label" ), cat.label() );
|
|
catElem.setAttribute( QStringLiteral( "render" ), cat.renderState() ? "true" : "false" );
|
|
catElem.setAttribute( QStringLiteral( "uuid" ), cat.uuid() );
|
|
catsElem.appendChild( catElem );
|
|
i++;
|
|
}
|
|
rendererElem.appendChild( catsElem );
|
|
|
|
// save symbols
|
|
QDomElement symbolsElem = QgsSymbolLayerUtils::saveSymbols( symbols, QStringLiteral( "symbols" ), doc, context );
|
|
rendererElem.appendChild( symbolsElem );
|
|
}
|
|
|
|
// save source symbol
|
|
if ( mSourceSymbol )
|
|
{
|
|
QgsSymbolMap sourceSymbols;
|
|
sourceSymbols.insert( QStringLiteral( "0" ), mSourceSymbol.get() );
|
|
QDomElement sourceSymbolElem = QgsSymbolLayerUtils::saveSymbols( sourceSymbols, QStringLiteral( "source-symbol" ), doc, context );
|
|
rendererElem.appendChild( sourceSymbolElem );
|
|
}
|
|
|
|
// save source color ramp
|
|
if ( mSourceColorRamp )
|
|
{
|
|
QDomElement colorRampElem = QgsSymbolLayerUtils::saveColorRamp( QStringLiteral( "[source]" ), mSourceColorRamp.get(), doc );
|
|
rendererElem.appendChild( colorRampElem );
|
|
}
|
|
|
|
QDomElement rotationElem = doc.createElement( QStringLiteral( "rotation" ) );
|
|
rendererElem.appendChild( rotationElem );
|
|
|
|
QDomElement sizeScaleElem = doc.createElement( QStringLiteral( "sizescale" ) );
|
|
rendererElem.appendChild( sizeScaleElem );
|
|
|
|
if ( mDataDefinedSizeLegend )
|
|
{
|
|
QDomElement ddsLegendElem = doc.createElement( QStringLiteral( "data-defined-size-legend" ) );
|
|
mDataDefinedSizeLegend->writeXml( ddsLegendElem, context );
|
|
rendererElem.appendChild( ddsLegendElem );
|
|
}
|
|
|
|
saveRendererData( doc, rendererElem, context );
|
|
|
|
return rendererElem;
|
|
}
|
|
|
|
|
|
QgsLegendSymbolList QgsCategorizedSymbolRenderer::baseLegendSymbolItems() const
|
|
{
|
|
QgsLegendSymbolList lst;
|
|
for ( const QgsRendererCategory &cat : mCategories )
|
|
{
|
|
lst << QgsLegendSymbolItem( cat.symbol(), cat.label(), cat.uuid(), true );
|
|
}
|
|
return lst;
|
|
}
|
|
|
|
QString QgsCategorizedSymbolRenderer::displayString( const QVariant &v, int precision )
|
|
{
|
|
|
|
auto _displayString = [ ]( const QVariant & v, int precision ) -> QString
|
|
{
|
|
|
|
if ( QgsVariantUtils::isNull( v ) )
|
|
{
|
|
return QgsApplication::nullRepresentation();
|
|
}
|
|
|
|
const bool isNumeric {v.userType() == QMetaType::Type::Double || v.userType() == QMetaType::Type::Int || v.userType() == QMetaType::Type::UInt || v.userType() == QMetaType::Type::LongLong || v.userType() == QMetaType::Type::ULongLong};
|
|
|
|
// Special treatment for numeric types if group separator is set or decimalPoint is not a dot
|
|
if ( v.userType() == QMetaType::Type::Double )
|
|
{
|
|
// if value doesn't contain a double (a default value expression for instance),
|
|
// apply no transformation
|
|
bool ok;
|
|
v.toDouble( &ok );
|
|
if ( !ok )
|
|
return v.toString();
|
|
|
|
// Locales with decimal point != '.' or that require group separator: use QLocale
|
|
if ( QLocale().decimalPoint() != '.' ||
|
|
!( QLocale().numberOptions() & QLocale::NumberOption::OmitGroupSeparator ) )
|
|
{
|
|
if ( precision > 0 )
|
|
{
|
|
if ( -1 < v.toDouble() && v.toDouble() < 1 )
|
|
{
|
|
return QLocale().toString( v.toDouble(), 'g', precision );
|
|
}
|
|
else
|
|
{
|
|
return QLocale().toString( v.toDouble(), 'f', precision );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Precision is not set, let's guess it from the
|
|
// standard conversion to string
|
|
const QString s( v.toString() );
|
|
const int dotPosition( s.indexOf( '.' ) );
|
|
int precision;
|
|
if ( dotPosition < 0 && s.indexOf( 'e' ) < 0 )
|
|
{
|
|
precision = 0;
|
|
return QLocale().toString( v.toDouble(), 'f', precision );
|
|
}
|
|
else
|
|
{
|
|
if ( dotPosition < 0 ) precision = 0;
|
|
else precision = s.length() - dotPosition - 1;
|
|
|
|
if ( -1 < v.toDouble() && v.toDouble() < 1 )
|
|
{
|
|
return QLocale().toString( v.toDouble(), 'g', precision );
|
|
}
|
|
else
|
|
{
|
|
return QLocale().toString( v.toDouble(), 'f', precision );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Default for doubles with precision
|
|
else if ( precision > 0 )
|
|
{
|
|
if ( -1 < v.toDouble() && v.toDouble() < 1 )
|
|
{
|
|
return QString::number( v.toDouble(), 'g', precision );
|
|
}
|
|
else
|
|
{
|
|
return QString::number( v.toDouble(), 'f', precision );
|
|
}
|
|
}
|
|
}
|
|
// Other numeric types than doubles
|
|
else if ( isNumeric &&
|
|
!( QLocale().numberOptions() & QLocale::NumberOption::OmitGroupSeparator ) )
|
|
{
|
|
bool ok;
|
|
const qlonglong converted( v.toLongLong( &ok ) );
|
|
if ( ok )
|
|
return QLocale().toString( converted );
|
|
}
|
|
else if ( v.userType() == QMetaType::Type::QByteArray )
|
|
{
|
|
return QObject::tr( "BLOB" );
|
|
}
|
|
|
|
// Fallback if special rules do not apply
|
|
return v.toString();
|
|
};
|
|
|
|
if ( v.userType() == QMetaType::Type::QStringList || v.userType() == QMetaType::Type::QVariantList )
|
|
{
|
|
// Note that this code is never hit because the joining of lists (merged categories) happens
|
|
// in data(); I'm leaving this here anyway because it is tested and it may be useful for
|
|
// other purposes in the future.
|
|
QString result;
|
|
const QVariantList list = v.toList();
|
|
for ( const QVariant &var : list )
|
|
{
|
|
if ( !result.isEmpty() )
|
|
{
|
|
result.append( ';' );
|
|
}
|
|
result.append( _displayString( var, precision ) );
|
|
}
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
return _displayString( v, precision );
|
|
}
|
|
}
|
|
|
|
QgsLegendSymbolList QgsCategorizedSymbolRenderer::legendSymbolItems() const
|
|
{
|
|
if ( mDataDefinedSizeLegend && mSourceSymbol && mSourceSymbol->type() == Qgis::SymbolType::Marker )
|
|
{
|
|
// check that all symbols that have the same size expression
|
|
QgsProperty ddSize;
|
|
for ( const QgsRendererCategory &category : mCategories )
|
|
{
|
|
const QgsMarkerSymbol *symbol = static_cast<const QgsMarkerSymbol *>( category.symbol() );
|
|
if ( ddSize )
|
|
{
|
|
QgsProperty sSize( symbol->dataDefinedSize() );
|
|
if ( sSize != ddSize )
|
|
{
|
|
// no common size expression
|
|
return baseLegendSymbolItems();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ddSize = symbol->dataDefinedSize();
|
|
}
|
|
}
|
|
|
|
if ( ddSize && ddSize.isActive() )
|
|
{
|
|
QgsLegendSymbolList lst;
|
|
|
|
QgsDataDefinedSizeLegend ddSizeLegend( *mDataDefinedSizeLegend );
|
|
ddSizeLegend.updateFromSymbolAndProperty( static_cast<const QgsMarkerSymbol *>( mSourceSymbol.get() ), ddSize );
|
|
lst += ddSizeLegend.legendSymbolList();
|
|
|
|
lst += baseLegendSymbolItems();
|
|
return lst;
|
|
}
|
|
}
|
|
|
|
return baseLegendSymbolItems();
|
|
}
|
|
|
|
QSet<QString> QgsCategorizedSymbolRenderer::legendKeysForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
|
|
{
|
|
const QVariant value = valueForFeature( feature, context );
|
|
|
|
for ( const QgsRendererCategory &cat : mCategories )
|
|
{
|
|
bool match = false;
|
|
if ( cat.value().userType() == QMetaType::Type::QVariantList )
|
|
{
|
|
const QVariantList list = cat.value().toList();
|
|
for ( const QVariant &v : list )
|
|
{
|
|
if ( value == v )
|
|
{
|
|
match = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Numeric NULL cat value is stored as an empty string
|
|
if ( QgsVariantUtils::isNull( value ) && ( value.userType() == QMetaType::Type::Double || value.userType() == QMetaType::Type::Int ||
|
|
value.userType() == QMetaType::Type::UInt || value.userType() == QMetaType::Type::LongLong ||
|
|
value.userType() == QMetaType::Type::ULongLong || value.userType() == QMetaType::Type::Bool ) )
|
|
{
|
|
match = cat.value().toString().isEmpty();
|
|
}
|
|
else
|
|
{
|
|
match = value == cat.value();
|
|
}
|
|
}
|
|
|
|
if ( match )
|
|
{
|
|
if ( cat.renderState() || mCounting )
|
|
return QSet< QString >() << cat.uuid();
|
|
else
|
|
return QSet< QString >();
|
|
}
|
|
}
|
|
|
|
return QSet< QString >();
|
|
}
|
|
|
|
QString QgsCategorizedSymbolRenderer::legendKeyToExpression( const QString &key, QgsVectorLayer *layer, bool &ok ) const
|
|
{
|
|
ok = false;
|
|
int i = 0;
|
|
for ( i = 0; i < mCategories.size(); i++ )
|
|
{
|
|
if ( mCategories[i].uuid() == key )
|
|
{
|
|
ok = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !ok )
|
|
{
|
|
ok = false;
|
|
return QString();
|
|
}
|
|
|
|
const int fieldIndex = layer ? layer->fields().lookupField( mAttrName ) : -1;
|
|
const bool isNumeric = layer && fieldIndex >= 0 ? layer->fields().at( fieldIndex ).isNumeric() : false;
|
|
const QMetaType::Type fieldType = layer && fieldIndex >= 0 ? layer->fields().at( fieldIndex ).type() : QMetaType::Type::UnknownType;
|
|
const QString attributeComponent = QgsExpression::quoteFieldExpression( mAttrName, layer );
|
|
|
|
ok = true;
|
|
const QgsRendererCategory &cat = mCategories[i];
|
|
if ( cat.value().userType() == QMetaType::Type::QVariantList )
|
|
{
|
|
const QVariantList list = cat.value().toList();
|
|
QStringList parts;
|
|
parts.reserve( list.size() );
|
|
for ( const QVariant &v : list )
|
|
{
|
|
parts.append( QgsExpression::quotedValue( v ) );
|
|
}
|
|
|
|
return QStringLiteral( "%1 IN (%2)" ).arg( attributeComponent, parts.join( QLatin1String( ", " ) ) );
|
|
}
|
|
else
|
|
{
|
|
// Numeric NULL cat value is stored as an empty string
|
|
QVariant value = cat.value();
|
|
if ( isNumeric && value.toString().isEmpty() )
|
|
{
|
|
value = QVariant();
|
|
}
|
|
|
|
if ( QgsVariantUtils::isNull( value ) )
|
|
return QStringLiteral( "%1 IS NULL" ).arg( attributeComponent );
|
|
else if ( fieldType == QMetaType::Type::UnknownType )
|
|
return QStringLiteral( "%1 = %2" ).arg( attributeComponent, QgsExpression::quotedValue( value ) );
|
|
else
|
|
return QStringLiteral( "%1 = %2" ).arg( attributeComponent, QgsExpression::quotedValue( value, fieldType ) );
|
|
}
|
|
}
|
|
|
|
QgsSymbol *QgsCategorizedSymbolRenderer::sourceSymbol()
|
|
{
|
|
return mSourceSymbol.get();
|
|
}
|
|
|
|
const QgsSymbol *QgsCategorizedSymbolRenderer::sourceSymbol() const
|
|
{
|
|
return mSourceSymbol.get();
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::setSourceSymbol( QgsSymbol *sym )
|
|
{
|
|
mSourceSymbol.reset( sym );
|
|
}
|
|
|
|
QgsColorRamp *QgsCategorizedSymbolRenderer::sourceColorRamp()
|
|
{
|
|
return mSourceColorRamp.get();
|
|
}
|
|
|
|
const QgsColorRamp *QgsCategorizedSymbolRenderer::sourceColorRamp() const
|
|
{
|
|
return mSourceColorRamp.get();
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::setSourceColorRamp( QgsColorRamp *ramp )
|
|
{
|
|
mSourceColorRamp.reset( ramp );
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::updateColorRamp( QgsColorRamp *ramp )
|
|
{
|
|
setSourceColorRamp( ramp );
|
|
double num = mCategories.count() - 1;
|
|
double count = 0;
|
|
|
|
QgsRandomColorRamp *randomRamp = dynamic_cast<QgsRandomColorRamp *>( ramp );
|
|
if ( randomRamp )
|
|
{
|
|
//ramp is a random colors ramp, so inform it of the total number of required colors
|
|
//this allows the ramp to pregenerate a set of visually distinctive colors
|
|
randomRamp->setTotalColorCount( mCategories.count() );
|
|
}
|
|
|
|
for ( const QgsRendererCategory &cat : mCategories )
|
|
{
|
|
double value = count / num;
|
|
cat.symbol()->setColor( mSourceColorRamp->color( value ) );
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::updateSymbols( QgsSymbol *sym )
|
|
{
|
|
int i = 0;
|
|
for ( const QgsRendererCategory &cat : mCategories )
|
|
{
|
|
QgsSymbol *symbol = sym->clone();
|
|
symbol->setColor( cat.symbol()->color() );
|
|
updateCategorySymbol( i, symbol );
|
|
++i;
|
|
}
|
|
setSourceSymbol( sym->clone() );
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::legendSymbolItemsCheckable() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool QgsCategorizedSymbolRenderer::legendSymbolItemChecked( const QString &key )
|
|
{
|
|
for ( const QgsRendererCategory &category : std::as_const( mCategories ) )
|
|
{
|
|
if ( category.uuid() == key )
|
|
{
|
|
return category.renderState();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::setLegendSymbolItem( const QString &key, QgsSymbol *symbol )
|
|
{
|
|
bool ok = false;
|
|
int i = 0;
|
|
for ( i = 0; i < mCategories.size(); i++ )
|
|
{
|
|
if ( mCategories[i].uuid() == key )
|
|
{
|
|
ok = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( ok )
|
|
updateCategorySymbol( i, symbol );
|
|
else
|
|
delete symbol;
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::checkLegendSymbolItem( const QString &key, bool state )
|
|
{
|
|
for ( int i = 0; i < mCategories.size(); i++ )
|
|
{
|
|
if ( mCategories[i].uuid() == key )
|
|
{
|
|
updateCategoryRenderState( i, state );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsCategorizedSymbolRenderer *QgsCategorizedSymbolRenderer::convertFromRenderer( const QgsFeatureRenderer *renderer, QgsVectorLayer *layer )
|
|
{
|
|
std::unique_ptr< QgsCategorizedSymbolRenderer > r;
|
|
if ( renderer->type() == QLatin1String( "categorizedSymbol" ) )
|
|
{
|
|
r.reset( static_cast<QgsCategorizedSymbolRenderer *>( renderer->clone() ) );
|
|
}
|
|
else if ( renderer->type() == QLatin1String( "graduatedSymbol" ) )
|
|
{
|
|
const QgsGraduatedSymbolRenderer *graduatedSymbolRenderer = dynamic_cast<const QgsGraduatedSymbolRenderer *>( renderer );
|
|
if ( graduatedSymbolRenderer )
|
|
{
|
|
r.reset( new QgsCategorizedSymbolRenderer( QString(), QgsCategoryList() ) );
|
|
if ( graduatedSymbolRenderer->sourceSymbol() )
|
|
r->setSourceSymbol( graduatedSymbolRenderer->sourceSymbol()->clone() );
|
|
if ( graduatedSymbolRenderer->sourceColorRamp() )
|
|
{
|
|
r->setSourceColorRamp( graduatedSymbolRenderer->sourceColorRamp()->clone() );
|
|
}
|
|
r->setClassAttribute( graduatedSymbolRenderer->classAttribute() );
|
|
}
|
|
}
|
|
else if ( renderer->type() == QLatin1String( "pointDisplacement" ) || renderer->type() == QLatin1String( "pointCluster" ) )
|
|
{
|
|
const QgsPointDistanceRenderer *pointDistanceRenderer = dynamic_cast<const QgsPointDistanceRenderer *>( renderer );
|
|
if ( pointDistanceRenderer )
|
|
r.reset( convertFromRenderer( pointDistanceRenderer->embeddedRenderer() ) );
|
|
}
|
|
else if ( renderer->type() == QLatin1String( "invertedPolygonRenderer" ) )
|
|
{
|
|
const QgsInvertedPolygonRenderer *invertedPolygonRenderer = dynamic_cast<const QgsInvertedPolygonRenderer *>( renderer );
|
|
if ( invertedPolygonRenderer )
|
|
r.reset( convertFromRenderer( invertedPolygonRenderer->embeddedRenderer() ) );
|
|
}
|
|
else if ( renderer->type() == QLatin1String( "embeddedSymbol" ) && layer )
|
|
{
|
|
const QgsEmbeddedSymbolRenderer *embeddedRenderer = dynamic_cast<const QgsEmbeddedSymbolRenderer *>( renderer );
|
|
QgsCategoryList categories;
|
|
QgsFeatureRequest req;
|
|
req.setFlags( Qgis::FeatureRequestFlag::EmbeddedSymbols | Qgis::FeatureRequestFlag::NoGeometry );
|
|
req.setNoAttributes();
|
|
QgsFeatureIterator it = layer->getFeatures( req );
|
|
QgsFeature feature;
|
|
while ( it.nextFeature( feature ) && categories.size() < 2000 )
|
|
{
|
|
if ( feature.embeddedSymbol() )
|
|
categories.append( QgsRendererCategory( feature.id(), feature.embeddedSymbol()->clone(), QString::number( feature.id() ) ) );
|
|
}
|
|
categories.append( QgsRendererCategory( QVariant(), embeddedRenderer->defaultSymbol()->clone(), QString() ) );
|
|
r.reset( new QgsCategorizedSymbolRenderer( QStringLiteral( "$id" ), categories ) );
|
|
}
|
|
|
|
// If not one of the specifically handled renderers, then just grab the symbol from the renderer
|
|
// Could have applied this to specific renderer types (singleSymbol, graduatedSymbol)
|
|
|
|
if ( !r )
|
|
{
|
|
r = std::make_unique< QgsCategorizedSymbolRenderer >( QString(), QgsCategoryList() );
|
|
QgsRenderContext context;
|
|
QgsSymbolList symbols = const_cast<QgsFeatureRenderer *>( renderer )->symbols( context );
|
|
if ( !symbols.isEmpty() )
|
|
{
|
|
QgsSymbol *newSymbol = symbols.at( 0 )->clone();
|
|
QgsSymbolLayerUtils::resetSymbolLayerIds( newSymbol );
|
|
QgsSymbolLayerUtils::clearSymbolLayerMasks( newSymbol );
|
|
r->setSourceSymbol( newSymbol );
|
|
}
|
|
}
|
|
|
|
renderer->copyRendererData( r.get() );
|
|
|
|
return r.release();
|
|
}
|
|
|
|
void QgsCategorizedSymbolRenderer::setDataDefinedSizeLegend( QgsDataDefinedSizeLegend *settings )
|
|
{
|
|
mDataDefinedSizeLegend.reset( settings );
|
|
}
|
|
|
|
QgsDataDefinedSizeLegend *QgsCategorizedSymbolRenderer::dataDefinedSizeLegend() const
|
|
{
|
|
return mDataDefinedSizeLegend.get();
|
|
}
|
|
|
|
int QgsCategorizedSymbolRenderer::matchToSymbols( QgsStyle *style, Qgis::SymbolType type, QVariantList &unmatchedCategories, QStringList &unmatchedSymbols, const bool caseSensitive, const bool useTolerantMatch )
|
|
{
|
|
if ( !style )
|
|
return 0;
|
|
|
|
int matched = 0;
|
|
unmatchedSymbols = style->symbolNames();
|
|
const QSet< QString > allSymbolNames( unmatchedSymbols.begin(), unmatchedSymbols.end() );
|
|
|
|
const thread_local QRegularExpression tolerantMatchRe( QStringLiteral( "[^\\w\\d ]" ), QRegularExpression::UseUnicodePropertiesOption );
|
|
|
|
for ( int catIdx = 0; catIdx < mCategories.count(); ++catIdx )
|
|
{
|
|
const QVariant value = mCategories.at( catIdx ).value();
|
|
const QString val = value.toString().trimmed();
|
|
std::unique_ptr< QgsSymbol > symbol( style->symbol( val ) );
|
|
// case-sensitive match
|
|
if ( symbol && symbol->type() == type )
|
|
{
|
|
matched++;
|
|
unmatchedSymbols.removeAll( val );
|
|
updateCategorySymbol( catIdx, symbol.release() );
|
|
continue;
|
|
}
|
|
|
|
if ( !caseSensitive || useTolerantMatch )
|
|
{
|
|
QString testVal = val;
|
|
if ( useTolerantMatch )
|
|
testVal.replace( tolerantMatchRe, QString() );
|
|
|
|
bool foundMatch = false;
|
|
for ( const QString &name : allSymbolNames )
|
|
{
|
|
QString testName = name.trimmed();
|
|
if ( useTolerantMatch )
|
|
testName.replace( tolerantMatchRe, QString() );
|
|
|
|
if ( testName == testVal || ( !caseSensitive && testName.trimmed().compare( testVal, Qt::CaseInsensitive ) == 0 ) )
|
|
{
|
|
// found a case-insensitive match
|
|
std::unique_ptr< QgsSymbol > symbol( style->symbol( name ) );
|
|
if ( symbol && symbol->type() == type )
|
|
{
|
|
matched++;
|
|
unmatchedSymbols.removeAll( name );
|
|
updateCategorySymbol( catIdx, symbol.release() );
|
|
foundMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ( foundMatch )
|
|
continue;
|
|
}
|
|
|
|
unmatchedCategories << value;
|
|
}
|
|
|
|
return matched;
|
|
}
|
|
|
|
QgsCategoryList QgsCategorizedSymbolRenderer::createCategories( const QList<QVariant> &values, const QgsSymbol *symbol, QgsVectorLayer *layer, const QString &attributeName )
|
|
{
|
|
QgsCategoryList cats;
|
|
QVariantList vals = values;
|
|
// sort the categories first
|
|
QgsSymbolLayerUtils::sortVariantList( vals, Qt::AscendingOrder );
|
|
|
|
if ( layer && !attributeName.isNull() )
|
|
{
|
|
const QgsFields fields = layer->fields();
|
|
for ( const QVariant &value : vals )
|
|
{
|
|
QgsSymbol *newSymbol = symbol->clone();
|
|
QgsSymbolLayerUtils::resetSymbolLayerIds( newSymbol );
|
|
if ( !QgsVariantUtils::isNull( value ) )
|
|
{
|
|
const int fieldIdx = fields.lookupField( attributeName );
|
|
QString categoryName = displayString( value );
|
|
if ( fieldIdx != -1 )
|
|
{
|
|
const QgsField field = fields.at( fieldIdx );
|
|
const QgsEditorWidgetSetup setup = field.editorWidgetSetup();
|
|
const QgsFieldFormatter *formatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() );
|
|
categoryName = formatter->representValue( layer, fieldIdx, setup.config(), QVariant(), value );
|
|
}
|
|
cats.append( QgsRendererCategory( value, newSymbol, categoryName, true ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
// add null (default) value
|
|
QgsSymbol *newSymbol = symbol->clone();
|
|
QgsSymbolLayerUtils::resetSymbolLayerIds( newSymbol );
|
|
cats.append( QgsRendererCategory( QVariant(), newSymbol, QString(), true ) );
|
|
|
|
return cats;
|
|
}
|