28 #include <QColorDialog> 
   29 #include <QInputDialog> 
   30 #include <QFileDialog> 
   31 #include <QMessageBox> 
   34 #include <QTextStream> 
   36 #ifdef ENABLE_MODELTEST 
   37 #include "modeltest.h" 
   45   mCalculatingProgressBar->hide();
 
   46   mCancelButton->hide();
 
   48   mContextMenu = 
new QMenu( tr( 
"Options" ), 
this );
 
   49   mContextMenu->addAction( tr( 
"Change Color…" ), 
this, SLOT( changeColor() ) );
 
   50   mContextMenu->addAction( tr( 
"Change Opacity…" ), 
this, SLOT( changeOpacity() ) );
 
   51   mContextMenu->addAction( tr( 
"Change Label…" ), 
this, SLOT( changeLabel() ) );
 
   53   mAdvancedMenu = 
new QMenu( tr( 
"Advanced Options" ), 
this );
 
   54   QAction *mLoadFromLayerAction = mAdvancedMenu->addAction( tr( 
"Load Classes from Layer" ) );
 
   55   connect( mLoadFromLayerAction, &QAction::triggered, 
this, &QgsPalettedRendererWidget::loadFromLayer );
 
   56   QAction *loadFromFile = mAdvancedMenu->addAction( tr( 
"Load Color Map from File…" ) );
 
   57   connect( loadFromFile, &QAction::triggered, 
this, &QgsPalettedRendererWidget::loadColorTable );
 
   58   QAction *exportToFile = mAdvancedMenu->addAction( tr( 
"Export Color Map to File…" ) );
 
   59   connect( exportToFile, &QAction::triggered, 
this, &QgsPalettedRendererWidget::saveColorTable );
 
   62   mButtonAdvanced->setMenu( mAdvancedMenu );
 
   64   mModel = 
new QgsPalettedRendererModel( 
this );
 
   65   mProxyModel = 
new QgsPalettedRendererProxyModel( 
this );
 
   66   mProxyModel->setSourceModel( mModel );
 
   67   mTreeView->setSortingEnabled( 
false );
 
   68   mTreeView->setModel( mProxyModel );
 
   72     mProxyModel->sort( QgsPalettedRendererModel::Column::ValueColumn );
 
   75 #ifdef ENABLE_MODELTEST 
   76   new ModelTest( mModel, 
this );
 
   79   mTreeView->setItemDelegateForColumn( QgsPalettedRendererModel::ColorColumn, 
new QgsColorSwatchDelegate( 
this ) );
 
   80   mValueDelegate = 
new QgsLocaleAwareNumericLineEditDelegate( Qgis::DataType::UnknownDataType, 
this );
 
   81   mTreeView->setItemDelegateForColumn( QgsPalettedRendererModel::ValueColumn, mValueDelegate );
 
   83 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) 
   84   mTreeView->setColumnWidth( QgsPalettedRendererModel::ColorColumn, 
Qgis::UI_SCALE_FACTOR * fontMetrics().width( 
'X' ) * 6.6 );
 
   86   mTreeView->setColumnWidth( QgsPalettedRendererModel::ColorColumn, 
Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 
'X' ) * 6.6 );
 
   88   mTreeView->setContextMenuPolicy( Qt::CustomContextMenu );
 
   89   mTreeView->setSelectionMode( QAbstractItemView::ExtendedSelection );
 
   90   mTreeView->setDragEnabled( 
true );
 
   91   mTreeView->setAcceptDrops( 
true );
 
   92   mTreeView->setDropIndicatorShown( 
true );
 
   93   mTreeView->setDragDropMode( QAbstractItemView::InternalMove );
 
   94   mTreeView->setSelectionBehavior( QAbstractItemView::SelectRows );
 
   95   mTreeView->setDefaultDropAction( Qt::MoveAction );
 
   97   connect( mTreeView, &QTreeView::customContextMenuRequested, 
this, [ = ]( QPoint ) { mContextMenu->exec( QCursor::pos() ); } );
 
   99   btnColorRamp->setShowRandomColorRamp( 
true );
 
  117   connect( mDeleteEntryButton, &QPushButton::clicked, 
this, &QgsPalettedRendererWidget::deleteEntry );
 
  118   connect( mButtonDeleteAll, &QPushButton::clicked, mModel, &QgsPalettedRendererModel::deleteAll );
 
  119   connect( mAddEntryButton, &QPushButton::clicked, 
this, &QgsPalettedRendererWidget::addEntry );
 
  120   connect( mClassifyButton, &QPushButton::clicked, 
this, &QgsPalettedRendererWidget::classify );
 
  128     mLoadFromLayerAction->setEnabled( 
false );
 
  147   int bandNumber = mBandComboBox->currentBand();
 
  150   if ( !btnColorRamp->isNull() )
 
  166     mModel->setClassData( pr->
classes() );
 
  191 void QgsPalettedRendererWidget::setSelectionColor( 
const QItemSelection &selection, 
const QColor &color )
 
  196   QModelIndex colorIndex;
 
  197   const auto constSelection = selection;
 
  198   for ( 
const QItemSelectionRange &range : constSelection )
 
  200     const auto constIndexes = range.indexes();
 
  201     for ( 
const QModelIndex &index : constIndexes )
 
  203       colorIndex = mModel->index( index.row(), QgsPalettedRendererModel::ColorColumn );
 
  204       mModel->setData( colorIndex, color, Qt::EditRole );
 
  212 void QgsPalettedRendererWidget::deleteEntry()
 
  217   QItemSelection sel = mProxyModel->mapSelectionToSource( mTreeView->selectionModel()->selection() );
 
  218   const auto constSel = sel;
 
  219   for ( 
const QItemSelectionRange &range : constSel )
 
  221     if ( range.isValid() )
 
  222       mModel->removeRows( range.top(), range.bottom() - range.top() + 1, range.parent() );
 
  230 void QgsPalettedRendererWidget::addEntry()
 
  234   QColor color( 150, 150, 150 );
 
  235   std::unique_ptr< QgsColorRamp > ramp( btnColorRamp->colorRamp() );
 
  238     color = ramp->color( 1.0 );
 
  240   QModelIndex newEntry = mModel->addEntry( color );
 
  241   mTreeView->scrollTo( newEntry );
 
  242   mTreeView->selectionModel()->select( mProxyModel->mapFromSource( newEntry ), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
 
  247 void QgsPalettedRendererWidget::changeColor()
 
  249   QItemSelection sel = mProxyModel->mapSelectionToSource( mTreeView->selectionModel()->selection() );
 
  250   QModelIndex colorIndex = mModel->index( sel.first().top(), QgsPalettedRendererModel::ColorColumn );
 
  251   QColor currentColor = mModel->data( colorIndex, Qt::DisplayRole ).value<QColor>();
 
  254   if ( panel && panel->dockMode() )
 
  260     panel->openPanel( colorWidget );
 
  266     if ( newColor.isValid() )
 
  268       setSelectionColor( sel, newColor );
 
  273 void QgsPalettedRendererWidget::changeOpacity()
 
  275   QItemSelection sel = mProxyModel->mapSelectionToSource( mTreeView->selectionModel()->selection() );
 
  276   QModelIndex colorIndex = mModel->index( sel.first().top(), QgsPalettedRendererModel::ColorColumn );
 
  277   QColor currentColor = mModel->data( colorIndex, Qt::DisplayRole ).value<QColor>();
 
  280   double oldOpacity = ( currentColor.alpha() / 255.0 ) * 100.0;
 
  281   double opacity = QInputDialog::getDouble( 
this, tr( 
"Opacity" ), tr( 
"Change color opacity [%]" ), oldOpacity, 0.0, 100.0, 0, &ok );
 
  284     int newOpacity = opacity / 100 * 255;
 
  289     const auto constSel = sel;
 
  290     for ( 
const QItemSelectionRange &range : constSel )
 
  292       const auto constIndexes = range.indexes();
 
  293       for ( 
const QModelIndex &index : constIndexes )
 
  295         colorIndex = mModel->index( index.row(), QgsPalettedRendererModel::ColorColumn );
 
  297         QColor newColor = mModel->data( colorIndex, Qt::DisplayRole ).value<QColor>();
 
  298         newColor.setAlpha( newOpacity );
 
  299         mModel->setData( colorIndex, newColor, Qt::EditRole );
 
  308 void QgsPalettedRendererWidget::changeLabel()
 
  310   QItemSelection sel = mProxyModel->mapSelectionToSource( mTreeView->selectionModel()->selection() );
 
  311   QModelIndex labelIndex = mModel->index( sel.first().top(), QgsPalettedRendererModel::LabelColumn );
 
  312   QString currentLabel = mModel->data( labelIndex, Qt::DisplayRole ).toString();
 
  315   QString newLabel = QInputDialog::getText( 
this, tr( 
"Label" ), tr( 
"Change label" ), QLineEdit::Normal, currentLabel, &ok );
 
  321     const auto constSel = sel;
 
  322     for ( 
const QItemSelectionRange &range : constSel )
 
  324       const auto constIndexes = range.indexes();
 
  325       for ( 
const QModelIndex &index : constIndexes )
 
  327         labelIndex = mModel->index( index.row(), QgsPalettedRendererModel::LabelColumn );
 
  328         mModel->setData( labelIndex, newLabel, Qt::EditRole );
 
  337 void QgsPalettedRendererWidget::applyColorRamp()
 
  339   std::unique_ptr< QgsColorRamp > ramp( btnColorRamp->colorRamp() );
 
  348   QgsPalettedRasterRenderer::ClassData::iterator cIt = data.begin();
 
  350   double numberOfEntries = data.count();
 
  357     randomRamp->setTotalColorCount( numberOfEntries );
 
  360   if ( numberOfEntries > 1 )
 
  361     numberOfEntries -= 1; 
 
  363   for ( ; cIt != data.end(); ++cIt )
 
  365     cIt->color = ramp->color( i / numberOfEntries );
 
  368   mModel->setClassData( data );
 
  374 void QgsPalettedRendererWidget::loadColorTable()
 
  377   QString lastDir = settings.
value( QStringLiteral( 
"lastColorMapDir" ), QDir::homePath() ).toString();
 
  378   QString fileName = QFileDialog::getOpenFileName( 
this, tr( 
"Load Color Table from File" ), lastDir );
 
  379   if ( !fileName.isEmpty() )
 
  382     if ( !classes.isEmpty() )
 
  385       mModel->setClassData( classes );
 
  391       QMessageBox::critical( 
nullptr, tr( 
"Load Color Table" ), tr( 
"Could not interpret file as a raster color table." ) );
 
  396 void QgsPalettedRendererWidget::saveColorTable()
 
  399   QString lastDir = settings.
value( QStringLiteral( 
"lastColorMapDir" ), QDir::homePath() ).toString();
 
  400   QString fileName = QFileDialog::getSaveFileName( 
this, tr( 
"Save Color Table as File" ), lastDir, tr( 
"Text (*.clr)" ) );
 
  401   if ( !fileName.isEmpty() )
 
  403     if ( !fileName.endsWith( QLatin1String( 
".clr" ), Qt::CaseInsensitive ) )
 
  405       fileName = fileName + 
".clr";
 
  408     QFile outputFile( fileName );
 
  409     if ( outputFile.open( QFile::WriteOnly | QIODevice::Truncate ) )
 
  411       QTextStream outputStream( &outputFile );
 
  413       outputStream.flush();
 
  416       QFileInfo fileInfo( fileName );
 
  417       settings.
setValue( QStringLiteral( 
"lastColorMapDir" ), fileInfo.absoluteDir().absolutePath() );
 
  421       QMessageBox::warning( 
this, tr( 
"Save Color Table as File" ), tr( 
"Write access denied. Adjust the file permissions and try again.\n\n" ) );
 
  426 void QgsPalettedRendererWidget::classify()
 
  442     mGatherer = 
new QgsPalettedRendererClassGatherer( 
mRasterLayer, mBandComboBox->currentBand(), mModel->classData(), btnColorRamp->colorRamp() );
 
  444     connect( mGatherer, &QgsPalettedRendererClassGatherer::progressChanged, mCalculatingProgressBar, [ = ]( 
int progress )
 
  446       mCalculatingProgressBar->setValue( progress );
 
  449     mCalculatingProgressBar->show();
 
  450     mCancelButton->show();
 
  451     connect( mCancelButton, &QPushButton::clicked, mGatherer, &QgsPalettedRendererClassGatherer::stop );
 
  453     connect( mGatherer, &QgsPalettedRendererClassGatherer::collectedClasses, 
this, &QgsPalettedRendererWidget::gatheredClasses );
 
  454     connect( mGatherer, &QgsPalettedRendererClassGatherer::finished, 
this, &QgsPalettedRendererWidget::gathererThreadFinished );
 
  455     mClassifyButton->setText( tr( 
"Calculating…" ) );
 
  456     mClassifyButton->setEnabled( 
false );
 
  461 void QgsPalettedRendererWidget::loadFromLayer()
 
  467     QList<QgsColorRampShader::ColorRampItem> table = provider->
colorTable( mBandComboBox->currentBand() );
 
  468     if ( !table.isEmpty() )
 
  471       mModel->setClassData( classes );
 
  477 void QgsPalettedRendererWidget::bandChanged( 
int band )
 
  487   bool deleteExisting = 
false;
 
  488   if ( !mModel->classData().isEmpty() )
 
  490     int res = QMessageBox::question( 
this,
 
  491                                      tr( 
"Delete Classification" ),
 
  492                                      tr( 
"The classification band was changed from %1 to %2.\n" 
  493                                          "Should the existing classes be deleted?" ).arg( mBand ).arg( band ),
 
  494                                      QMessageBox::Yes | QMessageBox::No );
 
  496     deleteExisting = ( res == QMessageBox::Yes );
 
  500   mModel->blockSignals( 
true );
 
  501   if ( deleteExisting )
 
  504   mModel->blockSignals( 
false );
 
  508 void QgsPalettedRendererWidget::gatheredClasses()
 
  510   if ( !mGatherer || mGatherer->wasCanceled() )
 
  513   mModel->setClassData( mGatherer->classes() );
 
  517 void QgsPalettedRendererWidget::gathererThreadFinished()
 
  519   mGatherer->deleteLater();
 
  521   mClassifyButton->setText( tr( 
"Classify" ) );
 
  522   mClassifyButton->setEnabled( 
true );
 
  523   mCalculatingProgressBar->hide();
 
  524   mCancelButton->hide();
 
  527 void QgsPalettedRendererWidget::layerWillBeRemoved( 
QgsMapLayer *layer )
 
  541 QgsPalettedRendererModel::QgsPalettedRendererModel( QObject *parent )
 
  542   : QAbstractItemModel( parent )
 
  554 QModelIndex QgsPalettedRendererModel::index( 
int row, 
int column, 
const QModelIndex &parent )
 const 
  556   if ( column < 0 || column >= columnCount() )
 
  559     return QModelIndex();
 
  562   if ( !parent.isValid() && row >= 0 && row < mData.size() )
 
  565     return createIndex( row, column );
 
  569   return QModelIndex();
 
  572 QModelIndex QgsPalettedRendererModel::parent( 
const QModelIndex &index )
 const 
  577   return QModelIndex();
 
  580 int QgsPalettedRendererModel::columnCount( 
const QModelIndex &parent )
 const 
  582   if ( parent.isValid() )
 
  588 int QgsPalettedRendererModel::rowCount( 
const QModelIndex &parent )
 const 
  590   if ( parent.isValid() )
 
  593   return mData.count();
 
  596 QVariant QgsPalettedRendererModel::data( 
const QModelIndex &index, 
int role )
 const 
  598   if ( !index.isValid() )
 
  603     case Qt::DisplayRole:
 
  606       switch ( index.column() )
 
  609           return mData.at( index.row() ).value;
 
  612           return mData.at( index.row() ).color;
 
  615           return mData.at( index.row() ).label;
 
  626 QVariant QgsPalettedRendererModel::headerData( 
int section, Qt::Orientation orientation, 
int role )
 const 
  628   switch ( orientation )
 
  637         case Qt::DisplayRole:
 
  642               return tr( 
"Value" );
 
  645               return tr( 
"Color" );
 
  648               return tr( 
"Label" );
 
  657       return QAbstractItemModel::headerData( section, orientation, role );
 
  659   return QAbstractItemModel::headerData( section, orientation, role );
 
  662 bool QgsPalettedRendererModel::setData( 
const QModelIndex &index, 
const QVariant &value, 
int )
 
  664   if ( !index.isValid() )
 
  666   if ( index.row() >= mData.length() )
 
  669   switch ( index.column() )
 
  674       double newValue = value.toDouble( &ok );
 
  678       mData[ index.row() ].value = newValue;
 
  679       emit dataChanged( index, index );
 
  680       emit classesChanged();
 
  686       mData[ index.row() ].color = value.value<QColor>();
 
  687       emit dataChanged( index, index );
 
  688       emit classesChanged();
 
  694       mData[ index.row() ].label = value.toString();
 
  695       emit dataChanged( index, index );
 
  696       emit classesChanged();
 
  704 Qt::ItemFlags QgsPalettedRendererModel::flags( 
const QModelIndex &index )
 const 
  706   if ( !index.isValid() )
 
  707     return QAbstractItemModel::flags( index ) | Qt::ItemIsDropEnabled;
 
  709   Qt::ItemFlags f = QAbstractItemModel::flags( index );
 
  710   switch ( index.column() )
 
  715       f = f | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;
 
  718   return f | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
 
  721 bool QgsPalettedRendererModel::removeRows( 
int row, 
int count, 
const QModelIndex &parent )
 
  723   if ( row < 0 || row >= mData.count() )
 
  725   if ( parent.isValid() )
 
  728   for ( 
int i = row + count - 1; i >= row; --i )
 
  730     beginRemoveRows( parent, i, i );
 
  734   emit classesChanged();
 
  738 bool QgsPalettedRendererModel::insertRows( 
int row, 
int count, 
const QModelIndex & )
 
  740   QgsPalettedRasterRenderer::ClassData::const_iterator cIt = mData.constBegin();
 
  741   int currentMaxValue = -std::numeric_limits<int>::max();
 
  742   for ( ; cIt != mData.constEnd(); ++cIt )
 
  744     int value = cIt->value;
 
  745     currentMaxValue = std::max( value, currentMaxValue );
 
  747   int nextValue = std::max( 0, currentMaxValue + 1 );
 
  749   beginInsertRows( QModelIndex(), row, row + count - 1 );
 
  750   for ( 
int i = row; i < row + count; ++i, ++nextValue )
 
  755   emit classesChanged();
 
  759 Qt::DropActions QgsPalettedRendererModel::supportedDropActions()
 const 
  761   return Qt::MoveAction;
 
  764 QStringList QgsPalettedRendererModel::mimeTypes()
 const 
  767   types << QStringLiteral( 
"application/x-qgspalettedrenderermodel" );
 
  771 QMimeData *QgsPalettedRendererModel::mimeData( 
const QModelIndexList &indexes )
 const 
  773   QMimeData *mimeData = 
new QMimeData();
 
  774   QByteArray encodedData;
 
  776   QDataStream stream( &encodedData, QIODevice::WriteOnly );
 
  779   const auto constIndexes = indexes;
 
  780   for ( 
const QModelIndex &index : constIndexes )
 
  782     if ( !index.isValid() || index.column() != 0 )
 
  785     stream << index.row();
 
  787   mimeData->setData( QStringLiteral( 
"application/x-qgspalettedrenderermodel" ), encodedData );
 
  791 bool QgsPalettedRendererModel::dropMimeData( 
const QMimeData *data, Qt::DropAction action, 
int row, 
int column, 
const QModelIndex & )
 
  794   if ( action != Qt::MoveAction ) 
return true;
 
  796   if ( !data->hasFormat( QStringLiteral( 
"application/x-qgspalettedrenderermodel" ) ) )
 
  799   QByteArray encodedData = data->data( QStringLiteral( 
"application/x-qgspalettedrenderermodel" ) );
 
  800   QDataStream stream( &encodedData, QIODevice::ReadOnly );
 
  803   while ( !stream.atEnd() )
 
  811   for ( 
int i = 0; i < rows.count(); ++i )
 
  812     newData << mData.at( rows.at( i ) );
 
  817   beginInsertRows( QModelIndex(), row, row + rows.count() - 1 );
 
  818   for ( 
int i = 0; i < rows.count(); ++i )
 
  819     mData.insert( row + i, newData.at( i ) );
 
  821   emit classesChanged();
 
  825 QModelIndex QgsPalettedRendererModel::addEntry( 
const QColor &color )
 
  827   insertRow( rowCount() );
 
  828   QModelIndex newRow = index( mData.count() - 1, 1 );
 
  829   setData( newRow, color );
 
  833 void QgsPalettedRendererModel::deleteAll()
 
  838   emit classesChanged();
 
  841 void QgsPalettedRendererClassGatherer::run()
 
  843   mWasCanceled = 
false;
 
  852   QgsPalettedRasterRenderer::ClassData::iterator classIt = newClasses.begin();
 
  853   emit progressChanged( 0 );
 
  855   for ( ; classIt != newClasses.end(); ++classIt )
 
  860       if ( existingClass.value == classIt->value )
 
  862         classIt->color = existingClass.color;
 
  863         classIt->label = existingClass.label;
 
  868     emit progressChanged( 100 * ( i / 
static_cast<float>( newClasses.count() ) ) );
 
  870   mClasses = newClasses;
 
  873   mFeedbackMutex.lock();
 
  876   mFeedbackMutex.unlock();
 
  878   emit collectedClasses();
 
  885   for ( 
int i = 0; i < rowCount( ); ++i )
 
  887     data.push_back( qobject_cast<QgsPalettedRendererModel *>( sourceModel() )->classAtIndex( mapToSource( index( i, 0 ) ) ) );
 
static const double UI_SCALE_FACTOR
UI scaling factor.
static QColor getColor(const QColor &initialColor, QWidget *parent, const QString &title=QString(), bool allowOpacity=false)
Returns a color selection from a color dialog.
A delegate for showing a color swatch in a list.
void progressChanged(double progress)
Emitted when the feedback object reports a progress change.
Base class for all map layer types.
Renderer for paletted raster images.
int band() const
Returns the raster band used for rendering the raster.
QgsColorRamp * sourceColorRamp() const
Gets the source color ramp.
void setSourceColorRamp(QgsColorRamp *ramp)
Set the source color ramp.
QList< QgsPalettedRasterRenderer::Class > ClassData
Map of value to class properties.
static QgsPalettedRasterRenderer::ClassData classDataFromFile(const QString &path)
Opens a color table file and returns corresponding paletted renderer class data.
static QgsPalettedRasterRenderer::ClassData colorTableToClassData(const QList< QgsColorRampShader::ColorRampItem > &table)
Converts a raster color table to paletted renderer class data.
ClassData classes() const
Returns a map of value to classes (colors) used by the renderer.
static QgsPalettedRasterRenderer::ClassData classDataFromRaster(QgsRasterInterface *raster, int bandNumber, QgsColorRamp *ramp=nullptr, QgsRasterBlockFeedback *feedback=nullptr)
Generates class data from a raster, for the specified bandNumber.
static QString classDataToString(const QgsPalettedRasterRenderer::ClassData &classes)
Converts classes to a string representation, using the .clr/gdal color table file format.
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
static QgsProject * instance()
Returns the QgsProject singleton instance.
void layerWillBeRemoved(const QString &layerId)
Emitted when a layer is about to be removed from the registry.
Totally random color ramp.
void bandChanged(int band)
Emitted when the currently selected band changes.
Feedback object tailored for raster block reading.
Base class for raster data providers.
virtual QList< QgsColorRampShader::ColorRampItem > colorTable(int bandNo) const
Qgis::DataType dataType(int bandNo) const override=0
Returns data type for the band specified by number.
Represents a raster layer.
QgsRasterDataProvider * dataProvider() override
Returns the source data provider.
QgsRasterRenderer * renderer() const
Returns the raster's renderer.
Raster renderer pipe that applies colors to a raster.
A rectangle specified with double values.
This class is a composition of two QSettings instances:
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Properties of a single value class.