Skip to content

Commit

Permalink
[FEATURE] Add option to merge categories in categorized renderer
Browse files Browse the repository at this point in the history
This allows users to select multiple existing categories and
group them into a single category, which applies to any of the
values from the selection.

This allows simpler styling of layers with a large number of
categories, where it may be possible to group numerous distinct
categories into a smaller, more managable set of categories
which apply to multiple values.

The option is available from the right click context menu
in the categories list view, whenever multiple categories
are selected.

Sponsored by SMEC/SJ
  • Loading branch information
nyalldawson committed Jan 8, 2019
1 parent 224df0a commit 7b245d1
Show file tree
Hide file tree
Showing 5 changed files with 476 additions and 37 deletions.
215 changes: 180 additions & 35 deletions src/gui/symbology/qgscategorizedsymbolrendererwidget.cpp
Expand Up @@ -125,40 +125,97 @@ QVariant QgsCategorizedSymbolRendererModel::data( const QModelIndex &index, int

const QgsRendererCategory category = mRenderer->categories().value( index.row() );

if ( role == Qt::CheckStateRole && index.column() == 0 )
switch ( role )
{
return category.renderState() ? Qt::Checked : Qt::Unchecked;
}
else if ( role == Qt::DisplayRole || role == Qt::ToolTipRole )
{
switch ( index.column() )
case Qt::CheckStateRole:
{
case 1:
return category.value().toString();
case 2:
return category.label();
default:
return QVariant();
if ( index.column() == 0 )
{
return category.renderState() ? Qt::Checked : Qt::Unchecked;
}
break;
}
}
else if ( role == Qt::DecorationRole && index.column() == 0 && category.symbol() )
{
return QgsSymbolLayerUtils::symbolPreviewIcon( category.symbol(), QSize( 16, 16 ) );
}
else if ( role == Qt::TextAlignmentRole )
{
return ( index.column() == 0 ) ? Qt::AlignHCenter : Qt::AlignLeft;
}
else if ( role == Qt::EditRole )
{
switch ( index.column() )

case Qt::DisplayRole:
case Qt::ToolTipRole:
{
switch ( index.column() )
{
case 1:
{
if ( category.value().type() == QVariant::List )
{
QStringList res;
const QVariantList list = category.value().toList();
res.reserve( list.size() );
for ( const QVariant &v : list )
res << v.toString();

return res.join( ';' );
}
else
{
return category.value().toString();
}
}
case 2:
return category.label();
}
break;
}

case Qt::DecorationRole:
{
if ( index.column() == 0 && category.symbol() )
{
return QgsSymbolLayerUtils::symbolPreviewIcon( category.symbol(), QSize( 16, 16 ) );
}
break;
}

case Qt::ForegroundRole:
{
case 1:
return category.value();
case 2:
return category.label();
default:
return QVariant();
QBrush brush( qApp->palette().color( QPalette::Text ), Qt::SolidPattern );
if ( index.column() == 1 && category.value().type() == QVariant::List )
{
QColor fadedTextColor = brush.color();
fadedTextColor.setAlpha( 128 );
brush.setColor( fadedTextColor );
}
return brush;
}

case Qt::TextAlignmentRole:
{
return ( index.column() == 0 ) ? Qt::AlignHCenter : Qt::AlignLeft;
}

case Qt::EditRole:
{
switch ( index.column() )
{
case 1:
{
if ( category.value().type() == QVariant::List )
{
QStringList res;
const QVariantList list = category.value().toList();
res.reserve( list.size() );
for ( const QVariant &v : list )
res << v.toString();

return res.join( ';' );
}
else
{
return category.value();
}
}

case 2:
return category.label();
}
break;
}
}

Expand Down Expand Up @@ -194,6 +251,20 @@ bool QgsCategorizedSymbolRendererModel::setData( const QModelIndex &index, const
case QVariant::Double:
val = value.toDouble();
break;
case QVariant::List:
{
const QStringList parts = value.toString().split( ';' );
QVariantList list;
list.reserve( parts.count() );
for ( const QString &p : parts )
list << p;

if ( list.count() == 1 )
val = list.at( 0 );
else
val = list;
break;
}
default:
val = value.toString();
break;
Expand Down Expand Up @@ -392,7 +463,7 @@ QgsRendererWidget *QgsCategorizedSymbolRendererWidget::create( QgsVectorLayer *l

QgsCategorizedSymbolRendererWidget::QgsCategorizedSymbolRendererWidget( QgsVectorLayer *layer, QgsStyle *style, QgsFeatureRenderer *renderer )
: QgsRendererWidget( layer, style )

, mContextMenu( new QMenu( this ) )
{

// try to recognize the previous renderer
Expand Down Expand Up @@ -450,7 +521,7 @@ QgsCategorizedSymbolRendererWidget::QgsCategorizedSymbolRendererWidget( QgsVecto
connect( mExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString & ) >( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsCategorizedSymbolRendererWidget::categoryColumnChanged );

connect( viewCategories, &QAbstractItemView::doubleClicked, this, &QgsCategorizedSymbolRendererWidget::categoriesDoubleClicked );
connect( viewCategories, &QTreeView::customContextMenuRequested, this, &QgsCategorizedSymbolRendererWidget::contextMenuViewCategories );
connect( viewCategories, &QTreeView::customContextMenuRequested, this, &QgsCategorizedSymbolRendererWidget::showContextMenu );

connect( btnChangeCategorizedSymbol, &QAbstractButton::clicked, this, &QgsCategorizedSymbolRendererWidget::changeCategorizedSymbol );
connect( btnAddCategories, &QAbstractButton::clicked, this, &QgsCategorizedSymbolRendererWidget::addCategories );
Expand All @@ -476,6 +547,9 @@ QgsCategorizedSymbolRendererWidget::QgsCategorizedSymbolRendererWidget( QgsVecto
btnAdvanced->setMenu( advMenu );

mExpressionWidget->registerExpressionContextGenerator( this );

mMergeCategoriesAction = new QAction( tr( "Merge Categories" ), this );
connect( mMergeCategoriesAction, &QAction::triggered, this, &QgsCategorizedSymbolRendererWidget::mergeClicked );
}

QgsCategorizedSymbolRendererWidget::~QgsCategorizedSymbolRendererWidget()
Expand Down Expand Up @@ -720,11 +794,28 @@ void QgsCategorizedSymbolRendererWidget::addCategories()
QVariant value = cats.at( i ).value();
for ( int j = 0; j < prevCats.size() && !contains; ++j )
{
if ( prevCats.at( j ).value() == value )
const QVariant prevCatValue = prevCats.at( j ).value();
if ( prevCatValue.type() == QVariant::List )
{
contains = true;
break;
const QVariantList list = prevCatValue.toList();
for ( const QVariant &v : list )
{
if ( v == value )
{
contains = true;
break;
}
}
}
else
{
if ( prevCats.at( j ).value() == value )
{
contains = true;
}
}
if ( contains )
break;
}

if ( !contains )
Expand Down Expand Up @@ -1061,3 +1152,57 @@ void QgsCategorizedSymbolRendererWidget::dataDefinedSizeLegend()
openPanel( panel ); // takes ownership of the panel
}
}

void QgsCategorizedSymbolRendererWidget::mergeClicked()
{
QList<int> categoryIndexes = selectedCategories();
if ( categoryIndexes.count() < 2 )
return;

const QgsCategoryList &categories = mRenderer->categories();

QStringList labels;
QVariantList values;
values.reserve( categoryIndexes.count() );
labels.reserve( categoryIndexes.count() );
for ( int i : categoryIndexes )
{
QVariant v = categories.at( i ).value();
if ( v.type() == QVariant::List )
{
values.append( v.toList() );
}
else
values << v;

labels << categories.at( i ).label();
}

// modify first category (basically we "merge up" into the first selected category)
mRenderer->updateCategoryLabel( categoryIndexes.at( 0 ), labels.join( ',' ) );
mRenderer->updateCategoryValue( categoryIndexes.at( 0 ), values );

categoryIndexes.pop_front();
mModel->deleteRows( categoryIndexes );

emit widgetChanged();
}

void QgsCategorizedSymbolRendererWidget::showContextMenu( QPoint )
{
mContextMenu->clear();
const QList< QAction * > actions = contextMenu->actions();
for ( QAction *act : actions )
{
mContextMenu->addAction( act );
}

mContextMenu->addSeparator();

if ( viewCategories->selectionModel()->selectedRows().count() > 1 )
{
mContextMenu->addAction( mMergeCategoriesAction );
}

mContextMenu->exec( QCursor::pos() );
}
6 changes: 6 additions & 0 deletions src/gui/symbology/qgscategorizedsymbolrendererwidget.h
Expand Up @@ -153,6 +153,8 @@ class GUI_EXPORT QgsCategorizedSymbolRendererWidget : public QgsRendererWidget,
void cleanUpSymbolSelector( QgsPanelWidget *container );
void updateSymbolsFromWidget();
void dataDefinedSizeLegend();
void mergeClicked();
void showContextMenu( QPoint p );

protected:

Expand Down Expand Up @@ -191,8 +193,12 @@ class GUI_EXPORT QgsCategorizedSymbolRendererWidget : public QgsRendererWidget,
private:
QString mOldClassificationAttribute;
QgsCategoryList mCopyBuffer;
QMenu *mContextMenu = nullptr;
QAction *mMergeCategoriesAction = nullptr;

QgsExpressionContext createExpressionContext() const override;

friend class TestQgsCategorizedRendererWidget;
};

#endif // QGSCATEGORIZEDSYMBOLRENDERERWIDGET_H
36 changes: 34 additions & 2 deletions src/ui/qgscategorizedsymbolrendererwidget.ui
Expand Up @@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>378</width>
<width>424</width>
<height>368</height>
</rect>
</property>
Expand Down Expand Up @@ -216,15 +216,47 @@
<tabstop>mExpressionWidget</tabstop>
<tabstop>btnChangeCategorizedSymbol</tabstop>
<tabstop>btnColorRamp</tabstop>
<tabstop>viewCategories</tabstop>
<tabstop>btnAddCategories</tabstop>
<tabstop>btnAddCategory</tabstop>
<tabstop>btnDeleteCategories</tabstop>
<tabstop>btnDeleteAllCategories</tabstop>
<tabstop>btnAdvanced</tabstop>
<tabstop>viewCategories</tabstop>
</tabstops>
<resources>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
</resources>
<connections/>
</ui>
1 change: 1 addition & 0 deletions tests/src/gui/CMakeLists.txt
Expand Up @@ -121,6 +121,7 @@ ADD_QGIS_TEST(edittooltest testqgsmaptooledit.cpp)
#ADD_EXECUTABLE(qgis_rendererv2gui ${rendererv2gui_SRCS} ${rendererv2gui_MOC_SRCS})

#ADD_QGIS_TEST(histogramtest testqgsrasterhistogram.cpp)
ADD_QGIS_TEST(categorizedrendererwidget testqgscategorizedrendererwidget.cpp)
ADD_QGIS_TEST(doublespinbox testqgsdoublespinbox.cpp)
ADD_QGIS_TEST(dualviewtest testqgsdualview.cpp)
ADD_QGIS_TEST(attributeformtest testqgsattributeform.cpp)
Expand Down

0 comments on commit 7b245d1

Please sign in to comment.