Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Fix Capitalize First Letter fails with curved labels (fix #14875)
Instead of using QFont's inbuilt capitalization support, which
applies only on rendering and accordingly fails for curved
labels which are drawn one character at a time, we now manually
capitalize label text while registering features.

The capitalize first method from Qt was reimplemented in QgsStringUtils
(together with what I expect is better handling of unicode characters
over the Qt method).

This change also makes it possible to implement other capitalization
methods not directly supported by Qt

(cherry-picked from 15dd295)
  • Loading branch information
nyalldawson committed Aug 30, 2016
1 parent e1390ad commit d30f66e
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 34 deletions.
19 changes: 19 additions & 0 deletions python/core/qgsstringutils.sip
Expand Up @@ -10,6 +10,25 @@ class QgsStringUtils
#include <qgsstringutils.h>
%End
public:


//! Capitalization options
enum Capitalization
{
MixedCase, //!< Mixed case, ie no change
AllUppercase, //!< Convert all characters to uppercase
AllLowercase, //!< Convert all characters to lowercase
ForceFirstLetterToCapital, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
};

/** Converts a string by applying capitalization rules to the string.
* @param string input string
* @param capitalization capitalization type to apply
* @return capitalized string
* @note added in QGIS 3.0
*/
static QString capitalize( const QString& string, Capitalization capitalization );

/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
* number of character edits (insertions, deletions or substitutions) required to change
* one string to another.
Expand Down
71 changes: 37 additions & 34 deletions src/core/qgspallabeling.cpp
Expand Up @@ -18,6 +18,7 @@
#include "qgspallabeling.h"
#include "qgstextlabelfeature.h"
#include "qgsunittypes.h"
#include "qgsstringutils.h"

#include <list>

Expand Down Expand Up @@ -2283,6 +2284,7 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont

// calculate rest of font attributes and store any data defined values
// this is done here for later use in making label backgrounds part of collision management (when implemented)
labelFont.setCapitalization( QFont::MixedCase ); // reset this - we don't use QFont's handling as it breaks with curved labels
parseTextStyle( labelFont, fontunits, context );
parseTextFormatting( context );
parseTextBuffer( context );
Expand Down Expand Up @@ -2316,6 +2318,41 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
labelText = v.isNull() ? "" : v.toString();
}

// apply capitalization
QgsStringUtils::Capitalization capitalization = QgsStringUtils::MixedCase;
// maintain API - capitalization may have been set in textFont
if ( textFont.capitalization() != QFont::MixedCase )
{
capitalization = static_cast< QgsStringUtils::Capitalization >( textFont.capitalization() );
}
// data defined font capitalization?
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
{
QString fcase = exprVal.toString().trimmed();
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );

if ( !fcase.isEmpty() )
{
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::MixedCase;
}
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::AllUppercase;
}
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::AllLowercase;
}
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::ForceFirstLetterToCapital;
}
}
}
labelText = QgsStringUtils::capitalize( labelText, capitalization );

// data defined format numbers?
bool formatnum = formatNumbers;
if ( dataDefinedEvaluate( QgsPalLayerSettings::NumFormat, exprVal, &context.expressionContext(), formatNumbers ) )
Expand Down Expand Up @@ -3315,7 +3352,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
// copy over existing font settings
//newFont = newFont.resolve( labelFont ); // should work, but let's be sure what's being copied
newFont.setPixelSize( labelFont.pixelSize() );
newFont.setCapitalization( labelFont.capitalization() );
newFont.setUnderline( labelFont.underline() );
newFont.setStrikeOut( labelFont.strikeOut() );
newFont.setWordSpacing( labelFont.wordSpacing() );
Expand Down Expand Up @@ -3352,39 +3388,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
}
labelFont.setLetterSpacing( QFont::AbsoluteSpacing, scaleToPixelContext( letterspace, context, fontunits, false, fontSizeMapUnitScale ) );

// data defined font capitalization?
QFont::Capitalization fontcaps = labelFont.capitalization();
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
{
QString fcase = exprVal.toString().trimmed();
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );

if ( !fcase.isEmpty() )
{
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::MixedCase;
}
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::AllUppercase;
}
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::AllLowercase;
}
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::Capitalize;
}

if ( fontcaps != labelFont.capitalization() )
{
labelFont.setCapitalization( fontcaps );
}
}
}

// data defined strikeout font style?
if ( dataDefinedEvaluate( QgsPalLayerSettings::Strikeout, exprVal, &context.expressionContext(), labelFont.strikeOut() ) )
{
Expand Down
50 changes: 50 additions & 0 deletions src/core/qgsstringutils.cpp
Expand Up @@ -15,6 +15,56 @@

#include "qgsstringutils.h"
#include <QVector>
#include <QRegExp>
#include <QStringList>
#include <QTextBoundaryFinder>

QString QgsStringUtils::capitalize( const QString& string, QgsStringUtils::Capitalization capitalization )
{
if ( string.isEmpty() )
return QString();

switch ( capitalization )
{
case MixedCase:
return string;

case AllUppercase:
return string.toUpper();

case AllLowercase:
return string.toLower();

case ForceFirstLetterToCapital:
{
QString temp = string;

QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word, string.constData(), string.length(), 0, 0 );
QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme, string.constData(), string.length(), 0, 0 );

wordSplitter.setPosition( 0 );
bool first = true;
#if QT_VERSION >= 0x050000
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
|| wordSplitter.toNextBoundary() >= 0 )
#else
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartWord )
|| wordSplitter.toNextBoundary() >= 0 )
#endif
{
first = false;
letterSplitter.setPosition( wordSplitter.position() );
letterSplitter.toNextBoundary();
QString substr = string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
}
return temp;
}

}
// no warnings
return string;
}

int QgsStringUtils::levenshteinDistance( const QString& string1, const QString& string2, bool caseSensitive )
{
Expand Down
18 changes: 18 additions & 0 deletions src/core/qgsstringutils.h
Expand Up @@ -14,6 +14,7 @@
***************************************************************************/

#include <QString>
#include <QFont> // for enum values

#ifndef QGSSTRINGUTILS_H
#define QGSSTRINGUTILS_H
Expand All @@ -28,6 +29,23 @@ class CORE_EXPORT QgsStringUtils
{
public:

//! Capitalization options
enum Capitalization
{
MixedCase = QFont::MixedCase, //!< Mixed case, ie no change
AllUppercase = QFont::AllUppercase, //!< Convert all characters to uppercase
AllLowercase = QFont::AllLowercase, //!< Convert all characters to lowercase
ForceFirstLetterToCapital = QFont::Capitalize, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
};

/** Converts a string by applying capitalization rules to the string.
* @param string input string
* @param capitalization capitalization type to apply
* @return capitalized string
* @note added in QGIS 3.0
*/
static QString capitalize( const QString& string, Capitalization capitalization );

/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
* number of character edits (insertions, deletions or substitutions) required to change
* one string to another.
Expand Down
2 changes: 2 additions & 0 deletions src/core/qgsvectorlayerlabelprovider.h
Expand Up @@ -114,6 +114,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider

//! List of generated
QList<QgsLabelFeature*> mLabels;

friend class TestQgsLabelingEngineV2;
};

#endif // QGSVECTORLAYERLABELPROVIDER_H
60 changes: 60 additions & 0 deletions tests/src/core/testqgslabelingenginev2.cpp
Expand Up @@ -43,6 +43,7 @@ class TestQgsLabelingEngineV2 : public QObject
void testRuleBased();
void zOrder(); //test that labels are stacked correctly
void testEncodeDecodePositionOrder();
void testCapitalization();

private:
QgsVectorLayer* vl;
Expand Down Expand Up @@ -413,6 +414,65 @@ void TestQgsLabelingEngineV2::testEncodeDecodePositionOrder()
QCOMPARE( decoded, expected );
}

void TestQgsLabelingEngineV2::testCapitalization()
{
QgsFeature f( vl->fields(), 1 );
f.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 2 ) ) );

// make a fake render context
QSize size( 640, 480 );
QgsMapSettings mapSettings;
mapSettings.setOutputSize( size );
mapSettings.setExtent( vl->extent() );
mapSettings.setLayers( QStringList() << vl->id() );
mapSettings.setOutputDpi( 96 );
QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings );
QStringList attributes;
QgsLabelingEngineV2 engine;
engine.setMapSettings( mapSettings );

// no change
QgsPalLayerSettings settings;
QFont font = settings.textFont;
font.setCapitalization( QFont::MixedCase );
settings.textFont = font;
settings.fieldName = QString( "'a teSt LABEL'" );
settings.isExpression = true;

QgsVectorLayerLabelProvider* provider = new QgsVectorLayerLabelProvider( vl, true, &settings );
engine.addProvider( provider );
provider->prepare( context, attributes );
provider->registerFeature( f, context );
QCOMPARE( provider->mLabels.at( 0 )->labelText(), QString( "a teSt LABEL" ) );

//uppercase
font.setCapitalization( QFont::AllUppercase );
settings.textFont = font;
QgsVectorLayerLabelProvider* provider2 = new QgsVectorLayerLabelProvider( vl, true, &settings );
engine.addProvider( provider2 );
provider2->prepare( context, attributes );
provider2->registerFeature( f, context );
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "A TEST LABEL" ) );

//lowercase
font.setCapitalization( QFont::AllLowercase );
settings.textFont = font;
QgsVectorLayerLabelProvider* provider3 = new QgsVectorLayerLabelProvider( vl, true, &settings );
engine.addProvider( provider3 );
provider3->prepare( context, attributes );
provider3->registerFeature( f, context );
QCOMPARE( provider3->mLabels.at( 0 )->labelText(), QString( "a test label" ) );

//first letter uppercase
font.setCapitalization( QFont::Capitalize );
settings.textFont = font;
QgsVectorLayerLabelProvider* provider4 = new QgsVectorLayerLabelProvider( vl, true, &settings );
engine.addProvider( provider4 );
provider4->prepare( context, attributes );
provider4->registerFeature( f, context );
QCOMPARE( provider4->mLabels.at( 0 )->labelText(), QString( "A TeSt LABEL" ) );
}

bool TestQgsLabelingEngineV2::imageCheck( const QString& testName, QImage &image, int mismatchCount )
{
//draw background
Expand Down

0 comments on commit d30f66e

Please sign in to comment.