Skip to content
Permalink
Browse files

Cleanup and refactor calculationg of label text metrics for curved

labels, and move methods for calculating curved text placement
out of PAL so that they can be reused elsewhere
  • Loading branch information
nyalldawson committed Apr 8, 2021
1 parent 19a3d8e commit 832d5e95eef9286b5ae58be308096f5481bdc7d3
@@ -224,7 +224,6 @@ Returns the height of a character when rendered with the specified text ``format

};


/************************************************************************
* This file has been generated automatically from *
* *
@@ -63,6 +63,8 @@ Attempts to decode a string representation of a text orientation.
%End




};


@@ -1551,6 +1551,7 @@ set(QGIS_CORE_HDRS
textrenderer/qgstextformat.h
textrenderer/qgstextfragment.h
textrenderer/qgstextmasksettings.h
textrenderer/qgstextmetrics.h
textrenderer/qgstextrenderer.h
textrenderer/qgstextrendererutils.h
textrenderer/qgstextshadowsettings.h
@@ -31,8 +31,6 @@ QgsLabelFeature::~QgsLabelFeature()
mPermissibleZoneGeosPrepared.reset();
mPermissibleZoneGeos.reset();
}

delete mInfo;
}

void QgsLabelFeature::setPermissibleZone( const QgsGeometry &geometry )
@@ -27,7 +27,6 @@

namespace pal
{
class LabelInfo;
class Layer;
}

@@ -344,11 +343,6 @@ class CORE_EXPORT QgsLabelFeature
//! Sets text of the label
void setLabelText( const QString &text ) { mLabelText = text; }

//! Gets additional info required for curved label placement. Returns NULLPTR if not set
pal::LabelInfo *curvedLabelInfo() const { return mInfo; }
//! takes ownership of the instance
void setCurvedLabelInfo( pal::LabelInfo *info ) { mInfo = info; }

//! Gets PAL layer of the label feature. Should be only used internally in PAL
pal::Layer *layer() const { return mLayer; }
//! Assign PAL layer to the label feature. Should be only used internally in PAL
@@ -596,8 +590,6 @@ class CORE_EXPORT QgsLabelFeature
bool mAlwaysShow = false;
//! text of the label
QString mLabelText;
//! extra information for curved labels (may be NULLPTR)
pal::LabelInfo *mInfo = nullptr;

//! Distance to allow label to overrun linear features
double mOverrunDistance = 0;
@@ -2524,13 +2524,28 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
// store the label's calculated font for later use during painting
QgsDebugMsgLevel( QStringLiteral( "PAL font stored definedFont: %1, Style: %2" ).arg( labelFont.toString(), labelFont.styleName() ), 4 );
lf->setDefinedFont( labelFont );
lf->setFontMetrics( *labelFontMetrics );

// TODO: only for placement which needs character info
// account for any data defined font metrics adjustments
lf->setMaximumCharacterAngleInside( std::clamp( maxcharanglein, 20.0, 60.0 ) * M_PI / 180 );
lf->setMaximumCharacterAngleOutside( std::clamp( maxcharangleout, -95.0, -20.0 ) * M_PI / 180 );
lf->calculateInfo( placement == QgsPalLayerSettings::Curved || placement == QgsPalLayerSettings::PerimeterCurved,
labelFontMetrics.get(), xform, format().allowHtmlFormatting() ? &doc : nullptr );
switch ( placement )
{
case QgsPalLayerSettings::AroundPoint:
case QgsPalLayerSettings::OverPoint:
case QgsPalLayerSettings::Line:
case QgsPalLayerSettings::Horizontal:
case QgsPalLayerSettings::Free:
case QgsPalLayerSettings::OrderedPositionsAroundPoint:
case QgsPalLayerSettings::OutsidePolygons:
// these placements don't require text metrics
break;

case QgsPalLayerSettings::Curved:
case QgsPalLayerSettings::PerimeterCurved:
lf->setTextMetrics( QgsTextLabelFeature::calculateTextMetrics( xform, *labelFontMetrics, labelFont.letterSpacing(), labelFont.wordSpacing(), labelText, format().allowHtmlFormatting() ? &doc : nullptr ) );
break;
}

// for labelFeature the LabelInfo is passed to feat when it is registered

// TODO: allow layer-wide feature dist in PAL...?
@@ -29,101 +29,89 @@ QgsTextLabelFeature::QgsTextLabelFeature( QgsFeatureId id, geos::unique_ptr geom
mDefinedFont = QFont();
}


QgsTextLabelFeature::~QgsTextLabelFeature()
{
delete mFontMetrics;
}

QgsTextLabelFeature::~QgsTextLabelFeature() = default;

QString QgsTextLabelFeature::text( int partId ) const
{
if ( partId == -1 )
return mLabelText;
else
return mClusters.at( partId );
return mTextMetrics->grapheme( partId );
}

QgsTextCharacterFormat QgsTextLabelFeature::characterFormat( int partId ) const
{
return mCharacterFormats.value( partId );
return mTextMetrics.has_value() ? mTextMetrics->graphemeFormat( partId ) : QgsTextCharacterFormat();
}

bool QgsTextLabelFeature::hasCharacterFormat( int partId ) const
{
return partId < mCharacterFormats.size();
return mTextMetrics.has_value() && partId < mTextMetrics->graphemeFormatCount();
}

void QgsTextLabelFeature::calculateInfo( bool curvedLabeling, QFontMetricsF *fm, const QgsMapToPixel *xform, QgsTextDocument *document )
void QgsTextLabelFeature::setFontMetrics( const QFontMetricsF &metrics )
{
if ( mInfo )
return;

mFontMetrics = new QFontMetricsF( *fm ); // duplicate metrics for when drawing label

qreal letterSpacing = mDefinedFont.letterSpacing();
qreal wordSpacing = mDefinedFont.wordSpacing();
mFontMetrics = metrics; // duplicate metrics for when drawing label
}

QgsPrecalculatedTextMetrics QgsTextLabelFeature::calculateTextMetrics( const QgsMapToPixel *xform, const QFontMetricsF &fontMetrics, double letterSpacing, double wordSpacing, const QString &text, QgsTextDocument *document )
{
// create label info!
const double mapScale = xform->mapUnitsPerPixel();
const double characterHeight = mapScale * fm->height();

// mLetterSpacing/mWordSpacing = 0.0 is default for non-curved labels
// (non-curved spacings handled by Qt in QgsPalLayerSettings/QgsPalLabeling)
qreal charWidth;
qreal wordSpaceFix;
const double characterHeight = mapScale * fontMetrics.height();
QStringList graphemes;
QVector< QgsTextCharacterFormat > graphemeFormats;

if ( document && curvedLabeling )
if ( document )
{
for ( const QgsTextBlock &block : std::as_const( *document ) )
{
for ( const QgsTextFragment &fragment : block )
{
const QStringList graphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
for ( const QString &grapheme : graphemes )
const QStringList fragmentGraphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
for ( const QString &grapheme : fragmentGraphemes )
{
mClusters.append( grapheme );
mCharacterFormats.append( fragment.characterFormat() );
graphemes.append( grapheme );
graphemeFormats.append( fragment.characterFormat() );
}
}
}
}
else
{
//split string by valid grapheme boundaries - required for certain scripts (see #6883)
mClusters = QgsPalLabeling::splitToGraphemes( mLabelText );
graphemes = QgsPalLabeling::splitToGraphemes( text );
}

std::vector< double > characterWidths( mClusters.count() );
for ( int i = 0; i < mClusters.count(); i++ )
QVector< double > characterWidths( graphemes.count() );
for ( int i = 0; i < graphemes.count(); i++ )
{
// reconstruct how Qt creates word spacing, then adjust per individual stored character
// this will allow PAL to create each candidate width = character width + correct spacing
charWidth = fm->horizontalAdvance( mClusters[i] );
if ( curvedLabeling )
{
wordSpaceFix = qreal( 0.0 );
if ( mClusters[i] == QLatin1String( " " ) )
{
// word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
int nxt = i + 1;
wordSpaceFix = ( nxt < mClusters.count() && mClusters[nxt] != QLatin1String( " " ) ) ? wordSpacing : qreal( 0.0 );
}
// this workaround only works for clusters with a single character. Not sure how it should be handled
// with multi-character clusters.
if ( mClusters[i].length() == 1 &&
!qgsDoubleNear( fm->horizontalAdvance( QString( mClusters[i].at( 0 ) ) ), fm->horizontalAdvance( mClusters[i].at( 0 ) ) + letterSpacing ) )
{
// word spacing applied when it shouldn't be
wordSpaceFix -= wordSpacing;
}

charWidth = fm->horizontalAdvance( QString( mClusters[i] ) ) + wordSpaceFix;
qreal wordSpaceFix = qreal( 0.0 );
if ( graphemes[i] == QLatin1String( " " ) )
{
// word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
int nxt = i + 1;
wordSpaceFix = ( nxt < graphemes.count() && graphemes[nxt] != QLatin1String( " " ) ) ? wordSpacing : qreal( 0.0 );
}
// this workaround only works for clusters with a single character. Not sure how it should be handled
// with multi-character clusters.
if ( graphemes[i].length() == 1 &&
!qgsDoubleNear( fontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ), fontMetrics.horizontalAdvance( graphemes[i].at( 0 ) ) + letterSpacing ) )
{
// word spacing applied when it shouldn't be
wordSpaceFix -= wordSpacing;
}

const double charWidth = fontMetrics.horizontalAdvance( QString( graphemes[i] ) ) + wordSpaceFix;
characterWidths[i] = mapScale * charWidth;
}
mInfo = new pal::LabelInfo( characterHeight, std::move( characterWidths ) );

QgsPrecalculatedTextMetrics res( graphemes, characterHeight, std::move( characterWidths ) );
res.setGraphemeFormats( graphemeFormats );
return res;
}

QgsTextDocument QgsTextLabelFeature::document() const
@@ -19,6 +19,8 @@

#include "qgslabelfeature.h"
#include "qgstextdocument.h"
#include "qgstextmetrics.h"
#include <optional>

class QgsTextCharacterFormat;

@@ -62,9 +64,6 @@ class QgsTextLabelFeature : public QgsLabelFeature
*/
bool hasCharacterFormat( int partId ) const;

//! calculate data for info(). setDefinedFont() must have been called already.
void calculateInfo( bool curvedLabeling, QFontMetricsF *fm, const QgsMapToPixel *xform, QgsTextDocument *document = nullptr );

//! Gets data-defined values
const QMap< QgsPalLayerSettings::Property, QVariant > &dataDefinedValues() const { return mDataDefinedValues; }
//! Sets data-defined values
@@ -75,8 +74,43 @@ class QgsTextLabelFeature : public QgsLabelFeature
//! Font to be used for rendering
QFont definedFont() { return mDefinedFont; }

//! Metrics of the font for rendering
QFontMetricsF *labelFontMetrics() { return mFontMetrics; }
/**
* Metrics of the font for rendering.
*
* May be NULLPTR.
*/
QFontMetricsF *labelFontMetrics() { return mFontMetrics.has_value() ? &mFontMetrics.value() : nullptr; }

/**
* Sets the font \a metrics.
*/
void setFontMetrics( const QFontMetricsF &metrics );

/**
* Returns additional info required for curved label placement.
*
* Returns NULLPTR if not set.
*
* \see setTextMetrics()
* \since QGIS 3.20
*/
const QgsPrecalculatedTextMetrics *textMetrics() const { return mTextMetrics.has_value() ? &mTextMetrics.value() : nullptr; }

/**
* Sets additional text \a metrics required for curved label placement.
*
* \see textMetrics()
* \since QGIS 3.20
*/
void setTextMetrics( const QgsPrecalculatedTextMetrics &metrics ) { mTextMetrics = metrics; }

/**
* Calculate text metrics for later retrieval via textMetrics().
*
* \since QGIS 3.20
*/
static QgsPrecalculatedTextMetrics calculateTextMetrics( const QgsMapToPixel *xform, const QFontMetricsF &fontMetrics, double letterSpacing,
double wordSpacing, const QString &text = QString(), QgsTextDocument *document = nullptr );

/**
* Returns the document for the label.
@@ -125,15 +159,13 @@ class QgsTextLabelFeature : public QgsLabelFeature
double maximumCharacterAngleOutside() const { return mMaximumCharacterAngleOutside; }

protected:
//! List of graphemes (used for curved labels)
QStringList mClusters;

QList< QgsTextCharacterFormat > mCharacterFormats;

//! Font for rendering
QFont mDefinedFont;

//! Metrics of the font for rendering
QFontMetricsF *mFontMetrics = nullptr;
std::optional< QFontMetricsF > mFontMetrics;

//! Stores attribute values for data defined properties
QMap< QgsPalLayerSettings::Property, QVariant > mDataDefinedValues;

@@ -142,6 +174,8 @@ class QgsTextLabelFeature : public QgsLabelFeature
double mMaximumCharacterAngleInside = 0;
double mMaximumCharacterAngleOutside = 0;

std::optional< QgsPrecalculatedTextMetrics > mTextMetrics;

};

#endif //QGSTEXTLABELFEATURE_H
@@ -629,7 +629,6 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q
}
}


QgsTextRenderer::HAlignment hAlign = QgsTextRenderer::AlignLeft;
if ( tmpLyr.multilineAlign == QgsPalLayerSettings::MultiCenter )
hAlign = QgsTextRenderer::AlignCenter;

0 comments on commit 832d5e9

Please sign in to comment.