diff --git a/src/core/labeling/qgstextlabelfeature.cpp b/src/core/labeling/qgstextlabelfeature.cpp index 1c64197c752..7b5a0808e5e 100644 --- a/src/core/labeling/qgstextlabelfeature.cpp +++ b/src/core/labeling/qgstextlabelfeature.cpp @@ -75,8 +75,8 @@ void QgsTextLabelFeature::calculateInfo( bool curvedLabeling, QFontMetricsF *fm, maxoutangle = -95.0; // create label info! - double mapScale = xform->mapUnitsPerPixel(); - double labelHeight = mapScale * fm->height(); + 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) @@ -104,7 +104,7 @@ void QgsTextLabelFeature::calculateInfo( bool curvedLabeling, QFontMetricsF *fm, mClusters = QgsPalLabeling::splitToGraphemes( mLabelText ); } - mInfo = new pal::LabelInfo( mClusters.count(), labelHeight, maxinangle, maxoutangle ); + std::vector< double > characterWidths( mClusters.count() ); for ( int i = 0; i < mClusters.count(); i++ ) { // reconstruct how Qt creates word spacing, then adjust per individual stored character @@ -131,9 +131,9 @@ void QgsTextLabelFeature::calculateInfo( bool curvedLabeling, QFontMetricsF *fm, charWidth = fm->horizontalAdvance( QString( mClusters[i] ) ) + wordSpaceFix; } - double labelWidth = mapScale * charWidth; - mInfo->char_info[i].width = labelWidth; + characterWidths[i] = mapScale * charWidth; } + mInfo = new pal::LabelInfo( characterHeight, std::move( characterWidths ), maxinangle, maxoutangle ); } QgsTextDocument QgsTextLabelFeature::document() const diff --git a/src/core/pal/feature.cpp b/src/core/pal/feature.cpp index 1718456e149..7da4c812dcf 100644 --- a/src/core/pal/feature.cpp +++ b/src/core/pal/feature.cpp @@ -1222,28 +1222,27 @@ std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std return lPos.size(); } - -std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *path_positions, double *path_distances, int &orientation, const double offsetAlongLine, bool &reversed, bool &flip, bool applyAngleConstraints ) +std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *mapShape, const std::vector< double> &pathDistances, int &orientation, const double offsetAlongLine, bool &reversed, bool &flip, bool applyAngleConstraints ) { double offsetAlongSegment = offsetAlongLine; int index = 1; // Find index of segment corresponding to starting offset - while ( index < path_positions->nbPoints && offsetAlongSegment > path_distances[index] ) + while ( index < mapShape->nbPoints && offsetAlongSegment > pathDistances[index] ) { - offsetAlongSegment -= path_distances[index]; + offsetAlongSegment -= pathDistances[index]; index += 1; } - if ( index >= path_positions->nbPoints ) + if ( index >= mapShape->nbPoints ) { return nullptr; } LabelInfo *li = mLF->curvedLabelInfo(); - double string_height = li->label_height; + const double characterHeight = li->characterHeight; - const double segment_length = path_distances[index]; - if ( qgsDoubleNear( segment_length, 0.0 ) ) + const double segmentLength = pathDistances[index]; + if ( qgsDoubleNear( segmentLength, 0.0 ) ) { // Not allowed to place across on 0 length segments or discontinuities return nullptr; @@ -1260,11 +1259,12 @@ std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet double startLabelY = 0; double endLabelX = 0; double endLabelY = 0; - for ( int i = 0; i < li->char_num; i++ ) + const int characterCount = li->count(); + for ( int i = 0; i < characterCount; i++ ) { - LabelInfo::CharacterInfo &ci = li->char_info[i]; + const double characterWidth = li->characterWidth( i ); double characterStartX, characterStartY; - if ( !nextCharPosition( ci.width, path_distances[endindex], path_positions, endindex, _distance, characterStartX, characterStartY, endLabelX, endLabelY ) ) + if ( !nextCharPosition( characterWidth, pathDistances[endindex], mapShape, endindex, _distance, characterStartX, characterStartY, endLabelX, endLabelY ) ) { return nullptr; } @@ -1298,29 +1298,30 @@ std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet std::unique_ptr< LabelPosition > slp; LabelPosition *slp_tmp = nullptr; - double old_x = path_positions->x[index - 1]; - double old_y = path_positions->y[index - 1]; + double old_x = mapShape->x[index - 1]; + double old_y = mapShape->y[index - 1]; - double new_x = path_positions->x[index]; - double new_y = path_positions->y[index]; + double new_x = mapShape->x[index]; + double new_y = mapShape->y[index]; double dx = new_x - old_x; double dy = new_y - old_y; double angle = std::atan2( -dy, dx ); - for ( int i = 0; i < li->char_num; i++ ) + const int characterCount = li->count(); + for ( int i = 0; i < characterCount; i++ ) { double last_character_angle = angle; // grab the next character according to the orientation - LabelInfo::CharacterInfo &ci = ( orientation > 0 ? li->char_info[i] : li->char_info[li->char_num - i - 1] ); - if ( qgsDoubleNear( ci.width, 0.0 ) ) + const double characterWidth = ( orientation > 0 ? li->characterWidth( i ) : li->characterWidth( characterCount - i - 1 ) ); + if ( qgsDoubleNear( characterWidth, 0.0 ) ) // Certain scripts rely on zero-width character, skip those to prevent failure (see #15801) continue; double start_x, start_y, end_x, end_y; - if ( !nextCharPosition( ci.width, path_distances[index], path_positions, index, offsetAlongSegment, start_x, start_y, end_x, end_y ) ) + if ( !nextCharPosition( characterWidth, pathDistances[index], mapShape, index, offsetAlongSegment, start_x, start_y, end_x, end_y ) ) { return nullptr; } @@ -1331,21 +1332,23 @@ std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet // Test last_character_angle vs angle // since our rendering angle has changed then check against our // max allowable angle change. - double angle_delta = last_character_angle - angle; + double angleDelta = last_character_angle - angle; // normalise between -180 and 180 - while ( angle_delta > M_PI ) angle_delta -= 2 * M_PI; - while ( angle_delta < -M_PI ) angle_delta += 2 * M_PI; - if ( applyAngleConstraints && ( ( li->max_char_angle_inside > 0 && angle_delta > 0 - && angle_delta > li->max_char_angle_inside * ( M_PI / 180 ) ) - || ( li->max_char_angle_outside < 0 && angle_delta < 0 - && angle_delta < li->max_char_angle_outside * ( M_PI / 180 ) ) ) ) + while ( angleDelta > M_PI ) + angleDelta -= 2 * M_PI; + while ( angleDelta < -M_PI ) + angleDelta += 2 * M_PI; + if ( applyAngleConstraints && ( ( li->maxCharAngleInsideRadians > 0 && angleDelta > 0 + && angleDelta > li->maxCharAngleInsideRadians ) + || ( li->maxCharAngleOutsideRadians < 0 && angleDelta < 0 + && angleDelta < li->maxCharAngleOutsideRadians ) ) ) { return nullptr; } // Shift the character downwards since the draw position is specified at the baseline // and we're calculating the mean line here - double dist = 0.9 * li->label_height / 2; + double dist = 0.9 * li->characterHeight / 2; if ( orientation < 0 ) { dist = -dist; @@ -1366,13 +1369,13 @@ std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet if ( orientation < 0 ) { // rotate in place - render_x += ci.width * std::cos( render_angle ); //- (string_height-2)*sin(render_angle); - render_y -= ci.width * std::sin( render_angle ); //+ (string_height-2)*cos(render_angle); + render_x += characterWidth * std::cos( render_angle ); //- (string_height-2)*sin(render_angle); + render_y -= characterWidth * std::sin( render_angle ); //+ (string_height-2)*cos(render_angle); render_angle += M_PI; } - std::unique_ptr< LabelPosition > tmp = std::make_unique< LabelPosition >( 0, render_x /*- xBase*/, render_y /*- yBase*/, ci.width, string_height, -render_angle, 0.0001, this, false, LabelPosition::QuadrantOver ); - tmp->setPartId( orientation > 0 ? i : li->char_num - i - 1 ); + std::unique_ptr< LabelPosition > tmp = std::make_unique< LabelPosition >( 0, render_x /*- xBase*/, render_y /*- yBase*/, characterWidth, characterHeight, -render_angle, 0.0001, this, false, LabelPosition::QuadrantOver ); + tmp->setPartId( orientation > 0 ? i : characterCount - i - 1 ); LabelPosition *next = tmp.get(); if ( !slp ) slp = std::move( tmp ); @@ -1401,17 +1404,20 @@ static std::unique_ptr< LabelPosition > _createCurvedCandidate( LabelPosition *l std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal ) { LabelInfo *li = mLF->curvedLabelInfo(); - // label info must be present - if ( !li || li->char_num == 0 ) + if ( !li ) + return 0; + + const int characterCount = li->count(); + if ( characterCount == 0 ) return 0; // TODO - we may need an explicit penalty for overhanging labels. Currently, they are penalized just because they // are further from the line center, so non-overhanding placements are picked where possible. double totalCharacterWidth = 0; - for ( int i = 0; i < li->char_num; ++i ) - totalCharacterWidth += li->char_info[ i ].width; + for ( int i = 0; i < characterCount; ++i ) + totalCharacterWidth += li->characterWidth( i ); std::unique_ptr< PointSet > expanded; double shapeLength = mapShape->length(); @@ -1442,7 +1448,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq } // distance calculation - std::unique_ptr< double [] > path_distances = std::make_unique( mapShape->nbPoints ); + std::vector< double > path_distances( mapShape->nbPoints ); double total_distance = 0; double old_x = -1.0, old_y = -1.0; for ( int i = 0; i < mapShape->nbPoints; i++ ) @@ -1469,7 +1475,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq std::vector< std::unique_ptr< LabelPosition >> positions; const std::size_t candidateTargetCount = maximumLineCandidates(); - double delta = std::max( li->label_height / 6, total_distance / candidateTargetCount ); + double delta = std::max( li->characterHeight / 6, total_distance / candidateTargetCount ); QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags(); if ( flags == 0 ) @@ -1507,18 +1513,18 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq orientation = 1; } - std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly ); + std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances, orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly ); if ( !slp ) continue; // If we placed too many characters upside down - if ( slp->upsideDownCharCount() >= li->char_num / 2.0 ) + if ( slp->upsideDownCharCount() >= characterCount / 2.0 ) { // if labels should be shown upright then retry with the opposite orientation if ( ( showUprightLabels() && !flip ) ) { orientation = -orientation; - slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly ); + slp = curvedPlacementAtOffset( mapShape, path_distances, orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly ); } } if ( !slp ) @@ -1547,7 +1553,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq // if anchor placement is towards start or end of line, we need to slightly tweak the costs to ensure that the // anchor weighting is sufficient to push labels towards start/end const bool anchorIsFlexiblePlacement = !singleCandidateOnly && mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9; - double angle_diff_avg = li->char_num > 1 ? ( angle_diff / ( li->char_num - 1 ) ) : 0; // <0, pi> but pi/8 is much already + double angle_diff_avg = characterCount > 1 ? ( angle_diff / ( characterCount - 1 ) ) : 0; // <0, pi> but pi/8 is much already double cost = angle_diff_avg / 100; // <0, 0.031 > but usually <0, 0.003 > if ( cost < 0.0001 ) cost = 0.0001; @@ -1559,14 +1565,14 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq slp->setCost( cost ); // average angle is calculated with respect to periodicity of angles - double angle_avg = std::atan2( sin_avg / li->char_num, cos_avg / li->char_num ); + double angle_avg = std::atan2( sin_avg / characterCount, cos_avg / characterCount ); bool localreversed = flip ? !reversed : reversed; // displacement - we loop through 3 times, generating above, online then below line placements successively for ( int i = 0; i <= 2; ++i ) { std::unique_ptr< LabelPosition > p; if ( i == 0 && ( ( !localreversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) || ( localreversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) ) ) - p = _createCurvedCandidate( slp.get(), angle_avg, mLF->distLabel() + li->label_height / 2 ); + p = _createCurvedCandidate( slp.get(), angle_avg, mLF->distLabel() + li->characterHeight / 2 ); if ( i == 1 && flags & QgsLabeling::LinePlacementFlag::OnLine ) { p = _createCurvedCandidate( slp.get(), angle_avg, 0 ); @@ -1574,7 +1580,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq } if ( i == 2 && ( ( !localreversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) || ( localreversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) ) ) { - p = _createCurvedCandidate( slp.get(), angle_avg, -li->label_height / 2 - mLF->distLabel() ); + p = _createCurvedCandidate( slp.get(), angle_avg, -li->characterHeight / 2 - mLF->distLabel() ); p->setCost( p->cost() + 0.001 ); } diff --git a/src/core/pal/feature.h b/src/core/pal/feature.h index 1f250b33794..f66419e1e9a 100644 --- a/src/core/pal/feature.h +++ b/src/core/pal/feature.h @@ -55,32 +55,59 @@ namespace pal class CORE_EXPORT LabelInfo { public: - struct CharacterInfo - { - double width; - }; - LabelInfo( int num, double height, double maxinangle = 20.0, double maxoutangle = -20.0 ) + /** + * Constructor for LabelInfo + * \param characterHeight height of characters + * \param characterWidths vector of character widths + * \param maxinangle maximum acceptable in angle (in degrees) + * \param maxoutangle maximum acceptable out angle (in degrees) + */ + LabelInfo( double characterHeight, std::vector< double > characterWidths, double maxinangle = 20.0, double maxoutangle = -20.0 ) + : maxCharAngleInsideRadians( maxinangle * M_PI / 180 ) + // outside angle should be negative + , maxCharAngleOutsideRadians( ( maxoutangle > 0 ? -maxoutangle : maxoutangle ) * M_PI / 180 ) + , characterHeight( characterHeight ) + , mCharacterWidths( characterWidths ) { - max_char_angle_inside = maxinangle; - // outside angle should be negative - max_char_angle_outside = maxoutangle > 0 ? -maxoutangle : maxoutangle; - label_height = height; - char_num = num; - char_info = new CharacterInfo[num]; } - ~LabelInfo() { delete [] char_info; } //! LabelInfo cannot be copied LabelInfo( const LabelInfo &rh ) = delete; //! LabelInfo cannot be copied LabelInfo &operator=( const LabelInfo &rh ) = delete; - double max_char_angle_inside; - double max_char_angle_outside; - double label_height; - int char_num; - CharacterInfo *char_info = nullptr; + /** + * Maximum angle between inside curved label characters (in radians). + * \see maxCharAngleOutsideRadians + */ + double maxCharAngleInsideRadians = 0; + + /** + * Maximum angle between outside curved label characters (in radians). + * \see maxCharAngleInsideRadians + */ + double maxCharAngleOutsideRadians = 0; + + // TODO - maybe individual character height would give better results! + + /** + * Character height (actually font metrics height, not individual character height). + */ + double characterHeight = 0; + + /** + * Returns the total number of characters. + */ + int count() const { return static_cast< int >( mCharacterWidths.size() ); } + + /** + * Returns the width of the character at the specified position. + */ + double characterWidth( int position ) const { return mCharacterWidths[position]; } + + private: + std::vector< double > mCharacterWidths; }; @@ -238,8 +265,8 @@ namespace pal /** * Returns the label position for a curved label at a specific offset along a path. - * \param path_positions line path to place label on - * \param path_distances array of distances to each segment on path + * \param mapShape line path to place label on + * \param pathDistances array of distances to each segment on path * \param orientation can be 0 for automatic calculation of orientation, or -1/+1 for a specific label orientation * \param distance distance to offset label along curve by * \param reversed if TRUE label is reversed from lefttoright to righttoleft @@ -247,7 +274,7 @@ namespace pal * \param applyAngleConstraints TRUE if label feature character angle constraints should be applied * \returns calculated label position */ - std::unique_ptr< LabelPosition > curvedPlacementAtOffset( PointSet *path_positions, double *path_distances, + std::unique_ptr< LabelPosition > curvedPlacementAtOffset( PointSet *mapShape, const std::vector &pathDistances, int &orientation, double distance, bool &reversed, bool &flip, bool applyAngleConstraints ); /** diff --git a/src/core/pal/geomfunction.cpp b/src/core/pal/geomfunction.cpp index cd7175cad00..b768d9e4c45 100644 --- a/src/core/pal/geomfunction.cpp +++ b/src/core/pal/geomfunction.cpp @@ -378,6 +378,21 @@ void GeomFunction::findLineCircleIntersection( double cx, double cy, double radi double x1, double y1, double x2, double y2, double &xRes, double &yRes ) { + double multiplier = 1; + if ( radius < 10 ) + { + // these calculations get unstable for small coordinates differences, e.g. as a result of map labeling in a geographic + // CRS + multiplier = 10000; + x1 *= multiplier; + y1 *= multiplier; + x2 *= multiplier; + y2 *= multiplier; + cx *= multiplier; + cy *= multiplier; + radius *= multiplier; + } + double dx = x2 - x1; double dy = y2 - y1; @@ -406,4 +421,10 @@ void GeomFunction::findLineCircleIntersection( double cx, double cy, double radi xRes = x1 + t * dx; yRes = y1 + t * dy; } + + if ( multiplier != 1 ) + { + xRes /= multiplier; + yRes /= multiplier; + } } diff --git a/tests/testdata/control_images/labelingengine/expected_label_curved_label_small_segments/expected_label_curved_label_small_segments_mask.png b/tests/testdata/control_images/labelingengine/expected_label_curved_label_small_segments/expected_label_curved_label_small_segments_mask.png index 22043197c7d..5c5d9bee388 100644 Binary files a/tests/testdata/control_images/labelingengine/expected_label_curved_label_small_segments/expected_label_curved_label_small_segments_mask.png and b/tests/testdata/control_images/labelingengine/expected_label_curved_label_small_segments/expected_label_curved_label_small_segments_mask.png differ diff --git a/tests/testdata/control_images/labelingengine/expected_label_curved_negative_distance/expected_label_curved_negative_distance_mask.png b/tests/testdata/control_images/labelingengine/expected_label_curved_negative_distance/expected_label_curved_negative_distance_mask.png index 901345b34fb..f4072691bed 100644 Binary files a/tests/testdata/control_images/labelingengine/expected_label_curved_negative_distance/expected_label_curved_negative_distance_mask.png and b/tests/testdata/control_images/labelingengine/expected_label_curved_negative_distance/expected_label_curved_negative_distance_mask.png differ diff --git a/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches_mask.png b/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches_mask.png index 74d32f990a1..1d49c2a281f 100644 Binary files a/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches_mask.png and b/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches_mask.png differ