Skip to content

Commit 7dea970

Browse files
committedAug 30, 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.
1 parent 4a89b7c commit 7dea970

17 files changed

+1360
-265
lines changed
 

‎python/core/qgspallabeling.sip

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,11 @@ class QgsPalLayerSettings
464464
QPainter::CompositionMode blendMode;
465465
QColor previewBkgrdColor;
466466

467+
//! Substitution collection for automatic text substitution with labels
468+
QgsStringReplacementCollection substitutions;
469+
//! True if substitutions should be applied
470+
bool useSubstitutions;
471+
467472
//-- text formatting
468473

469474
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
@@ -114,6 +114,7 @@ SET(QGIS_APP_SRCS
114114
qgsrelationadddlg.cpp
115115
qgsselectbyformdialog.cpp
116116
qgsstatisticalsummarydockwidget.cpp
117+
qgssubstitutionlistwidget.cpp
117118
qgstextannotationdialog.cpp
118119
qgssnappingdialog.cpp
119120
qgssvgannotationdialog.cpp
@@ -290,6 +291,7 @@ SET (QGIS_APP_MOC_HDRS
290291
qgssnappingdialog.h
291292
qgssponsors.h
292293
qgsstatisticalsummarydockwidget.h
294+
qgssubstitutionlistwidget.h
293295
qgssvgannotationdialog.h
294296
qgstextannotationdialog.h
295297
qgstipgui.h

‎src/app/qgslabelinggui.cpp

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include "qgssvgselectorwidget.h"
3535
#include "qgsvectorlayerlabeling.h"
3636
#include "qgslogger.h"
37+
#include "qgssubstitutionlistwidget.h"
3738

3839
#include <QCheckBox>
3940
#include <QSettings>
@@ -137,6 +138,7 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
137138
connect( mShadowTranspSlider, SIGNAL( valueChanged( int ) ), mShadowTranspSpnBx, SLOT( setValue( int ) ) );
138139
connect( mShadowTranspSpnBx, SIGNAL( valueChanged( int ) ), mShadowTranspSlider, SLOT( setValue( int ) ) );
139140
connect( mLimitLabelChkBox, SIGNAL( toggled( bool ) ), mLimitLabelSpinBox, SLOT( setEnabled( bool ) ) );
141+
connect( mCheckBoxSubstituteText, SIGNAL( toggled( bool ) ), mToolButtonConfigureSubstitutes, SLOT( setEnabled( bool ) ) );
140142

141143
//connections to prevent users removing all line placement positions
142144
connect( chkLineAbove, SIGNAL( toggled( bool ) ), this, SLOT( updateLinePlacementOptions() ) );
@@ -466,7 +468,8 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
466468
<< radPolygonPerimeter
467469
<< radPolygonPerimeterCurved
468470
<< radPredefinedOrder
469-
<< mFieldExpressionWidget;
471+
<< mFieldExpressionWidget
472+
<< mCheckBoxSubstituteText;
470473
connectValueChanged( widgets, SLOT( updatePreview() ) );
471474

472475
connect( mQuadrantBtnGrp, SIGNAL( buttonClicked( int ) ), this, SLOT( updatePreview() ) );
@@ -623,6 +626,8 @@ void QgsLabelingGui::init()
623626
// set the current field or add the current expression to the bottom of the list
624627
mFieldExpressionWidget->setRow( -1 );
625628
mFieldExpressionWidget->setField( lyr.fieldName );
629+
mCheckBoxSubstituteText->setChecked( lyr.useSubstitutions );
630+
mSubstitutions = lyr.substitutions;
626631

627632
// populate placement options
628633
mCentroidRadioWhole->setChecked( lyr.centroidWhole );
@@ -1013,6 +1018,8 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
10131018
lyr.scaleVisibility = mScaleBasedVisibilityChkBx->isChecked();
10141019
lyr.scaleMin = mScaleBasedVisibilityMinSpnBx->value();
10151020
lyr.scaleMax = mScaleBasedVisibilityMaxSpnBx->value();
1021+
lyr.useSubstitutions = mCheckBoxSubstituteText->isChecked();
1022+
lyr.substitutions = mSubstitutions;
10161023

10171024
// buffer
10181025
lyr.bufferDraw = mBufferDrawChkBx->isChecked();
@@ -1973,6 +1980,12 @@ void QgsLabelingGui::updateLinePlacementOptions()
19731980
}
19741981
}
19751982

1983+
void QgsLabelingGui::onSubstitutionsChanged( const QgsStringReplacementCollection& substitutions )
1984+
{
1985+
mSubstitutions = substitutions;
1986+
emit widgetChanged();
1987+
}
1988+
19761989
void QgsLabelingGui::updateSvgWidgets( const QString& svgPath )
19771990
{
19781991
if ( mShapeSVGPathLineEdit->text() != svgPath )
@@ -2099,6 +2112,28 @@ void QgsLabelingGui::on_mChkNoObstacle_toggled( bool active )
20992112
mObstaclePriorityFrame->setEnabled( active );
21002113
}
21012114

2115+
void QgsLabelingGui::on_mToolButtonConfigureSubstitutes_clicked()
2116+
{
2117+
QgsPanelWidget* panel = QgsPanelWidget::findParentPanel( this );
2118+
if ( panel && panel->dockMode() )
2119+
{
2120+
QgsSubstitutionListWidget* widget = new QgsSubstitutionListWidget( panel );
2121+
widget->setPanelTitle( tr( "Substitutions" ) );
2122+
widget->setSubstitutions( mSubstitutions );
2123+
connect( widget, SIGNAL( substitutionsChanged( QgsStringReplacementCollection ) ), this, SLOT( onSubstitutionsChanged( QgsStringReplacementCollection ) ) );
2124+
panel->openPanel( widget );
2125+
return;
2126+
}
2127+
2128+
QgsSubstitutionListDialog dlg( this );
2129+
dlg.setSubstitutions( mSubstitutions );
2130+
if ( dlg.exec() == QDialog::Accepted )
2131+
{
2132+
mSubstitutions = dlg.substitutions();
2133+
emit widgetChanged();
2134+
}
2135+
}
2136+
21022137
void QgsLabelingGui::showBackgroundRadius( bool show )
21032138
{
21042139
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;
@@ -94,6 +95,8 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
9495
void on_mDirectSymbRightToolBtn_clicked();
9596
void on_mChkNoObstacle_toggled( bool active );
9697

98+
void on_mToolButtonConfigureSubstitutes_clicked();
99+
97100
protected:
98101
void blockInitSignals( bool block );
99102
void blockFontChangeSignals( bool blk );
@@ -133,6 +136,8 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
133136

134137
bool mLoadSvgParams;
135138

139+
QgsStringReplacementCollection mSubstitutions;
140+
136141
void enableDataDefinedAlignment( bool enable );
137142

138143
QgsExpressionContext createExpressionContext() const override;
@@ -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
@@ -128,6 +128,7 @@ QgsPalLayerSettings::QgsPalLayerSettings()
128128
// font processing info
129129
mTextFontFound = true;
130130
mTextFontFamily = QApplication::font().family();
131+
useSubstitutions = false;
131132

132133
// text formatting
133134
wrapChar = "";
@@ -387,6 +388,8 @@ QgsPalLayerSettings& QgsPalLayerSettings::operator=( const QgsPalLayerSettings &
387388
// font processing info
388389
mTextFontFound = s.mTextFontFound;
389390
mTextFontFamily = s.mTextFontFamily;
391+
substitutions = s.substitutions;
392+
useSubstitutions = s.useSubstitutions;
390393

391394
// text formatting
392395
wrapChar = s.wrapChar;
@@ -847,7 +850,11 @@ void QgsPalLayerSettings::readFromLayer( QgsVectorLayer* layer )
847850
blendMode = QgsPainting::getCompositionMode(
848851
static_cast< QgsPainting::BlendMode >( layer->customProperty( "labeling/blendMode", QVariant( QgsPainting::BlendNormal ) ).toUInt() ) );
849852
previewBkgrdColor = QColor( layer->customProperty( "labeling/previewBkgrdColor", QVariant( "#ffffff" ) ).toString() );
850-
853+
QDomDocument doc( "substitutions" );
854+
doc.setContent( layer->customProperty( "labeling/substitutions" ).toString() );
855+
QDomElement replacementElem = doc.firstChildElement( "substitutions" );
856+
substitutions.readXml( replacementElem );
857+
useSubstitutions = layer->customProperty( "labeling/useSubstitutions" ).toBool();
851858

852859
// text formatting
853860
wrapChar = layer->customProperty( "labeling/wrapChar" ).toString();
@@ -1127,6 +1134,14 @@ void QgsPalLayerSettings::writeToLayer( QgsVectorLayer* layer )
11271134
layer->setCustomProperty( "labeling/textTransp", textTransp );
11281135
layer->setCustomProperty( "labeling/blendMode", QgsPainting::getBlendModeEnum( blendMode ) );
11291136
layer->setCustomProperty( "labeling/previewBkgrdColor", previewBkgrdColor.name() );
1137+
QDomDocument doc( "substitutions" );
1138+
QDomElement replacementElem = doc.createElement( "substitutions" );
1139+
substitutions.writeXml( replacementElem, doc );
1140+
QString replacementProps;
1141+
QTextStream stream( &replacementProps );
1142+
replacementElem.save( stream, -1 );
1143+
layer->setCustomProperty( "labeling/substitutions", replacementProps );
1144+
layer->setCustomProperty( "labeling/useSubstitutions", useSubstitutions );
11301145

11311146
// text formatting
11321147
layer->setCustomProperty( "labeling/wrapChar", wrapChar );
@@ -1298,7 +1313,8 @@ void QgsPalLayerSettings::readXml( QDomElement& elem )
12981313
blendMode = QgsPainting::getCompositionMode(
12991314
static_cast< QgsPainting::BlendMode >( textStyleElem.attribute( "blendMode", QString::number( QgsPainting::BlendNormal ) ).toUInt() ) );
13001315
previewBkgrdColor = QColor( textStyleElem.attribute( "previewBkgrdColor", "#ffffff" ) );
1301-
1316+
substitutions.readXml( textStyleElem.firstChildElement( "substitutions" ) );
1317+
useSubstitutions = textStyleElem.attribute( "useSubstitutions" ).toInt();
13021318

13031319
// text formatting
13041320
QDomElement textFormatElem = elem.firstChildElement( "text-format" );
@@ -1564,6 +1580,10 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument& doc )
15641580
textStyleElem.setAttribute( "textTransp", textTransp );
15651581
textStyleElem.setAttribute( "blendMode", QgsPainting::getBlendModeEnum( blendMode ) );
15661582
textStyleElem.setAttribute( "previewBkgrdColor", previewBkgrdColor.name() );
1583+
QDomElement replacementElem = doc.createElement( "substitutions" );
1584+
substitutions.writeXml( replacementElem, doc );
1585+
textStyleElem.appendChild( replacementElem );
1586+
textStyleElem.setAttribute( "useSubstitutions", useSubstitutions );
15671587

15681588
// text formatting
15691589
QDomElement textFormatElem = doc.createElement( "text-format" );
@@ -2314,6 +2334,12 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
23142334
labelText = v.isNull() ? "" : v.toString();
23152335
}
23162336

2337+
// apply text replacements
2338+
if ( useSubstitutions )
2339+
{
2340+
labelText = substitutions.process( labelText );
2341+
}
2342+
23172343
// data defined format numbers?
23182344
bool formatnum = formatNumbers;
23192345
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
@@ -34,6 +34,7 @@
3434
#include "qgsfield.h"
3535
#include "qgspoint.h"
3636
#include "qgsmapunitscale.h"
37+
#include "qgsstringutils.h"
3738

3839
namespace pal
3940
{
@@ -480,6 +481,11 @@ class CORE_EXPORT QgsPalLayerSettings
480481
QPainter::CompositionMode blendMode;
481482
QColor previewBkgrdColor;
482483

484+
//! Substitution collection for automatic text substitution with labels
485+
QgsStringReplacementCollection substitutions;
486+
//! True if substitutions should be applied
487+
bool useSubstitutions;
488+
483489
//-- text formatting
484490

485491
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 TestQgsLabelingEngine;
121123
};
122124

123125
#endif // QGSVECTORLAYERLABELPROVIDER_H

‎src/ui/qgslabelingguibase.ui

Lines changed: 285 additions & 262 deletions
Large diffs are not rendered by default.
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/testqgslabelingengine.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class TestQgsLabelingEngine : 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 TestQgsLabelingEngine::testEncodeDecodePositionOrder()
413414
QCOMPARE( decoded, expected );
414415
}
415416

417+
void TestQgsLabelingEngine::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+
QgsLabelingEngine 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 TestQgsLabelingEngine::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
@@ -92,6 +92,7 @@ ADD_PYTHON_TEST(PyQgsSymbolLayerCreateSld test_qgssymbollayer_createsld.py)
9292
ADD_PYTHON_TEST(PyQgsArrowSymbolLayer test_qgsarrowsymbollayer.py)
9393
ADD_PYTHON_TEST(PyQgsSymbolExpressionVariables test_qgssymbolexpressionvariables.py)
9494
ADD_PYTHON_TEST(PyQgsSyntacticSugar test_syntactic_sugar.py)
95+
ADD_PYTHON_TEST(PyQgsStringUtils test_qgsstringutils.py)
9596
ADD_PYTHON_TEST(PyQgsSymbol test_qgssymbol.py)
9697
ADD_PYTHON_TEST(PyQgsTreeWidgetItem test_qgstreewidgetitem.py)
9798
ADD_PYTHON_TEST(PyQgsUnitTypes test_qgsunittypes.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.