Skip to content

Commit 9c62eec

Browse files
committedAug 29, 2016
[FEATURE] Substitution list support for labeling
Adds the ability to specify a list of text substitutes to make which apply to label text. Eg abbrevating street types. Users can export and import lists of substitutes to make reuse and sharing easier. (cherry-picked from 46fba7c)
1 parent eef50ea commit 9c62eec

17 files changed

+1098
-3
lines changed
 

‎python/core/qgspallabeling.sip

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,11 @@ class QgsPalLayerSettings
388388
QPainter::CompositionMode blendMode;
389389
QColor previewBkgrdColor;
390390

391+
//! Substitution collection for automatic text substitution with labels
392+
QgsStringReplacementCollection substitutions;
393+
//! True if substitutions should be applied
394+
bool useSubstitutions;
395+
391396
//-- text formatting
392397

393398
QString wrapChar;

‎python/core/qgsstringutils.sip

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,115 @@
1+
2+
3+
/** \ingroup core
4+
* \class QgsStringReplacement
5+
* \brief A representation of a single string replacement.
6+
* \note Added in version 3.0
7+
*/
8+
9+
class QgsStringReplacement
10+
{
11+
%TypeHeaderCode
12+
#include <qgsstringutils.h>
13+
%End
14+
public:
15+
16+
/** Constructor for QgsStringReplacement.
17+
* @param match string to match
18+
* @param replacement string to replace match with
19+
* @param caseSensitive set to true for a case sensitive match
20+
* @param wholeWordOnly set to true to match complete words only, or false to allow partial word matches
21+
*/
22+
QgsStringReplacement( const QString& match,
23+
const QString& replacement,
24+
bool caseSensitive = false,
25+
bool wholeWordOnly = false );
26+
27+
//! Returns the string matched by this object
28+
QString match() const;
29+
30+
//! Returns the string to replace matches with
31+
QString replacement() const;
32+
33+
//! Returns true if match is case sensitive
34+
bool caseSensitive() const;
35+
36+
//! Returns true if match only applies to whole words, or false if partial word matches are permitted
37+
bool wholeWordOnly() const;
38+
39+
/** Processes a given input string, applying any valid replacements which should be made.
40+
* @param input input string
41+
* @returns input string with any matches replaced by replacement string
42+
*/
43+
QString process( const QString& input ) const;
44+
45+
bool operator==( const QgsStringReplacement& other );
46+
47+
/** Returns a map of the replacement properties.
48+
* @see fromProperties()
49+
*/
50+
QgsStringMap properties() const;
51+
52+
/** Creates a new QgsStringReplacement from an encoded properties map.
53+
* @see properties()
54+
*/
55+
static QgsStringReplacement fromProperties( const QgsStringMap& properties );
56+
57+
};
58+
59+
60+
/** \ingroup core
61+
* \class QgsStringReplacementCollection
62+
* \brief A collection of string replacements (specified using QgsStringReplacement objects).
63+
* \note Added in version 3.0
64+
*/
65+
66+
class QgsStringReplacementCollection
67+
{
68+
%TypeHeaderCode
69+
#include <qgsstringutils.h>
70+
%End
71+
public:
72+
73+
/** Constructor for QgsStringReplacementCollection
74+
* @param replacements initial list of string replacements
75+
*/
76+
QgsStringReplacementCollection( const QList< QgsStringReplacement >& replacements = QList< QgsStringReplacement >() );
77+
78+
/** Returns the list of string replacements in this collection.
79+
* @see setReplacements()
80+
*/
81+
QList< QgsStringReplacement > replacements() const;
82+
83+
/** Sets the list of string replacements in this collection.
84+
* @param replacements list of string replacements to apply. Replacements are applied in the
85+
* order they are specified here.
86+
* @see replacements()
87+
*/
88+
void setReplacements( const QList< QgsStringReplacement >& replacements );
89+
90+
/** Processes a given input string, applying any valid replacements which should be made
91+
* using QgsStringReplacement objects contained by this collection. Replacements
92+
* are made in order of the QgsStringReplacement objects contained in the collection.
93+
* @param input input string
94+
* @returns input string with any matches replaced by replacement string
95+
*/
96+
QString process( const QString& input ) const;
97+
98+
/** Writes the collection state to an XML element.
99+
* @param elem target DOM element
100+
* @param doc DOM document
101+
* @see readXml()
102+
*/
103+
void writeXml( QDomElement& elem, QDomDocument& doc ) const;
104+
105+
/** Reads the collection state from an XML element.
106+
* @param elem DOM element
107+
* @see writeXml()
108+
*/
109+
void readXml( const QDomElement& elem );
110+
};
111+
112+
1113
/** \ingroup core
2114
* \class QgsStringUtils
3115
* \brief Utility functions for working with strings.

‎src/app/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ SET(QGIS_APP_SRCS
115115
qgsrelationadddlg.cpp
116116
qgsselectbyformdialog.cpp
117117
qgsstatisticalsummarydockwidget.cpp
118+
qgssubstitutionlistwidget.cpp
118119
qgstextannotationdialog.cpp
119120
qgssnappingdialog.cpp
120121
qgssvgannotationdialog.cpp
@@ -292,6 +293,7 @@ SET (QGIS_APP_MOC_HDRS
292293
qgssnappingdialog.h
293294
qgssponsors.h
294295
qgsstatisticalsummarydockwidget.h
296+
qgssubstitutionlistwidget.h
295297
qgssvgannotationdialog.h
296298
qgstextannotationdialog.h
297299
qgstipgui.h

‎src/app/qgslabelinggui.cpp

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "qgssvgselectorwidget.h"
3636
#include "qgsvectorlayerlabeling.h"
3737
#include "qgslogger.h"
38+
#include "qgssubstitutionlistwidget.h"
3839

3940
#include <QCheckBox>
4041
#include <QSettings>
@@ -139,6 +140,7 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
139140
connect( mShadowTranspSlider, SIGNAL( valueChanged( int ) ), mShadowTranspSpnBx, SLOT( setValue( int ) ) );
140141
connect( mShadowTranspSpnBx, SIGNAL( valueChanged( int ) ), mShadowTranspSlider, SLOT( setValue( int ) ) );
141142
connect( mLimitLabelChkBox, SIGNAL( toggled( bool ) ), mLimitLabelSpinBox, SLOT( setEnabled( bool ) ) );
143+
connect( mCheckBoxSubstituteText, SIGNAL( toggled( bool ) ), mToolButtonConfigureSubstitutes, SLOT( setEnabled( bool ) ) );
142144

143145
//connections to prevent users removing all line placement positions
144146
connect( chkLineAbove, SIGNAL( toggled( bool ) ), this, SLOT( updateLinePlacementOptions() ) );
@@ -468,7 +470,8 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
468470
<< radPolygonPerimeter
469471
<< radPolygonPerimeterCurved
470472
<< radPredefinedOrder
471-
<< mFieldExpressionWidget;
473+
<< mFieldExpressionWidget
474+
<< mCheckBoxSubstituteText;
472475
connectValueChanged( widgets, SLOT( updatePreview() ) );
473476

474477
connect( mQuadrantBtnGrp, SIGNAL( buttonClicked( int ) ), this, SLOT( updatePreview() ) );
@@ -625,6 +628,8 @@ void QgsLabelingGui::init()
625628
// set the current field or add the current expression to the bottom of the list
626629
mFieldExpressionWidget->setRow( -1 );
627630
mFieldExpressionWidget->setField( lyr.fieldName );
631+
mCheckBoxSubstituteText->setChecked( lyr.useSubstitutions );
632+
mSubstitutions = lyr.substitutions;
628633

629634
// populate placement options
630635
mCentroidRadioWhole->setChecked( lyr.centroidWhole );
@@ -1015,6 +1020,8 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
10151020
lyr.scaleVisibility = mScaleBasedVisibilityChkBx->isChecked();
10161021
lyr.scaleMin = mScaleBasedVisibilityMinSpnBx->value();
10171022
lyr.scaleMax = mScaleBasedVisibilityMaxSpnBx->value();
1023+
lyr.useSubstitutions = mCheckBoxSubstituteText->isChecked();
1024+
lyr.substitutions = mSubstitutions;
10181025

10191026
// buffer
10201027
lyr.bufferDraw = mBufferDrawChkBx->isChecked();
@@ -1975,6 +1982,12 @@ void QgsLabelingGui::updateLinePlacementOptions()
19751982
}
19761983
}
19771984

1985+
void QgsLabelingGui::onSubstitutionsChanged( const QgsStringReplacementCollection& substitutions )
1986+
{
1987+
mSubstitutions = substitutions;
1988+
emit widgetChanged();
1989+
}
1990+
19781991
void QgsLabelingGui::updateSvgWidgets( const QString& svgPath )
19791992
{
19801993
if ( mShapeSVGPathLineEdit->text() != svgPath )
@@ -2101,6 +2114,28 @@ void QgsLabelingGui::on_mChkNoObstacle_toggled( bool active )
21012114
mObstaclePriorityFrame->setEnabled( active );
21022115
}
21032116

2117+
void QgsLabelingGui::on_mToolButtonConfigureSubstitutes_clicked()
2118+
{
2119+
QgsPanelWidget* panel = QgsPanelWidget::findParentPanel( this );
2120+
if ( panel && panel->dockMode() )
2121+
{
2122+
QgsSubstitutionListWidget* widget = new QgsSubstitutionListWidget( panel );
2123+
widget->setPanelTitle( tr( "Substitutions" ) );
2124+
widget->setSubstitutions( mSubstitutions );
2125+
connect( widget, SIGNAL( substitutionsChanged( QgsStringReplacementCollection ) ), this, SLOT( onSubstitutionsChanged( QgsStringReplacementCollection ) ) );
2126+
panel->openPanel( widget );
2127+
return;
2128+
}
2129+
2130+
QgsSubstitutionListDialog dlg( this );
2131+
dlg.setSubstitutions( mSubstitutions );
2132+
if ( dlg.exec() == QDialog::Accepted )
2133+
{
2134+
mSubstitutions = dlg.substitutions();
2135+
emit widgetChanged();
2136+
}
2137+
}
2138+
21042139
void QgsLabelingGui::showBackgroundRadius( bool show )
21052140
{
21062141
mShapeRadiusLabel->setVisible( show );

‎src/app/qgslabelinggui.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <QDialog>
2222
#include <QFontDatabase>
2323
#include <ui_qgslabelingguibase.h>
24+
#include "qgsstringutils.h"
2425

2526
class QgsVectorLayer;
2627
class QgsMapCanvas;
@@ -96,6 +97,8 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
9697
void on_mDirectSymbRightToolBtn_clicked();
9798
void on_mChkNoObstacle_toggled( bool active );
9899

100+
void on_mToolButtonConfigureSubstitutes_clicked();
101+
99102
protected:
100103
void blockInitSignals( bool block );
101104
void blockFontChangeSignals( bool blk );
@@ -135,6 +138,8 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
135138

136139
bool mLoadSvgParams;
137140

141+
QgsStringReplacementCollection mSubstitutions;
142+
138143
void enableDataDefinedAlignment( bool enable );
139144

140145
private slots:
@@ -143,6 +148,7 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
143148
void showBackgroundPenStyle( bool show );
144149
void on_mShapeSVGPathLineEdit_textChanged( const QString& text );
145150
void updateLinePlacementOptions();
151+
void onSubstitutionsChanged( const QgsStringReplacementCollection& substitutions );
146152
};
147153

148154
#endif

‎src/app/qgssubstitutionlistwidget.cpp

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/***************************************************************************
2+
qgssubstitutionlistwidget.cpp
3+
-----------------------------
4+
begin : August 2016
5+
copyright : (C) 2016 Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
8+
9+
***************************************************************************/
10+
11+
/***************************************************************************
12+
* *
13+
* This program is free software; you can redistribute it and/or modify *
14+
* it under the terms of the GNU General Public License as published by *
15+
* the Free Software Foundation; either version 2 of the License, or *
16+
* (at your option) any later version. *
17+
* *
18+
***************************************************************************/
19+
20+
#include "qgssubstitutionlistwidget.h"
21+
#include <QDialogButtonBox>
22+
#include <QCheckBox>
23+
#include <QFileDialog>
24+
#include <QMessageBox>
25+
#include <QTextStream>
26+
27+
QgsSubstitutionListWidget::QgsSubstitutionListWidget( QWidget* parent )
28+
: QgsPanelWidget( parent )
29+
{
30+
setupUi( this );
31+
connect( mTableSubstitutions, SIGNAL( cellChanged( int, int ) ), this, SLOT( tableChanged() ) );
32+
}
33+
34+
void QgsSubstitutionListWidget::setSubstitutions( const QgsStringReplacementCollection& substitutions )
35+
{
36+
mTableSubstitutions->blockSignals( true );
37+
mTableSubstitutions->clearContents();
38+
Q_FOREACH ( const QgsStringReplacement& replacement, substitutions.replacements() )
39+
{
40+
addSubstitution( replacement );
41+
}
42+
mTableSubstitutions->blockSignals( false );
43+
}
44+
45+
QgsStringReplacementCollection QgsSubstitutionListWidget::substitutions() const
46+
{
47+
QList< QgsStringReplacement > result;
48+
for ( int i = 0; i < mTableSubstitutions->rowCount(); ++i )
49+
{
50+
if ( !mTableSubstitutions->item( i, 0 ) )
51+
continue;
52+
53+
if ( mTableSubstitutions->item( i, 0 )->text().isEmpty() )
54+
continue;
55+
56+
QCheckBox* chkCaseSensitive = qobject_cast<QCheckBox*>( mTableSubstitutions->cellWidget( i, 2 ) );
57+
QCheckBox* chkWholeWord = qobject_cast<QCheckBox*>( mTableSubstitutions->cellWidget( i, 3 ) );
58+
59+
QgsStringReplacement replacement( mTableSubstitutions->item( i, 0 )->text(),
60+
mTableSubstitutions->item( i, 1 )->text(),
61+
chkCaseSensitive->isChecked(),
62+
chkWholeWord->isChecked() );
63+
result << replacement;
64+
}
65+
return QgsStringReplacementCollection( result );
66+
}
67+
68+
void QgsSubstitutionListWidget::on_mButtonAdd_clicked()
69+
{
70+
addSubstitution( QgsStringReplacement( QString(), QString(), false, true ) );
71+
mTableSubstitutions->setFocus();
72+
mTableSubstitutions->setCurrentCell( mTableSubstitutions->rowCount() - 1, 0 );
73+
}
74+
75+
void QgsSubstitutionListWidget::on_mButtonRemove_clicked()
76+
{
77+
int currentRow = mTableSubstitutions->currentRow();
78+
mTableSubstitutions->removeRow( currentRow );
79+
tableChanged();
80+
}
81+
82+
void QgsSubstitutionListWidget::tableChanged()
83+
{
84+
emit substitutionsChanged( substitutions() );
85+
}
86+
87+
void QgsSubstitutionListWidget::on_mButtonExport_clicked()
88+
{
89+
QString fileName = QFileDialog::getSaveFileName( this, tr( "Save substitutions" ), QDir::homePath(),
90+
tr( "XML files (*.xml *.XML)" ) );
91+
if ( fileName.isEmpty() )
92+
{
93+
return;
94+
}
95+
96+
// ensure the user never ommited the extension from the file name
97+
if ( !fileName.endsWith( ".xml", Qt::CaseInsensitive ) )
98+
{
99+
fileName += ".xml";
100+
}
101+
102+
QDomDocument doc;
103+
QDomElement root = doc.createElement( "substitutions" );
104+
root.setAttribute( "version", "1.0" );
105+
QgsStringReplacementCollection collection = substitutions();
106+
collection.writeXml( root, doc );
107+
doc.appendChild( root );
108+
109+
QFile file( fileName );
110+
if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
111+
{
112+
QMessageBox::warning( nullptr, tr( "Export substitutions" ),
113+
tr( "Cannot write file %1:\n%2." ).arg( fileName, file.errorString() ),
114+
QMessageBox::Ok,
115+
QMessageBox::Ok );
116+
return;
117+
}
118+
119+
QTextStream out( &file );
120+
doc.save( out, 4 );
121+
}
122+
123+
void QgsSubstitutionListWidget::on_mButtonImport_clicked()
124+
{
125+
QString fileName = QFileDialog::getOpenFileName( this, tr( "Load substitutions" ), QDir::homePath(),
126+
tr( "XML files (*.xml *.XML)" ) );
127+
if ( fileName.isEmpty() )
128+
{
129+
return;
130+
}
131+
132+
QFile file( fileName );
133+
if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
134+
{
135+
QMessageBox::warning( nullptr, tr( "Import substitutions" ),
136+
tr( "Cannot read file %1:\n%2." ).arg( fileName, file.errorString() ),
137+
QMessageBox::Ok,
138+
QMessageBox::Ok );
139+
return;
140+
}
141+
142+
QDomDocument doc;
143+
QString errorStr;
144+
int errorLine;
145+
int errorColumn;
146+
147+
if ( !doc.setContent( &file, true, &errorStr, &errorLine, &errorColumn ) )
148+
{
149+
QMessageBox::warning( nullptr, tr( "Import substitutions" ),
150+
tr( "Parse error at line %1, column %2:\n%3" )
151+
.arg( errorLine )
152+
.arg( errorColumn )
153+
.arg( errorStr ),
154+
QMessageBox::Ok,
155+
QMessageBox::Ok );
156+
return;
157+
}
158+
159+
QDomElement root = doc.documentElement();
160+
if ( root.tagName() != "substitutions" )
161+
{
162+
QMessageBox::warning( nullptr, tr( "Import substitutions" ),
163+
tr( "The selected file in not an substitutions list." ),
164+
QMessageBox::Ok,
165+
QMessageBox::Ok );
166+
return;
167+
}
168+
169+
QgsStringReplacementCollection collection;
170+
collection.readXml( root );
171+
setSubstitutions( collection );
172+
tableChanged();
173+
}
174+
175+
void QgsSubstitutionListWidget::addSubstitution( const QgsStringReplacement& substitution )
176+
{
177+
int row = mTableSubstitutions->rowCount();
178+
mTableSubstitutions->insertRow( row );
179+
180+
Qt::ItemFlags itemFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable
181+
| Qt::ItemIsEditable;
182+
183+
QTableWidgetItem* matchItem = new QTableWidgetItem( substitution.match() );
184+
matchItem->setFlags( itemFlags );
185+
mTableSubstitutions->setItem( row, 0, matchItem );
186+
QTableWidgetItem* replaceItem = new QTableWidgetItem( substitution.replacement() );
187+
replaceItem->setFlags( itemFlags );
188+
mTableSubstitutions->setItem( row, 1, replaceItem );
189+
190+
QCheckBox* caseSensitiveChk = new QCheckBox( this );
191+
caseSensitiveChk->setChecked( substitution.caseSensitive() );
192+
mTableSubstitutions->setCellWidget( row, 2, caseSensitiveChk );
193+
connect( caseSensitiveChk, SIGNAL( toggled( bool ) ), this, SLOT( tableChanged() ) );
194+
195+
QCheckBox* wholeWordChk = new QCheckBox( this );
196+
wholeWordChk->setChecked( substitution.wholeWordOnly() );
197+
mTableSubstitutions->setCellWidget( row, 3, wholeWordChk );
198+
connect( wholeWordChk, SIGNAL( toggled( bool ) ), this, SLOT( tableChanged() ) );
199+
}
200+
201+
202+
//
203+
// QgsSubstitutionListDialog
204+
//
205+
206+
207+
QgsSubstitutionListDialog::QgsSubstitutionListDialog( QWidget* parent )
208+
: QDialog( parent )
209+
, mWidget( nullptr )
210+
{
211+
setWindowTitle( tr( "Substitutions" ) );
212+
QVBoxLayout* vLayout = new QVBoxLayout();
213+
mWidget = new QgsSubstitutionListWidget();
214+
vLayout->addWidget( mWidget );
215+
QDialogButtonBox* bbox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal );
216+
connect( bbox, SIGNAL( accepted() ), this, SLOT( accept() ) );
217+
connect( bbox, SIGNAL( rejected() ), this, SLOT( reject() ) );
218+
vLayout->addWidget( bbox );
219+
setLayout( vLayout );
220+
}
221+
222+
void QgsSubstitutionListDialog::setSubstitutions( const QgsStringReplacementCollection& substitutions )
223+
{
224+
mWidget->setSubstitutions( substitutions );
225+
}
226+
227+
QgsStringReplacementCollection QgsSubstitutionListDialog::substitutions() const
228+
{
229+
return mWidget->substitutions();
230+
}

‎src/app/qgssubstitutionlistwidget.h

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/***************************************************************************
2+
qgssubstitutionlistwidget.h
3+
---------------------------
4+
begin : August 2016
5+
copyright : (C) 2016 Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
8+
***************************************************************************/
9+
10+
/***************************************************************************
11+
* *
12+
* This program is free software; you can redistribute it and/or modify *
13+
* it under the terms of the GNU General Public License as published by *
14+
* the Free Software Foundation; either version 2 of the License, or *
15+
* (at your option) any later version. *
16+
* *
17+
***************************************************************************/
18+
#ifndef QGSSUBSTITUTIONLISTWIDGET_H
19+
#define QGSSUBSTITUTIONLISTWIDGET_H
20+
21+
#include <QDialog>
22+
#include "qgspanelwidget.h"
23+
#include "ui_qgssubstitutionlistwidgetbase.h"
24+
#include "qgsstringutils.h"
25+
26+
/** \class QgsSubstitutionListWidget
27+
* \ingroup app
28+
* A widget which allows users to specify a list of substitutions to apply to a string, with
29+
* options for exporting and importing substitution lists.
30+
* \note added in QGIS 3.0
31+
* \see QgsSubstitutionListDialog
32+
*/
33+
class APP_EXPORT QgsSubstitutionListWidget : public QgsPanelWidget, private Ui::QgsSubstitutionListWidgetBase
34+
{
35+
Q_OBJECT
36+
Q_PROPERTY( QgsStringReplacementCollection substitutions READ substitutions WRITE setSubstitutions NOTIFY substitutionsChanged )
37+
38+
public:
39+
40+
/** Constructor for QgsSubstitutionListWidget.
41+
* @param parent parent widget
42+
*/
43+
QgsSubstitutionListWidget( QWidget* parent = nullptr );
44+
45+
/** Sets the list of substitutions to show in the widget.
46+
* @param substitutions substitution list
47+
* @see substitutions()
48+
*/
49+
void setSubstitutions( const QgsStringReplacementCollection& substitutions );
50+
51+
/** Returns the list of substitutions currently defined by the widget.
52+
* @see setSubstitutions()
53+
*/
54+
QgsStringReplacementCollection substitutions() const;
55+
56+
signals:
57+
58+
//! Emitted when the substitution definitions change.
59+
void substitutionsChanged( const QgsStringReplacementCollection& substitutions );
60+
61+
private slots:
62+
63+
void on_mButtonAdd_clicked();
64+
void on_mButtonRemove_clicked();
65+
void tableChanged();
66+
void on_mButtonExport_clicked();
67+
void on_mButtonImport_clicked();
68+
69+
private:
70+
71+
void addSubstitution( const QgsStringReplacement& substitution );
72+
73+
};
74+
75+
/** \class QgsSubstitutionListDialog
76+
* \ingroup app
77+
* A dialog which allows users to specify a list of substitutions to apply to a string, with
78+
* options for exporting and importing substitution lists.
79+
* \see QgsSubstitutionListWidget
80+
*/
81+
class APP_EXPORT QgsSubstitutionListDialog : public QDialog
82+
{
83+
Q_OBJECT
84+
Q_PROPERTY( QgsStringReplacementCollection substitutions READ substitutions WRITE setSubstitutions )
85+
86+
public:
87+
88+
/** Constructor for QgsSubstitutionListDialog.
89+
* @param parent parent widget
90+
*/
91+
QgsSubstitutionListDialog( QWidget* parent = nullptr );
92+
93+
/** Sets the list of substitutions to show in the dialog.
94+
* @param substitutions substitution list
95+
* @see substitutions()
96+
*/
97+
void setSubstitutions( const QgsStringReplacementCollection& substitutions );
98+
99+
/** Returns the list of substitutions currently defined by the dialog.
100+
* @see setSubstitutions()
101+
*/
102+
QgsStringReplacementCollection substitutions() const;
103+
104+
105+
private:
106+
107+
QgsSubstitutionListWidget* mWidget;
108+
109+
};
110+
111+
#endif // QGSSUBSTITUTIONLISTWIDGET_H

‎src/core/qgspallabeling.cpp

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ QgsPalLayerSettings::QgsPalLayerSettings()
129129
// font processing info
130130
mTextFontFound = true;
131131
mTextFontFamily = QApplication::font().family();
132+
useSubstitutions = false;
132133

133134
// text formatting
134135
wrapChar = "";
@@ -389,6 +390,8 @@ QgsPalLayerSettings& QgsPalLayerSettings::operator=( const QgsPalLayerSettings &
389390
// font processing info
390391
mTextFontFound = s.mTextFontFound;
391392
mTextFontFamily = s.mTextFontFamily;
393+
substitutions = s.substitutions;
394+
useSubstitutions = s.useSubstitutions;
392395

393396
// text formatting
394397
wrapChar = s.wrapChar;
@@ -851,7 +854,11 @@ void QgsPalLayerSettings::readFromLayer( QgsVectorLayer* layer )
851854
blendMode = QgsMapRenderer::getCompositionMode(
852855
static_cast< QgsMapRenderer::BlendMode >( layer->customProperty( "labeling/blendMode", QVariant( QgsMapRenderer::BlendNormal ) ).toUInt() ) );
853856
previewBkgrdColor = QColor( layer->customProperty( "labeling/previewBkgrdColor", QVariant( "#ffffff" ) ).toString() );
854-
857+
QDomDocument doc( "substitutions" );
858+
doc.setContent( layer->customProperty( "labeling/substitutions" ).toString() );
859+
QDomElement replacementElem = doc.firstChildElement( "substitutions" );
860+
substitutions.readXml( replacementElem );
861+
useSubstitutions = layer->customProperty( "labeling/useSubstitutions" ).toBool();
855862

856863
// text formatting
857864
wrapChar = layer->customProperty( "labeling/wrapChar" ).toString();
@@ -1131,6 +1138,14 @@ void QgsPalLayerSettings::writeToLayer( QgsVectorLayer* layer )
11311138
layer->setCustomProperty( "labeling/textTransp", textTransp );
11321139
layer->setCustomProperty( "labeling/blendMode", QgsMapRenderer::getBlendModeEnum( blendMode ) );
11331140
layer->setCustomProperty( "labeling/previewBkgrdColor", previewBkgrdColor.name() );
1141+
QDomDocument doc( "substitutions" );
1142+
QDomElement replacementElem = doc.createElement( "substitutions" );
1143+
substitutions.writeXml( replacementElem, doc );
1144+
QString replacementProps;
1145+
QTextStream stream( &replacementProps );
1146+
replacementElem.save( stream, -1 );
1147+
layer->setCustomProperty( "labeling/substitutions", replacementProps );
1148+
layer->setCustomProperty( "labeling/useSubstitutions", useSubstitutions );
11341149

11351150
// text formatting
11361151
layer->setCustomProperty( "labeling/wrapChar", wrapChar );
@@ -1302,7 +1317,8 @@ void QgsPalLayerSettings::readXml( QDomElement& elem )
13021317
blendMode = QgsMapRenderer::getCompositionMode(
13031318
static_cast< QgsMapRenderer::BlendMode >( textStyleElem.attribute( "blendMode", QString::number( QgsMapRenderer::BlendNormal ) ).toUInt() ) );
13041319
previewBkgrdColor = QColor( textStyleElem.attribute( "previewBkgrdColor", "#ffffff" ) );
1305-
1320+
substitutions.readXml( textStyleElem.firstChildElement( "substitutions" ) );
1321+
useSubstitutions = textStyleElem.attribute( "useSubstitutions" ).toInt();
13061322

13071323
// text formatting
13081324
QDomElement textFormatElem = elem.firstChildElement( "text-format" );
@@ -1568,6 +1584,10 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument& doc )
15681584
textStyleElem.setAttribute( "textTransp", textTransp );
15691585
textStyleElem.setAttribute( "blendMode", QgsMapRenderer::getBlendModeEnum( blendMode ) );
15701586
textStyleElem.setAttribute( "previewBkgrdColor", previewBkgrdColor.name() );
1587+
QDomElement replacementElem = doc.createElement( "substitutions" );
1588+
substitutions.writeXml( replacementElem, doc );
1589+
textStyleElem.appendChild( replacementElem );
1590+
textStyleElem.setAttribute( "useSubstitutions", useSubstitutions );
15711591

15721592
// text formatting
15731593
QDomElement textFormatElem = doc.createElement( "text-format" );
@@ -2319,6 +2339,12 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
23192339
labelText = v.isNull() ? "" : v.toString();
23202340
}
23212341

2342+
// apply text replacements
2343+
if ( useSubstitutions )
2344+
{
2345+
labelText = substitutions.process( labelText );
2346+
}
2347+
23222348
// data defined format numbers?
23232349
bool formatnum = formatNumbers;
23242350
if ( dataDefinedEvaluate( QgsPalLayerSettings::NumFormat, exprVal, &context.expressionContext(), formatNumbers ) )

‎src/core/qgspallabeling.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "qgsmaprenderer.h" // definition of QgsLabelingEngineInterface
3333
#include "qgsdiagramrendererv2.h"
3434
#include "qgsmapunitscale.h"
35+
#include "qgsstringutils.h"
3536

3637
namespace pal
3738
{
@@ -374,6 +375,11 @@ class CORE_EXPORT QgsPalLayerSettings
374375
QPainter::CompositionMode blendMode;
375376
QColor previewBkgrdColor;
376377

378+
//! Substitution collection for automatic text substitution with labels
379+
QgsStringReplacementCollection substitutions;
380+
//! True if substitutions should be applied
381+
bool useSubstitutions;
382+
377383
//-- text formatting
378384

379385
QString wrapChar;

‎src/core/qgsstringutils.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,89 @@ QString QgsStringUtils::insertLinks( const QString& string, bool *foundLinks )
337337

338338
return converted;
339339
}
340+
341+
QgsStringReplacement::QgsStringReplacement( const QString& match, const QString& replacement, bool caseSensitive, bool wholeWordOnly )
342+
: mMatch( match )
343+
, mReplacement( replacement )
344+
, mCaseSensitive( caseSensitive )
345+
, mWholeWordOnly( wholeWordOnly )
346+
{
347+
if ( mWholeWordOnly )
348+
mRx = QRegExp( QString( "\\b%1\\b" ).arg( mMatch ),
349+
mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
350+
}
351+
352+
QString QgsStringReplacement::process( const QString& input ) const
353+
{
354+
QString result = input;
355+
if ( !mWholeWordOnly )
356+
{
357+
return result.replace( mMatch, mReplacement, mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
358+
}
359+
else
360+
{
361+
return result.replace( mRx, mReplacement );
362+
}
363+
}
364+
365+
QgsStringMap QgsStringReplacement::properties() const
366+
{
367+
QgsStringMap map;
368+
map.insert( "match", mMatch );
369+
map.insert( "replace", mReplacement );
370+
map.insert( "caseSensitive", mCaseSensitive ? "1" : "0" );
371+
map.insert( "wholeWord", mWholeWordOnly ? "1" : "0" );
372+
return map;
373+
}
374+
375+
QgsStringReplacement QgsStringReplacement::fromProperties( const QgsStringMap& properties )
376+
{
377+
return QgsStringReplacement( properties.value( "match" ),
378+
properties.value( "replace" ),
379+
properties.value( "caseSensitive", "0" ) == "1",
380+
properties.value( "wholeWord", "0" ) == "1" );
381+
}
382+
383+
QString QgsStringReplacementCollection::process( const QString& input ) const
384+
{
385+
QString result = input;
386+
Q_FOREACH ( const QgsStringReplacement& r, mReplacements )
387+
{
388+
result = r.process( result );
389+
}
390+
return result;
391+
}
392+
393+
void QgsStringReplacementCollection::writeXml( QDomElement& elem, QDomDocument& doc ) const
394+
{
395+
Q_FOREACH ( const QgsStringReplacement& r, mReplacements )
396+
{
397+
QgsStringMap props = r.properties();
398+
QDomElement propEl = doc.createElement( "replacement" );
399+
QgsStringMap::const_iterator it = props.constBegin();
400+
for ( ; it != props.constEnd(); ++it )
401+
{
402+
propEl.setAttribute( it.key(), it.value() );
403+
}
404+
elem.appendChild( propEl );
405+
}
406+
}
407+
408+
void QgsStringReplacementCollection::readXml( const QDomElement& elem )
409+
{
410+
mReplacements.clear();
411+
QDomNodeList nodelist = elem.elementsByTagName( "replacement" );
412+
for ( int i = 0;i < nodelist.count(); i++ )
413+
{
414+
QDomElement replacementElem = nodelist.at( i ).toElement();
415+
QDomNamedNodeMap nodeMap = replacementElem.attributes();
416+
417+
QgsStringMap props;
418+
for ( int j = 0; j < nodeMap.count(); ++j )
419+
{
420+
props.insert( nodeMap.item( j ).nodeName(), nodeMap.item( j ).nodeValue() );
421+
}
422+
mReplacements << QgsStringReplacement::fromProperties( props );
423+
}
424+
425+
}

‎src/core/qgsstringutils.h

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,148 @@
1414
***************************************************************************/
1515

1616
#include <QString>
17+
#include <QRegExp>
18+
#include <QList>
19+
#include <QDomDocument>
20+
#include "qgis.h"
1721

1822
#ifndef QGSSTRINGUTILS_H
1923
#define QGSSTRINGUTILS_H
2024

25+
26+
/** \ingroup core
27+
* \class QgsStringReplacement
28+
* \brief A representation of a single string replacement.
29+
* \note Added in version 3.0
30+
*/
31+
32+
class CORE_EXPORT QgsStringReplacement
33+
{
34+
35+
public:
36+
37+
/** Constructor for QgsStringReplacement.
38+
* @param match string to match
39+
* @param replacement string to replace match with
40+
* @param caseSensitive set to true for a case sensitive match
41+
* @param wholeWordOnly set to true to match complete words only, or false to allow partial word matches
42+
*/
43+
QgsStringReplacement( const QString& match,
44+
const QString& replacement,
45+
bool caseSensitive = false,
46+
bool wholeWordOnly = false );
47+
48+
//! Returns the string matched by this object
49+
QString match() const { return mMatch; }
50+
51+
//! Returns the string to replace matches with
52+
QString replacement() const { return mReplacement; }
53+
54+
//! Returns true if match is case sensitive
55+
bool caseSensitive() const { return mCaseSensitive; }
56+
57+
//! Returns true if match only applies to whole words, or false if partial word matches are permitted
58+
bool wholeWordOnly() const { return mWholeWordOnly; }
59+
60+
/** Processes a given input string, applying any valid replacements which should be made.
61+
* @param input input string
62+
* @returns input string with any matches replaced by replacement string
63+
*/
64+
QString process( const QString& input ) const;
65+
66+
bool operator==( const QgsStringReplacement& other )
67+
{
68+
return mMatch == other.mMatch
69+
&& mReplacement == other.mReplacement
70+
&& mCaseSensitive == other.mCaseSensitive
71+
&& mWholeWordOnly == other.mWholeWordOnly;
72+
}
73+
74+
/** Returns a map of the replacement properties.
75+
* @see fromProperties()
76+
*/
77+
QgsStringMap properties() const;
78+
79+
/** Creates a new QgsStringReplacement from an encoded properties map.
80+
* @see properties()
81+
*/
82+
static QgsStringReplacement fromProperties( const QgsStringMap& properties );
83+
84+
private:
85+
86+
QString mMatch;
87+
88+
QString mReplacement;
89+
90+
bool mCaseSensitive;
91+
92+
bool mWholeWordOnly;
93+
94+
QRegExp mRx;
95+
};
96+
97+
98+
/** \ingroup core
99+
* \class QgsStringReplacementCollection
100+
* \brief A collection of string replacements (specified using QgsStringReplacement objects).
101+
* \note Added in version 3.0
102+
*/
103+
104+
class CORE_EXPORT QgsStringReplacementCollection
105+
{
106+
107+
public:
108+
109+
/** Constructor for QgsStringReplacementCollection
110+
* @param replacements initial list of string replacements
111+
*/
112+
QgsStringReplacementCollection( const QList< QgsStringReplacement >& replacements = QList< QgsStringReplacement >() )
113+
: mReplacements( replacements )
114+
{}
115+
116+
/** Returns the list of string replacements in this collection.
117+
* @see setReplacements()
118+
*/
119+
QList< QgsStringReplacement > replacements() const { return mReplacements; }
120+
121+
/** Sets the list of string replacements in this collection.
122+
* @param replacements list of string replacements to apply. Replacements are applied in the
123+
* order they are specified here.
124+
* @see replacements()
125+
*/
126+
void setReplacements( const QList< QgsStringReplacement >& replacements )
127+
{
128+
mReplacements = replacements;
129+
}
130+
131+
/** Processes a given input string, applying any valid replacements which should be made
132+
* using QgsStringReplacement objects contained by this collection. Replacements
133+
* are made in order of the QgsStringReplacement objects contained in the collection.
134+
* @param input input string
135+
* @returns input string with any matches replaced by replacement string
136+
*/
137+
QString process( const QString& input ) const;
138+
139+
/** Writes the collection state to an XML element.
140+
* @param elem target DOM element
141+
* @param doc DOM document
142+
* @see readXml()
143+
*/
144+
void writeXml( QDomElement& elem, QDomDocument& doc ) const;
145+
146+
/** Reads the collection state from an XML element.
147+
* @param elem DOM element
148+
* @see writeXml()
149+
*/
150+
void readXml( const QDomElement& elem );
151+
152+
private:
153+
154+
QList< QgsStringReplacement > mReplacements;
155+
156+
157+
};
158+
21159
/** \ingroup core
22160
* \class QgsStringUtils
23161
* \brief Utility functions for working with strings.

‎src/core/qgsvectorlayerlabelprovider.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider
118118

119119
//! List of generated
120120
QList<QgsLabelFeature*> mLabels;
121+
122+
friend class TestQgsLabelingEngineV2;
121123
};
122124

123125
#endif // QGSVECTORLAYERLABELPROVIDER_H

‎src/ui/qgslabelingguibase.ui

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,29 @@ font-style: italic;</string>
13341334
</layout>
13351335
</widget>
13361336
</item>
1337+
<item row="13" column="0" colspan="2">
1338+
<widget class="QCheckBox" name="mCheckBoxSubstituteText">
1339+
<property name="toolTip">
1340+
<string>If enabled, the label text will automatically be modified using a preset list of substitutes</string>
1341+
</property>
1342+
<property name="text">
1343+
<string>Apply label text substitutes</string>
1344+
</property>
1345+
</widget>
1346+
</item>
1347+
<item row="13" column="2">
1348+
<widget class="QToolButton" name="mToolButtonConfigureSubstitutes">
1349+
<property name="enabled">
1350+
<bool>false</bool>
1351+
</property>
1352+
<property name="toolTip">
1353+
<string>Configure substitutes</string>
1354+
</property>
1355+
<property name="text">
1356+
<string>...</string>
1357+
</property>
1358+
</widget>
1359+
</item>
13371360
<item row="1" column="1">
13381361
<widget class="QFontComboBox" name="mFontFamilyCmbBx">
13391362
<property name="editable">
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>QgsSubstitutionListWidgetBase</class>
4+
<widget class="QgsPanelWidget" name="QgsSubstitutionListWidgetBase">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>291</width>
10+
<height>416</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Form</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<property name="leftMargin">
18+
<number>0</number>
19+
</property>
20+
<property name="topMargin">
21+
<number>0</number>
22+
</property>
23+
<property name="rightMargin">
24+
<number>0</number>
25+
</property>
26+
<property name="bottomMargin">
27+
<number>0</number>
28+
</property>
29+
<item>
30+
<widget class="QTableWidget" name="mTableSubstitutions">
31+
<property name="selectionBehavior">
32+
<enum>QAbstractItemView::SelectRows</enum>
33+
</property>
34+
<column>
35+
<property name="text">
36+
<string>Text</string>
37+
</property>
38+
</column>
39+
<column>
40+
<property name="text">
41+
<string>Substitution</string>
42+
</property>
43+
</column>
44+
<column>
45+
<property name="text">
46+
<string>Case Sensitive</string>
47+
</property>
48+
</column>
49+
<column>
50+
<property name="text">
51+
<string>Whole Word</string>
52+
</property>
53+
<property name="toolTip">
54+
<string>If checked, only whole word matches are replaced</string>
55+
</property>
56+
</column>
57+
</widget>
58+
</item>
59+
<item>
60+
<layout class="QHBoxLayout" name="horizontalLayout">
61+
<item>
62+
<widget class="QToolButton" name="mButtonAdd">
63+
<property name="text">
64+
<string>...</string>
65+
</property>
66+
<property name="icon">
67+
<iconset resource="../../images/images.qrc">
68+
<normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset>
69+
</property>
70+
</widget>
71+
</item>
72+
<item>
73+
<widget class="QToolButton" name="mButtonRemove">
74+
<property name="text">
75+
<string>...</string>
76+
</property>
77+
<property name="icon">
78+
<iconset resource="../../images/images.qrc">
79+
<normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset>
80+
</property>
81+
</widget>
82+
</item>
83+
<item>
84+
<widget class="QToolButton" name="mButtonImport">
85+
<property name="text">
86+
<string>...</string>
87+
</property>
88+
<property name="icon">
89+
<iconset resource="../../images/images.qrc">
90+
<normaloff>:/images/themes/default/mActionFileOpen.svg</normaloff>:/images/themes/default/mActionFileOpen.svg</iconset>
91+
</property>
92+
</widget>
93+
</item>
94+
<item>
95+
<widget class="QToolButton" name="mButtonExport">
96+
<property name="text">
97+
<string>...</string>
98+
</property>
99+
<property name="icon">
100+
<iconset resource="../../images/images.qrc">
101+
<normaloff>:/images/themes/default/mActionFileSave.svg</normaloff>:/images/themes/default/mActionFileSave.svg</iconset>
102+
</property>
103+
</widget>
104+
</item>
105+
<item>
106+
<spacer name="horizontalSpacer">
107+
<property name="orientation">
108+
<enum>Qt::Horizontal</enum>
109+
</property>
110+
<property name="sizeHint" stdset="0">
111+
<size>
112+
<width>40</width>
113+
<height>20</height>
114+
</size>
115+
</property>
116+
</spacer>
117+
</item>
118+
</layout>
119+
</item>
120+
</layout>
121+
</widget>
122+
<customwidgets>
123+
<customwidget>
124+
<class>QgsPanelWidget</class>
125+
<extends>QWidget</extends>
126+
<header>qgspanelwidget.h</header>
127+
<container>1</container>
128+
</customwidget>
129+
</customwidgets>
130+
<resources>
131+
<include location="../../images/images.qrc"/>
132+
</resources>
133+
<connections/>
134+
</ui>

‎tests/src/core/testqgslabelingenginev2.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class TestQgsLabelingEngineV2 : public QObject
4343
void testRuleBased();
4444
void zOrder(); //test that labels are stacked correctly
4545
void testEncodeDecodePositionOrder();
46+
void testSubstitutions();
4647

4748
private:
4849
QgsVectorLayer* vl;
@@ -413,6 +414,46 @@ void TestQgsLabelingEngineV2::testEncodeDecodePositionOrder()
413414
QCOMPARE( decoded, expected );
414415
}
415416

417+
void TestQgsLabelingEngineV2::testSubstitutions()
418+
{
419+
QgsPalLayerSettings settings;
420+
settings.useSubstitutions = false;
421+
QgsStringReplacementCollection collection( QList< QgsStringReplacement >() << QgsStringReplacement( "aa", "bb" ) );
422+
settings.substitutions = collection;
423+
settings.fieldName = QString( "'aa label'" );
424+
settings.isExpression = true;
425+
426+
QgsVectorLayerLabelProvider* provider = new QgsVectorLayerLabelProvider( vl, "test", true, &settings );
427+
QgsFeature f( vl->fields(), 1 );
428+
f.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 2 ) ) );
429+
430+
// make a fake render context
431+
QSize size( 640, 480 );
432+
QgsMapSettings mapSettings;
433+
mapSettings.setOutputSize( size );
434+
mapSettings.setExtent( vl->extent() );
435+
mapSettings.setLayers( QStringList() << vl->id() );
436+
mapSettings.setOutputDpi( 96 );
437+
QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings );
438+
QStringList attributes;
439+
QgsLabelingEngineV2 engine;
440+
engine.setMapSettings( mapSettings );
441+
engine.addProvider( provider );
442+
provider->prepare( context, attributes );
443+
444+
provider->registerFeature( f, context );
445+
QCOMPARE( provider->mLabels.at( 0 )->labelText(), QString( "aa label" ) );
446+
447+
//with substitution
448+
settings.useSubstitutions = true;
449+
QgsVectorLayerLabelProvider* provider2 = new QgsVectorLayerLabelProvider( vl, "test2", true, &settings );
450+
engine.addProvider( provider2 );
451+
provider2->prepare( context, attributes );
452+
453+
provider2->registerFeature( f, context );
454+
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "bb label" ) );
455+
}
456+
416457
bool TestQgsLabelingEngineV2::imageCheck( const QString& testName, QImage &image, int mismatchCount )
417458
{
418459
//draw background

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ ADD_PYTHON_TEST(PyQgsSymbolLayerV2 test_qgssymbollayerv2.py)
9191
ADD_PYTHON_TEST(PyQgsArrowSymbolLayer test_qgsarrowsymbollayer.py)
9292
ADD_PYTHON_TEST(PyQgsSymbolExpressionVariables test_qgssymbolexpressionvariables.py)
9393
ADD_PYTHON_TEST(PyQgsSyntacticSugar test_syntactic_sugar.py)
94+
ADD_PYTHON_TEST(PyQgsStringUtils test_qgsstringutils.py)
9495
ADD_PYTHON_TEST(PyQgsSymbolV2 test_qgssymbolv2.py)
9596
ADD_PYTHON_TEST(PyQgsUnitTypes test_qgsunittypes.py)
9697
ADD_PYTHON_TEST(PyQgsVectorColorRamp test_qgsvectorcolorramp.py)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsStringUtils.
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Nyall Dawson'
10+
__date__ = '30/08/2016'
11+
__copyright__ = 'Copyright 2016, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import qgis # NOQA
16+
17+
from qgis.PyQt.QtXml import (QDomDocument, QDomElement)
18+
19+
from qgis.core import (QgsStringUtils,
20+
QgsStringReplacement,
21+
QgsStringReplacementCollection
22+
)
23+
from qgis.testing import unittest
24+
25+
26+
class PyQgsStringReplacement(unittest.TestCase):
27+
28+
def testBasic(self):
29+
""" basic tests for QgsStringReplacement"""
30+
r = QgsStringReplacement('match', 'replace')
31+
self.assertEqual(r.match(), 'match')
32+
self.assertEqual(r.replacement(), 'replace')
33+
34+
r = QgsStringReplacement('match', 'replace', True, True)
35+
self.assertTrue(r.wholeWordOnly())
36+
self.assertTrue(r.caseSensitive())
37+
38+
def testReplace(self):
39+
""" test applying replacements"""
40+
41+
# case insensitive
42+
r = QgsStringReplacement('match', 'replace', False, False)
43+
self.assertEqual(r.process('one MaTch only'), 'one replace only')
44+
self.assertEqual(r.process('more then one MaTch here match two'), 'more then one replace here replace two')
45+
self.assertEqual(r.process('match start and end MaTch'), 'replace start and end replace')
46+
self.assertEqual(r.process('no hits'), 'no hits')
47+
self.assertEqual(r.process('some exmatches here'), 'some exreplacees here')
48+
self.assertEqual(r.process(''), '')
49+
50+
# case sensitive
51+
r = QgsStringReplacement('match', 'replace', True, False)
52+
self.assertEqual(r.process('one MaTch only'), 'one MaTch only')
53+
self.assertEqual(r.process('one match only'), 'one replace only')
54+
55+
# whole word only, case insensitive
56+
r = QgsStringReplacement('match', 'replace', False, True)
57+
self.assertEqual(r.process('some exmatches here'), 'some exmatches here')
58+
self.assertEqual(r.process('some match here'), 'some replace here')
59+
self.assertEqual(r.process('some exmatches MaTch here'), 'some exmatches replace here')
60+
self.assertEqual(r.process('some match maTCh here'), 'some replace replace here')
61+
self.assertEqual(r.process('some -match. here'), 'some -replace. here')
62+
self.assertEqual(r.process('match here'), 'replace here')
63+
self.assertEqual(r.process('some match'), 'some replace')
64+
65+
# whole word only, case sensitive
66+
r = QgsStringReplacement('match', 'replace', True, True)
67+
self.assertEqual(r.process('some exmatches here'), 'some exmatches here')
68+
self.assertEqual(r.process('some match here'), 'some replace here')
69+
self.assertEqual(r.process('some exmatches MaTch here'), 'some exmatches MaTch here')
70+
self.assertEqual(r.process('some match maTCh here'), 'some replace maTCh here')
71+
72+
def testEquality(self):
73+
""" test equality operator"""
74+
r1 = QgsStringReplacement('a', 'b', True, True)
75+
r2 = QgsStringReplacement('a', 'b', True, True)
76+
self.assertEqual(r1, r2)
77+
r2 = QgsStringReplacement('c', 'b')
78+
self.assertNotEqual(r1, r2)
79+
r2 = QgsStringReplacement('a', 'c')
80+
self.assertNotEqual(r1, r2)
81+
r2 = QgsStringReplacement('a', 'b', False, True)
82+
self.assertNotEqual(r1, r2)
83+
r2 = QgsStringReplacement('c', 'b', True, False)
84+
self.assertNotEqual(r1, r2)
85+
86+
def testSaveRestore(self):
87+
""" test saving/restoring replacement to map"""
88+
r1 = QgsStringReplacement('a', 'b', True, True)
89+
props = r1.properties()
90+
r2 = QgsStringReplacement.fromProperties(props)
91+
self.assertEqual(r1, r2)
92+
r1 = QgsStringReplacement('a', 'b', False, False)
93+
props = r1.properties()
94+
r2 = QgsStringReplacement.fromProperties(props)
95+
self.assertEqual(r1, r2)
96+
97+
98+
class PyQgsStringReplacementCollection(unittest.TestCase):
99+
100+
def testBasic(self):
101+
""" basic QgsStringReplacementCollection tests"""
102+
list = [QgsStringReplacement('aa', '11'),
103+
QgsStringReplacement('bb', '22')]
104+
c = QgsStringReplacementCollection(list)
105+
self.assertEqual(c.replacements(), list)
106+
107+
def testReplacements(self):
108+
""" test replacing using collection of replacements """
109+
c = QgsStringReplacementCollection()
110+
c.setReplacements([QgsStringReplacement('aa', '11'),
111+
QgsStringReplacement('bb', '22')])
112+
self.assertEqual(c.process('here aa bb is aa string bb'), 'here 11 22 is 11 string 22')
113+
self.assertEqual(c.process('no matches'), 'no matches')
114+
self.assertEqual(c.process(''), '')
115+
116+
# test replacements are done in order
117+
c.setReplacements([QgsStringReplacement('aa', '11'),
118+
QgsStringReplacement('11', '22')])
119+
self.assertEqual(c.process('string aa'), 'string 22')
120+
# no replacements
121+
c.setReplacements([])
122+
self.assertEqual(c.process('string aa'), 'string aa')
123+
124+
def testSaveRestore(self):
125+
""" test saving and restoring collections """
126+
c = QgsStringReplacementCollection([QgsStringReplacement('aa', '11', False, False),
127+
QgsStringReplacement('bb', '22', True, True)])
128+
doc = QDomDocument("testdoc")
129+
elem = doc.createElement("replacements")
130+
c.writeXml(elem, doc)
131+
c2 = QgsStringReplacementCollection()
132+
c2.readXml(elem)
133+
self.assertEqual(c2.replacements(), c.replacements())
134+
135+
136+
if __name__ == '__main__':
137+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.