Skip to content
Permalink
Browse files
Rework the loop to use distance of each offseted curves
  • Loading branch information
nirvn authored and nyalldawson committed Apr 29, 2021
1 parent 370cf25 commit 76ee5281e4789ced08a139a912464f1ee5f0e0c9
Showing with 127 additions and 142 deletions.
  1. +124 −141 src/core/pal/feature.cpp
  2. +1 −1 src/core/pal/pointset.cpp
  3. +2 −0 tests/src/core/testqgslabelingengine.cpp
  4. BIN ...ontrol_images/expected_pal_canvas_line/sp_curved_placement_above/v1/sp_curved_placement_above.png
  5. BIN ...ontrol_images/expected_pal_canvas_line/sp_curved_placement_above/v2/sp_curved_placement_above.png
  6. BIN ...ontrol_images/expected_pal_canvas_line/sp_curved_placement_below/v1/sp_curved_placement_below.png
  7. BIN ...ontrol_images/expected_pal_canvas_line/sp_curved_placement_below/v2/sp_curved_placement_below.png
  8. BIN ...testdata/control_images/expected_pal_canvas_line/sp_length_expression/v1/sp_length_expression.png
  9. BIN ...testdata/control_images/expected_pal_canvas_line/sp_length_expression/v2/sp_length_expression.png
  10. BIN ...ges/expected_pal_composer_line/sp_img_curved_placement_above/v1/sp_img_curved_placement_above.png
  11. BIN ...ges/expected_pal_composer_line/sp_img_curved_placement_above/v2/sp_img_curved_placement_above.png
  12. BIN ...ges/expected_pal_composer_line/sp_img_curved_placement_below/v1/sp_img_curved_placement_below.png
  13. BIN ...ges/expected_pal_composer_line/sp_img_curved_placement_below/v2/sp_img_curved_placement_below.png
  14. BIN ...ontrol_images/expected_pal_composer_line/sp_img_length_expression/v1/sp_img_length_expression.png
  15. BIN ...ontrol_images/expected_pal_composer_line/sp_img_length_expression/v2/sp_img_length_expression.png
  16. BIN ...ges/expected_pal_composer_line/sp_pdf_curved_placement_above/v1/sp_pdf_curved_placement_above.png
  17. BIN ...ges/expected_pal_composer_line/sp_pdf_curved_placement_above/v2/sp_pdf_curved_placement_above.png
  18. BIN ...ges/expected_pal_composer_line/sp_pdf_curved_placement_below/v1/sp_pdf_curved_placement_below.png
  19. BIN ...ges/expected_pal_composer_line/sp_pdf_curved_placement_below/v2/sp_pdf_curved_placement_below.png
  20. BIN ...ontrol_images/expected_pal_composer_line/sp_pdf_length_expression/v1/sp_pdf_length_expression.png
  21. BIN ...ontrol_images/expected_pal_composer_line/sp_pdf_length_expression/v2/sp_pdf_length_expression.png
  22. BIN ...ges/expected_pal_composer_line/sp_svg_curved_placement_above/v2/sp_svg_curved_placement_above.png
  23. BIN ...ges/expected_pal_composer_line/sp_svg_curved_placement_below/v1/sp_svg_curved_placement_below.png
  24. BIN ...ges/expected_pal_composer_line/sp_svg_curved_placement_below/v2/sp_svg_curved_placement_below.png
  25. BIN ...ontrol_images/expected_pal_composer_line/sp_svg_length_expression/v1/sp_svg_length_expression.png
  26. BIN ...ontrol_images/expected_pal_composer_line/sp_svg_length_expression/v2/sp_svg_length_expression.png
  27. BIN ...ent/sp_prefer_line_curved_above_instead_of_below/sp_prefer_line_curved_above_instead_of_below.png
  28. BIN ...t/sp_prefer_line_curved_above_instead_of_online/sp_prefer_line_curved_above_instead_of_online.png
  29. BIN ...t/sp_prefer_line_curved_below_instead_of_online/sp_prefer_line_curved_below_instead_of_online.png
  30. BIN ...control_images/labelingengine/expected_curved_hint_anchor_end/expected_curved_hint_anchor_end.png
  31. BIN ...rol_images/labelingengine/expected_curved_hint_anchor_start/expected_curved_hint_anchor_start.png
  32. BIN ...rol_images/labelingengine/expected_curved_strict_anchor_end/expected_curved_strict_anchor_end.png
  33. BIN ...ges/labelingengine/expected_label_curved_label_above_1/v1/expected_label_curved_label_above_1.png
  34. BIN ...ges/labelingengine/expected_label_curved_label_above_1/v2/expected_label_curved_label_above_1.png
  35. BIN ...ges/labelingengine/expected_label_curved_label_below_1/v1/expected_label_curved_label_below_1.png
  36. BIN ...ges/labelingengine/expected_label_curved_label_below_1/v2/expected_label_curved_label_below_1.png
  37. BIN ...gengine/expected_label_curved_label_small_segments/expected_label_curved_label_small_segments.png
  38. BIN ...abelingengine/expected_label_curved_negative_distance/expected_label_curved_negative_distance.png
  39. BIN .../control_images/labelingengine/expected_label_curved_overrun/v1/expected_label_curved_overrun.png
  40. BIN .../control_images/labelingengine/expected_label_curved_overrun/v2/expected_label_curved_overrun.png
  41. BIN ...ine/expected_label_curved_small_feature_centered/expected_label_curved_small_feature_centered.png
  42. BIN ...l_images/labelingengine/expected_label_merged_minimum_size/expected_label_merged_minimum_size.png
  43. BIN ...gengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches.png
  44. BIN ...abelingengine/expected_label_multipart_touching_lines/expected_label_multipart_touching_lines.png
@@ -1309,186 +1309,168 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
expanded = mapShape->clone();
expanded->extendLineByDistance( overrun, overrun, mLF->overrunSmoothDistance() );
mapShape = expanded.get();
shapeLength = mapShape->length();
}

QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags();
if ( flags == 0 )
flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag

const bool hasAboveBelowLinePlacement = flags & QgsLabeling::LinePlacementFlag::AboveLine || flags & QgsLabeling::LinePlacementFlag::BelowLine;
const double offsetDistance = mLF->distLabel() + li->characterHeight() / 2;
std::unique_ptr< PointSet > mapShapeOffsetPositive;
std::unique_ptr< PointSet > mapShapeOffsetNegative;
if ( flags & QgsLabeling::LinePlacementFlag::AboveLine || flags & QgsLabeling::LinePlacementFlag::BelowLine )
if ( hasAboveBelowLinePlacement )
{
// create offseted map shapes to be used for above and below line placements
mapShapeOffsetPositive = mapShape->clone();
mapShapeOffsetPositive->offsetCurveByDistance( mLF->distLabel() + li->characterHeight() / 2 );
mapShapeOffsetNegative = mapShape->clone();
mapShapeOffsetNegative->offsetCurveByDistance( ( mLF->distLabel() + li->characterHeight() / 2 ) * -1 );
}

// distance calculation
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++ )
{
if ( i == 0 )
path_distances[i] = 0;
if ( offsetDistance >= 0.0 )
{
mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance );
mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance * -1 );
}
else
path_distances[i] = std::sqrt( std::pow( old_x - mapShape->x[i], 2 ) + std::pow( old_y - mapShape->y[i], 2 ) );
old_x = mapShape->x[i];
old_y = mapShape->y[i];

total_distance += path_distances[i];
{
if ( flags & QgsLabeling::LinePlacementFlag::AboveLine
&& !( flags & QgsLabeling::LinePlacementFlag::BelowLine ) )
{
flags &= ~QgsLabeling::LinePlacementFlag::AboveLine;
flags |= QgsLabeling::LinePlacementFlag::BelowLine;
}
else if ( flags & QgsLabeling::LinePlacementFlag::BelowLine
&& !( flags & QgsLabeling::LinePlacementFlag::AboveLine ) )
{
flags &= ~QgsLabeling::LinePlacementFlag::BelowLine;
flags |= QgsLabeling::LinePlacementFlag::AboveLine;
}
mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance * -1 );
mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance );
}
}

if ( qgsDoubleNear( total_distance, 0.0 ) )
return 0;

std::vector< double > pathDistancesPositive;
if ( mapShapeOffsetPositive )
std::vector< std::unique_ptr< LabelPosition >> positions;
for ( int i = 0; i <= 2; i++ )
{
pathDistancesPositive.resize( mapShapeOffsetPositive->nbPoints );
double old_x = -1.0, old_y = -1.0;
for ( int i = 0; i < mapShapeOffsetPositive->nbPoints; i++ )
PointSet *currentMapShape = nullptr;
if ( i == 0 && hasAboveBelowLinePlacement )
{
if ( i == 0 )
pathDistancesPositive[i] = 0;
else
pathDistancesPositive[i] = std::sqrt( std::pow( old_x - mapShapeOffsetPositive->x[i], 2 ) + std::pow( old_y - mapShapeOffsetPositive->y[i], 2 ) );
old_x = mapShapeOffsetPositive->x[i];
old_y = mapShapeOffsetPositive->y[i];
currentMapShape = mapShapeOffsetPositive.get();
}
}
if ( i == 1 && flags & QgsLabeling::LinePlacementFlag::OnLine )
{
currentMapShape = mapShape;
}
if ( i == 2 && hasAboveBelowLinePlacement )
{
currentMapShape = mapShapeOffsetNegative.get();
}
if ( !currentMapShape )
continue;

std::vector< double > pathDistancesNegative;
if ( mapShapeOffsetNegative )
{
pathDistancesNegative.resize( mapShapeOffsetNegative->nbPoints );
double old_x = -1.0, old_y = -1.0;
for ( int i = 0; i < mapShapeOffsetNegative->nbPoints; i++ )
// distance calculation
std::vector< double > pathDistances( currentMapShape->nbPoints );
double totalDistance = 0;
double oldX = -1.0, oldY = -1.0;
for ( int i = 0; i < currentMapShape->nbPoints; i++ )
{
if ( i == 0 )
pathDistancesNegative[i] = 0;
pathDistances[i] = 0;
else
pathDistancesNegative[i] = std::sqrt( std::pow( old_x - mapShapeOffsetNegative->x[i], 2 ) + std::pow( old_y - mapShapeOffsetNegative->y[i], 2 ) );
old_x = mapShapeOffsetNegative->x[i];
old_y = mapShapeOffsetNegative->y[i];
pathDistances[i] = std::sqrt( std::pow( oldX - currentMapShape->x[i], 2 ) + std::pow( oldY - currentMapShape->y[i], 2 ) );
oldX = currentMapShape->x[i];
oldY = currentMapShape->y[i];

totalDistance += pathDistances[i];
}
}
if ( qgsDoubleNear( totalDistance, 0.0 ) )
continue;

const double lineAnchorPoint = total_distance * mLF->lineAnchorPercent();
const double lineAnchorPoint = totalDistance * mLF->lineAnchorPercent();

if ( pal->isCanceled() )
return 0;
if ( pal->isCanceled() )
return 0;

std::vector< std::unique_ptr< LabelPosition >> positions;
const std::size_t candidateTargetCount = maximumLineCandidates();
double delta = std::max( li->characterHeight() / 6, total_distance / candidateTargetCount );
const std::size_t candidateTargetCount = maximumLineCandidates();
double delta = std::max( li->characterHeight() / 6, totalDistance / candidateTargetCount );

// generate curved labels
double distanceAlongLineToStartCandidate = 0;
bool singleCandidateOnly = false;
switch ( mLF->lineAnchorType() )
{
case QgsLabelLineSettings::AnchorType::HintOnly:
break;
// generate curved labels
double distanceAlongLineToStartCandidate = 0;
bool singleCandidateOnly = false;
switch ( mLF->lineAnchorType() )
{
case QgsLabelLineSettings::AnchorType::HintOnly:
break;

case QgsLabelLineSettings::AnchorType::Strict:
distanceAlongLineToStartCandidate = std::min( lineAnchorPoint, total_distance * 0.99 - getLabelWidth() );
singleCandidateOnly = true;
break;
}
case QgsLabelLineSettings::AnchorType::Strict:
distanceAlongLineToStartCandidate = std::min( lineAnchorPoint, totalDistance * 0.99 - getLabelWidth() );
singleCandidateOnly = true;
break;
}

auto calculateCost = [ = ]( LabelPosition * labelPosition, double distanceToStartCandidate )
{
// evaluate cost
double angle_diff = 0.0, angle_last = 0.0, diff;
LabelPosition *tmp = labelPosition;
double sin_avg = 0, cos_avg = 0;
while ( tmp )
for ( ; distanceAlongLineToStartCandidate <= totalDistance; distanceAlongLineToStartCandidate += delta )
{
if ( tmp != labelPosition ) // not first?
{
diff = std::fabs( tmp->getAlpha() - angle_last );
if ( diff > 2 * M_PI ) diff -= 2 * M_PI;
diff = std::min( diff, 2 * M_PI - diff ); // difference 350 deg is actually just 10 deg...
angle_diff += diff;
}
if ( pal->isCanceled() )
return 0;

sin_avg += std::sin( tmp->getAlpha() );
cos_avg += std::cos( tmp->getAlpha() );
angle_last = tmp->getAlpha();
tmp = tmp->nextPart();
}
// placements may need to be reversed if using map orientation and the line has right-to-left direction
bool labeledLineSegmentIsRightToLeft = false;
const QgsTextRendererUtils::LabelLineDirection direction = ( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) ? QgsTextRendererUtils::RespectPainterOrientation : QgsTextRendererUtils::FollowLineDirection;
std::unique_ptr< LabelPosition > labelPosition = curvedPlacementAtOffset( currentMapShape, pathDistances, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeft, !singleCandidateOnly );

// 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 = 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;
if ( !labelPosition )
continue;
if ( ( i == 0 || i == 2 ) && !labeledLineSegmentIsRightToLeft && !( flags & QgsLabeling::LinePlacementFlag::AboveLine ) )
continue;
if ( ( i == 0 || i == 2 ) && labeledLineSegmentIsRightToLeft && !( flags & QgsLabeling::LinePlacementFlag::BelowLine ) )
continue;

// penalize positions which are further from the line's anchor point
double labelCenter = distanceToStartCandidate + getLabelWidth() / 2;
double costCenter = std::fabs( lineAnchorPoint - labelCenter ) / total_distance; // <0, 0.5>
cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line
labelPosition->setCost( cost );
};
// evaluate cost
double angleDiff = 0.0, angleLast = 0.0, diff;
LabelPosition *tmp = labelPosition.get();
double sinAvg = 0, cosAvg = 0;
while ( tmp )
{
if ( tmp != labelPosition.get() ) // not first?
{
diff = std::fabs( tmp->getAlpha() - angleLast );
if ( diff > 2 * M_PI ) diff -= 2 * M_PI;
diff = std::min( diff, 2 * M_PI - diff ); // difference 350 deg is actually just 10 deg...
angleDiff += diff;
}

for ( ; distanceAlongLineToStartCandidate <= total_distance; distanceAlongLineToStartCandidate += delta )
{
if ( pal->isCanceled() )
return 0;
sinAvg += std::sin( tmp->getAlpha() );
cosAvg += std::cos( tmp->getAlpha() );
angleLast = tmp->getAlpha();
tmp = tmp->nextPart();
}

// placements may need to be reversed if using map orientation and the line has right-to-left direction
bool labeledLineSegmentIsRightToLeft = false, labeledLineSegmentIsRightToLeftOffsetPositive = false, labeledLineSegmentIsRightToLeftOffsetNegative = false;
const QgsTextRendererUtils::LabelLineDirection direction = ( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) ? QgsTextRendererUtils::RespectPainterOrientation : QgsTextRendererUtils::FollowLineDirection;
// 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 angleDiffAvg = characterCount > 1 ? ( angleDiff / ( characterCount - 1 ) ) : 0; // <0, pi> but pi/8 is much already
double cost = angleDiffAvg / 100; // <0, 0.031 > but usually <0, 0.003 >
if ( cost < 0.0001 )
cost = 0.0001;

std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeft, !singleCandidateOnly );
std::unique_ptr< LabelPosition > slpOffsetPositive;
std::unique_ptr< LabelPosition > slpOffsetNegative;
if ( mapShapeOffsetPositive )
{
slpOffsetPositive = curvedPlacementAtOffset( mapShapeOffsetPositive.get(), pathDistancesPositive, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeftOffsetPositive, !singleCandidateOnly );
slpOffsetNegative = curvedPlacementAtOffset( mapShapeOffsetNegative.get(), pathDistancesNegative, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeftOffsetNegative, !singleCandidateOnly );
}
// penalize positions which are further from the line's anchor point
double labelCenter = distanceAlongLineToStartCandidate + getLabelWidth() / 2;
double costCenter = std::fabs( lineAnchorPoint - labelCenter ) / totalDistance; // <0, 0.5>
cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line

// 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 && flags & QgsLabeling::LinePlacementFlag::AboveLine )
const bool isBelow = ( i == 0 || i == 2 ) && labeledLineSegmentIsRightToLeft;
if ( isBelow )
{
// labels "above" line
LabelPosition *labelPosition = labeledLineSegmentIsRightToLeftOffsetPositive ? slpOffsetNegative.get() : slpOffsetPositive.get();
if ( !labelPosition )
continue;
calculateCost( labelPosition, distanceAlongLineToStartCandidate );
p = std::make_unique< LabelPosition >( *labelPosition );
// add additional cost for on line placement
cost += 0.001;
}
if ( i == 1 && flags & QgsLabeling::LinePlacementFlag::OnLine )
else if ( i == 1 )
{
// labels on line
if ( !slp )
continue;
calculateCost( slp.get(), distanceAlongLineToStartCandidate );
p = std::make_unique< LabelPosition >( *slp.get() );
p->setCost( p->cost() + 0.002 );
}
if ( i == 2 && flags & QgsLabeling::LinePlacementFlag::BelowLine )
{
// labels "below" line
LabelPosition *labelPosition = labeledLineSegmentIsRightToLeftOffsetNegative ? slpOffsetNegative.get() : slpOffsetPositive.get();
if ( !labelPosition )
continue;
calculateCost( labelPosition, distanceAlongLineToStartCandidate );
p = std::make_unique< LabelPosition >( *labelPosition );
p->setCost( p->cost() + 0.001 );
// add additional cost for below line placement
cost += 0.002;
}

labelPosition->setCost( cost );

std::unique_ptr< LabelPosition > p = std::make_unique< LabelPosition >( *labelPosition );
if ( p && mLF->permissibleZonePrepared() )
{
bool within = true;
@@ -1506,9 +1488,10 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq

if ( p )
positions.emplace_back( std::move( p ) );

if ( singleCandidateOnly )
break;
}
if ( singleCandidateOnly )
break;
}

for ( std::unique_ptr< LabelPosition > &pos : positions )
@@ -557,7 +557,7 @@ void PointSet::offsetCurveByDistance( double distance )
GEOSGeometry *newGeos;
try
{
newGeos = GEOSOffsetCurve_r( geosctxt, mGeos, distance, 0, GEOSBUF_JOIN_ROUND, 2 );
newGeos = GEOSOffsetCurve_r( geosctxt, mGeos, distance, 0, GEOSBUF_JOIN_MITRE, 2 );
const int newNbPoints = GEOSGeomGetNumPoints_r( geosctxt, newGeos );
const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, newGeos );
std::vector< double > newX;
@@ -2577,6 +2577,8 @@ void TestQgsLabelingEngine::curvedOverrun()
QVERIFY( imageCheck( QStringLiteral( "label_curved_no_overrun" ), img, 20 ) );

settings.lineSettings().setOverrunDistance( 11 );
settings.maxCurvedCharAngleIn = 99;
settings.maxCurvedCharAngleOut = 99;
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary!
vl2->setLabelsEnabled( true );
QgsMapRendererSequentialJob job2( mapSettings );
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.

0 comments on commit 76ee528

Please sign in to comment.