[core] Introduce PlacementContext

Make `placeSymbol()` a method and introduce copiable `PlacementContext`.
This commit is contained in:
Mikhail Pozdnyakov
2020-04-01 15:20:21 +03:00
parent 42dc1e997f
commit a32672b9e6
2 changed files with 480 additions and 417 deletions

View File

@@ -59,6 +59,93 @@ const CollisionGroups::CollisionGroup& CollisionGroups::get(const std::string& s
}
}
using namespace style;
// PlacementContext implemenation
class PlacementContext {
std::reference_wrapper<const SymbolBucket> bucket;
std::reference_wrapper<const RenderTile> renderTile;
std::reference_wrapper<const TransformState> state;
public:
PlacementContext(const SymbolBucket& bucket_,
const RenderTile& renderTile_,
const TransformState& state_,
float placementZoom,
const CollisionGroups::CollisionGroup& collisionGroup_,
optional<CollisionBoundaries> avoidEdges_ = nullopt)
: bucket(bucket_),
renderTile(renderTile_),
state(state_),
pixelsToTileUnits(renderTile_.id.pixelsToTileUnits(1, placementZoom)),
scale(std::pow(2, placementZoom - getOverscaledID().overscaledZ)),
pixelRatio(util::tileSize * getOverscaledID().overscaleFactor() / util::EXTENT),
collisionGroup(collisionGroup_),
partiallyEvaluatedTextSize(bucket_.textSizeBinder->evaluateForZoom(placementZoom)),
partiallyEvaluatedIconSize(bucket_.iconSizeBinder->evaluateForZoom(placementZoom)),
avoidEdges(std::move(avoidEdges_)) {}
const SymbolBucket& getBucket() const { return bucket.get(); }
const style::SymbolLayoutProperties::PossiblyEvaluated& getLayout() const { return *getBucket().layout; }
const RenderTile& getRenderTile() const { return renderTile.get(); }
const OverscaledTileID& getOverscaledID() const { return renderTile.get().getOverscaledTileID(); }
const TransformState& getTransformState() const { return state; }
const std::vector<style::TextVariableAnchorType>& getVariableTextAnchors() const {
return getLayout().get<style::TextVariableAnchor>();
}
const float pixelsToTileUnits;
const float scale;
const float pixelRatio;
const bool rotateTextWithMap = getLayout().get<TextRotationAlignment>() == AlignmentType::Map;
const bool pitchTextWithMap = getLayout().get<TextPitchAlignment>() == AlignmentType::Map;
const bool rotateIconWithMap = getLayout().get<IconRotationAlignment>() == AlignmentType::Map;
const bool pitchIconWithMap = getLayout().get<IconPitchAlignment>() == AlignmentType::Map;
const SymbolPlacementType placementType = getLayout().get<SymbolPlacement>();
const mat4 textLabelPlaneMatrix =
getLabelPlaneMatrix(renderTile.get().matrix, pitchTextWithMap, rotateTextWithMap, state, pixelsToTileUnits);
const mat4 iconLabelPlaneMatrix =
(rotateTextWithMap == rotateIconWithMap && pitchTextWithMap == pitchIconWithMap)
? textLabelPlaneMatrix
: getLabelPlaneMatrix(
renderTile.get().matrix, pitchIconWithMap, rotateIconWithMap, state, pixelsToTileUnits);
const CollisionGroups::CollisionGroup collisionGroup;
const ZoomEvaluatedSize partiallyEvaluatedTextSize;
const ZoomEvaluatedSize partiallyEvaluatedIconSize;
const bool textAllowOverlap = getLayout().get<style::TextAllowOverlap>();
const bool iconAllowOverlap = getLayout().get<style::IconAllowOverlap>();
// This logic is similar to the "defaultOpacityState" logic below in updateBucketOpacities
// If we know a symbol is always supposed to show, force it to be marked visible even if
// it wasn't placed into the collision index (because some or all of it was outside the range
// of the collision grid).
// There is a subtle edge case here we're accepting:
// Symbol A has text-allow-overlap: true, icon-allow-overlap: true, icon-optional: false
// A's icon is outside the grid, so doesn't get placed
// A's text would be inside grid, but doesn't get placed because of icon-optional: false
// We still show A because of the allow-overlap settings.
// Symbol B has allow-overlap: false, and gets placed where A's text would be
// On panning in, there is a short period when Symbol B and Symbol A will overlap
// This is the reverse of our normal policy of "fade in on pan", but should look like any other
// collision and hopefully not be too noticeable.
// See https://github.com/mapbox/mapbox-gl-native/issues/12683
const bool alwaysShowText =
textAllowOverlap && (iconAllowOverlap || !(getBucket().hasIconData() || getBucket().hasSdfIconData()) ||
getLayout().get<style::IconOptional>());
const bool alwaysShowIcon =
iconAllowOverlap && (textAllowOverlap || !getBucket().hasTextData() || getLayout().get<style::TextOptional>());
const bool hasIconTextFit = getLayout().get<IconTextFit>() != IconTextFitType::None;
const optional<CollisionBoundaries> avoidEdges;
};
// PlacementController implemenation
PlacementController::PlacementController() : placement(makeMutable<Placement>()) {}
@@ -143,419 +230,388 @@ Point<float> calculateVariableLayoutOffset(style::SymbolAnchorType anchor,
void Placement::placeSymbolBucket(const BucketPlacementData& params, std::set<uint32_t>& seenCrossTileIDs) {
assert(updateParameters);
const auto& bucket = static_cast<const SymbolBucket&>(params.bucket.get());
const auto& layout = *bucket.layout;
const SymbolBucket& symbolBucket = static_cast<const SymbolBucket&>(params.bucket.get());
const RenderTile& renderTile = params.tile;
const auto& state = collisionIndex.getTransformState();
const float pixelsToTileUnits = renderTile.id.pixelsToTileUnits(1, placementZoom);
const OverscaledTileID& overscaledID = renderTile.getOverscaledTileID();
const float scale = std::pow(2, placementZoom - overscaledID.overscaledZ);
const float pixelRatio = (util::tileSize * overscaledID.overscaleFactor()) / util::EXTENT;
const bool rotateTextWithMap = layout.get<style::TextRotationAlignment>() == style::AlignmentType::Map;
const bool pitchTextWithMap = layout.get<style::TextPitchAlignment>() == style::AlignmentType::Map;
const bool rotateIconWithMap = layout.get<style::IconRotationAlignment>() == style::AlignmentType::Map;
const bool pitchIconWithMap = layout.get<style::IconPitchAlignment>() == style::AlignmentType::Map;
const style::SymbolPlacementType placementType = layout.get<style::SymbolPlacement>();
const mat4& posMatrix = renderTile.matrix;
mat4 textLabelPlaneMatrix =
getLabelPlaneMatrix(posMatrix, pitchTextWithMap, rotateTextWithMap, state, pixelsToTileUnits);
mat4 iconLabelPlaneMatrix;
if (rotateTextWithMap == rotateIconWithMap && pitchTextWithMap == pitchIconWithMap) {
iconLabelPlaneMatrix = textLabelPlaneMatrix;
} else {
iconLabelPlaneMatrix =
getLabelPlaneMatrix(posMatrix, pitchIconWithMap, rotateIconWithMap, state, pixelsToTileUnits);
}
const auto& collisionGroup = collisionGroups.get(params.sourceId);
auto partiallyEvaluatedTextSize = bucket.textSizeBinder->evaluateForZoom(placementZoom);
auto partiallyEvaluatedIconSize = bucket.iconSizeBinder->evaluateForZoom(placementZoom);
optional<CollisionBoundaries> avoidEdges = getAvoidEdges(bucket, posMatrix);
const bool textAllowOverlap = layout.get<style::TextAllowOverlap>();
const bool iconAllowOverlap = layout.get<style::IconAllowOverlap>();
// This logic is similar to the "defaultOpacityState" logic below in updateBucketOpacities
// If we know a symbol is always supposed to show, force it to be marked visible even if
// it wasn't placed into the collision index (because some or all of it was outside the range
// of the collision grid).
// There is a subtle edge case here we're accepting:
// Symbol A has text-allow-overlap: true, icon-allow-overlap: true, icon-optional: false
// A's icon is outside the grid, so doesn't get placed
// A's text would be inside grid, but doesn't get placed because of icon-optional: false
// We still show A because of the allow-overlap settings.
// Symbol B has allow-overlap: false, and gets placed where A's text would be
// On panning in, there is a short period when Symbol B and Symbol A will overlap
// This is the reverse of our normal policy of "fade in on pan", but should look like any other
// collision and hopefully not be too noticeable.
// See https://github.com/mapbox/mapbox-gl-native/issues/12683
const bool alwaysShowText = textAllowOverlap && (iconAllowOverlap || !(bucket.hasIconData() || bucket.hasSdfIconData()) || layout.get<style::IconOptional>());
const bool alwaysShowIcon = iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || layout.get<style::TextOptional>());
std::vector<style::TextVariableAnchorType> variableTextAnchors = layout.get<style::TextVariableAnchor>();
const bool hasIconTextFit = layout.get<style::IconTextFit>() != style::IconTextFitType::None;
std::vector<ProjectedCollisionBox> textBoxes;
std::vector<ProjectedCollisionBox> iconBoxes;
auto placeSymbol = [&](const SymbolInstance& symbolInstance) {
if (symbolInstance.crossTileID == SymbolInstance::invalidCrossTileID() ||
seenCrossTileIDs.count(symbolInstance.crossTileID) != 0u)
return;
if (renderTile.holdForFade()) {
// Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
// know yet if we have a duplicate in a parent tile that _should_ be placed.
placements.emplace(symbolInstance.crossTileID, JointPlacement(false, false, false));
return;
}
textBoxes.clear();
iconBoxes.clear();
bool placeText = false;
bool placeIcon = false;
bool offscreen = true;
std::pair<bool, bool> placed{ false, false };
std::pair<bool, bool> placedVerticalText{ false, false };
std::pair<bool, bool> placedVerticalIcon{ false, false };
Point<float> shift{0.0f, 0.0f};
optional<size_t> horizontalTextIndex = symbolInstance.getDefaultHorizontalPlacedTextIndex();
if (horizontalTextIndex) {
const PlacedSymbol& placedSymbol = bucket.text.placedSymbols.at(*horizontalTextIndex);
const float fontSize = evaluateSizeForFeature(partiallyEvaluatedTextSize, placedSymbol);
const auto updatePreviousOrientationIfNotPlaced = [&](bool isPlaced) {
if (bucket.allowVerticalPlacement && !isPlaced && getPrevPlacement()) {
auto prevOrientation = getPrevPlacement()->placedOrientations.find(symbolInstance.crossTileID);
if (prevOrientation != getPrevPlacement()->placedOrientations.end()) {
placedOrientations[symbolInstance.crossTileID] = prevOrientation->second;
}
}
};
const auto placeTextForPlacementModes = [&](auto& placeHorizontalFn, auto& placeVerticalFn) {
if (bucket.allowVerticalPlacement && symbolInstance.writingModes & WritingModeType::Vertical) {
assert(!bucket.placementModes.empty());
for (auto& placementMode : bucket.placementModes) {
if (placementMode == style::TextWritingModeType::Vertical) {
placedVerticalText = placed = placeVerticalFn();
} else {
placed = placeHorizontalFn();
}
if (placed.first) {
break;
}
}
} else {
placed = placeHorizontalFn();
}
};
// Line or point label placement
if (variableTextAnchors.empty()) {
const auto placeFeature = [&](const CollisionFeature& collisionFeature,
style::TextWritingModeType orientation) {
textBoxes.clear();
auto placedFeature = collisionIndex.placeFeature(collisionFeature,
{},
posMatrix,
textLabelPlaneMatrix,
pixelRatio,
placedSymbol,
scale,
fontSize,
textAllowOverlap,
pitchTextWithMap,
showCollisionBoxes,
avoidEdges,
collisionGroup.second,
textBoxes);
if (placedFeature.first) {
placedOrientations.emplace(symbolInstance.crossTileID, orientation);
}
return placedFeature;
};
const auto placeHorizontal = [&] {
return placeFeature(symbolInstance.textCollisionFeature, style::TextWritingModeType::Horizontal);
};
const auto placeVertical = [&] {
if (bucket.allowVerticalPlacement && symbolInstance.verticalTextCollisionFeature) {
return placeFeature(*symbolInstance.verticalTextCollisionFeature,
style::TextWritingModeType::Vertical);
}
return std::pair<bool, bool>{false, false};
};
placeTextForPlacementModes(placeHorizontal, placeVertical);
updatePreviousOrientationIfNotPlaced(placed.first);
placeText = placed.first;
offscreen &= placed.second;
} else if (!symbolInstance.textCollisionFeature.alongLine &&
!symbolInstance.textCollisionFeature.boxes.empty()) {
// If this symbol was in the last placement, shift the previously used
// anchor to the front of the anchor list, only if the previous anchor
// is still in the anchor list.
if (getPrevPlacement()) {
auto prevOffset = getPrevPlacement()->variableOffsets.find(symbolInstance.crossTileID);
if (prevOffset != getPrevPlacement()->variableOffsets.end()) {
const auto prevAnchor = prevOffset->second.anchor;
auto found = std::find(variableTextAnchors.begin(), variableTextAnchors.end(), prevAnchor);
if (found != variableTextAnchors.begin() && found != variableTextAnchors.end()) {
std::vector<style::TextVariableAnchorType> filtered{prevAnchor};
if (!isTiltedView()) {
for (auto anchor : variableTextAnchors) {
if (anchor != prevAnchor) {
filtered.push_back(anchor);
}
}
}
variableTextAnchors = std::move(filtered);
}
}
}
const bool doVariableIconPlacement =
hasIconTextFit && !iconAllowOverlap && symbolInstance.placedIconIndex;
const auto placeFeatureForVariableAnchors = [&](const CollisionFeature& textCollisionFeature,
style::TextWritingModeType orientation,
const CollisionFeature& iconCollisionFeature) {
const CollisionBox& textBox = textCollisionFeature.boxes[0];
const float width = textBox.x2 - textBox.x1;
const float height = textBox.y2 - textBox.y1;
const float textBoxScale = symbolInstance.textBoxScale;
std::pair<bool, bool> placedFeature = {false, false};
const size_t anchorsSize = variableTextAnchors.size();
const size_t placementAttempts = textAllowOverlap ? anchorsSize * 2 : anchorsSize;
for (size_t i = 0u; i < placementAttempts; ++i) {
auto anchor = variableTextAnchors[i % anchorsSize];
const bool isFirstAnchor = (anchor == variableTextAnchors.front());
const bool allowOverlap = (i >= anchorsSize);
shift = calculateVariableLayoutOffset(anchor,
width,
height,
symbolInstance.variableTextOffset,
textBoxScale,
rotateTextWithMap,
pitchTextWithMap,
state.getBearing());
textBoxes.clear();
if (!isFirstAnchor &&
stickToFirstVariableAnchor(
symbolInstance.textCollisionFeature.boxes.front(), shift, posMatrix, pixelRatio)) {
continue;
}
placedFeature = collisionIndex.placeFeature(textCollisionFeature,
shift,
posMatrix,
mat4(),
pixelRatio,
placedSymbol,
scale,
fontSize,
allowOverlap,
pitchTextWithMap,
showCollisionBoxes,
avoidEdges,
collisionGroup.second,
textBoxes);
if (doVariableIconPlacement) {
auto placedIconFeature =
collisionIndex.placeFeature(iconCollisionFeature,
shift,
posMatrix,
iconLabelPlaneMatrix,
pixelRatio,
placedSymbol,
scale,
fontSize,
iconAllowOverlap,
pitchTextWithMap, // TODO: shall it be pitchIconWithMap?
showCollisionBoxes,
avoidEdges,
collisionGroup.second,
iconBoxes);
iconBoxes.clear();
if (!placedIconFeature.first) continue;
}
if (placedFeature.first) {
assert(symbolInstance.crossTileID != 0u);
optional<style::TextVariableAnchorType> prevAnchor;
// If this label was placed in the previous placement, record the anchor position
// to allow us to animate the transition
if (getPrevPlacement()) {
auto prevOffset = getPrevPlacement()->variableOffsets.find(symbolInstance.crossTileID);
auto prevPlacements = getPrevPlacement()->placements.find(symbolInstance.crossTileID);
if (prevOffset != getPrevPlacement()->variableOffsets.end() &&
prevPlacements != getPrevPlacement()->placements.end() &&
prevPlacements->second.text) {
// TODO: The prevAnchor seems to be unused, needs to be fixed.
prevAnchor = prevOffset->second.anchor;
}
}
variableOffsets.insert(std::make_pair(symbolInstance.crossTileID, VariableOffset{
symbolInstance.variableTextOffset,
width,
height,
anchor,
textBoxScale,
prevAnchor
}));
if (bucket.allowVerticalPlacement) {
placedOrientations.emplace(symbolInstance.crossTileID, orientation);
}
break;
}
}
return placedFeature;
};
const auto placeHorizontal = [&] {
return placeFeatureForVariableAnchors(symbolInstance.textCollisionFeature,
style::TextWritingModeType::Horizontal,
symbolInstance.iconCollisionFeature);
};
const auto placeVertical = [&] {
if (bucket.allowVerticalPlacement && !placed.first && symbolInstance.verticalTextCollisionFeature) {
return placeFeatureForVariableAnchors(*symbolInstance.verticalTextCollisionFeature,
style::TextWritingModeType::Vertical,
symbolInstance.verticalIconCollisionFeature
? *symbolInstance.verticalIconCollisionFeature
: symbolInstance.iconCollisionFeature);
}
return std::pair<bool, bool>{false, false};
};
placeTextForPlacementModes(placeHorizontal, placeVertical);
placeText = placed.first;
offscreen &= placed.second;
updatePreviousOrientationIfNotPlaced(placed.first);
// If we didn't get placed, we still need to copy our position from the last placement for
// fade animations
if (!placeText && getPrevPlacement()) {
auto prevOffset = getPrevPlacement()->variableOffsets.find(symbolInstance.crossTileID);
if (prevOffset != getPrevPlacement()->variableOffsets.end()) {
variableOffsets[symbolInstance.crossTileID] = prevOffset->second;
}
}
}
}
if (symbolInstance.placedIconIndex) {
if (!hasIconTextFit || !placeText || variableTextAnchors.empty()) {
shift = {0.0f, 0.0f};
}
const auto& iconBuffer = symbolInstance.hasSdfIcon() ? bucket.sdfIcon : bucket.icon;
const PlacedSymbol& placedSymbol = iconBuffer.placedSymbols.at(*symbolInstance.placedIconIndex);
const float fontSize = evaluateSizeForFeature(partiallyEvaluatedIconSize, placedSymbol);
const auto& placeIconFeature = [&](const CollisionFeature& collisionFeature) {
return collisionIndex.placeFeature(collisionFeature,
shift,
posMatrix,
iconLabelPlaneMatrix,
pixelRatio,
placedSymbol,
scale,
fontSize,
iconAllowOverlap,
pitchTextWithMap,
showCollisionBoxes,
avoidEdges,
collisionGroup.second,
iconBoxes);
};
std::pair<bool, bool> placedIcon = {false, false};
if (placedVerticalText.first && symbolInstance.verticalIconCollisionFeature) {
placedIcon = placedVerticalIcon = placeIconFeature(*symbolInstance.verticalIconCollisionFeature);
} else {
placedIcon = placeIconFeature(symbolInstance.iconCollisionFeature);
}
placeIcon = placedIcon.first;
offscreen &= placedIcon.second;
}
const bool iconWithoutText = !symbolInstance.hasText() || layout.get<style::TextOptional>();
const bool textWithoutIcon = !symbolInstance.hasIcon() || layout.get<style::IconOptional>();
// combine placements for icon and text
if (!iconWithoutText && !textWithoutIcon) {
placeText = placeIcon = placeText && placeIcon;
} else if (!textWithoutIcon) {
placeText = placeText && placeIcon;
} else if (!iconWithoutText) {
placeIcon = placeText && placeIcon;
}
if (placeText) {
if (placedVerticalText.first && symbolInstance.verticalTextCollisionFeature) {
collisionIndex.insertFeature(*symbolInstance.verticalTextCollisionFeature, textBoxes, layout.get<style::TextIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first);
} else {
collisionIndex.insertFeature(symbolInstance.textCollisionFeature, textBoxes, layout.get<style::TextIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first);
}
}
if (placeIcon) {
if (placedVerticalIcon.first && symbolInstance.verticalIconCollisionFeature) {
collisionIndex.insertFeature(*symbolInstance.verticalIconCollisionFeature, iconBoxes, layout.get<style::IconIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first);
} else {
collisionIndex.insertFeature(symbolInstance.iconCollisionFeature, iconBoxes, layout.get<style::IconIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first);
}
}
const bool hasIconCollisionCircleData = bucket.hasIconCollisionCircleData();
const bool hasTextCollisionCircleData = bucket.hasTextCollisionCircleData();
if (hasIconCollisionCircleData && symbolInstance.iconCollisionFeature.alongLine && !iconBoxes.empty()) {
collisionCircles[&symbolInstance.iconCollisionFeature] = iconBoxes;
}
if (hasTextCollisionCircleData && symbolInstance.textCollisionFeature.alongLine && !textBoxes.empty()) {
collisionCircles[&symbolInstance.textCollisionFeature] = textBoxes;
}
assert(symbolInstance.crossTileID != 0);
if (placements.find(symbolInstance.crossTileID) != placements.end()) {
// If there's a previous placement with this ID, it comes from a tile that's fading out
// Erase it so that the placement result from the non-fading tile supersedes it
placements.erase(symbolInstance.crossTileID);
}
auto pair = placements.emplace(
symbolInstance.crossTileID,
JointPlacement(placeText || alwaysShowText, placeIcon || alwaysShowIcon, offscreen || bucket.justReloaded));
assert(pair.second);
newSymbolPlaced(symbolInstance, pair.first->second, placementType, textBoxes, iconBoxes);
seenCrossTileIDs.insert(symbolInstance.crossTileID);
};
for (const SymbolInstance& symbol : getSortedSymbols(params, pixelRatio)) {
placeSymbol(symbol);
PlacementContext ctx{symbolBucket,
params.tile,
collisionIndex.getTransformState(),
placementZoom,
collisionGroups.get(params.sourceId),
getAvoidEdges(symbolBucket, renderTile.matrix)};
for (const SymbolInstance& symbol : getSortedSymbols(params, ctx.pixelRatio)) {
placeSymbol(symbol, ctx, seenCrossTileIDs);
}
// As long as this placement lives, we have to hold onto this bucket's
// matching FeatureIndex/data for querying purposes
retainedQueryData.emplace(std::piecewise_construct,
std::forward_as_tuple(bucket.bucketInstanceId),
std::forward_as_tuple(bucket.bucketInstanceId, params.featureIndex, overscaledID));
retainedQueryData.emplace(
std::piecewise_construct,
std::forward_as_tuple(symbolBucket.bucketInstanceId),
std::forward_as_tuple(symbolBucket.bucketInstanceId, params.featureIndex, ctx.getOverscaledID()));
}
void Placement::placeSymbol(const SymbolInstance& symbolInstance,
const PlacementContext& ctx,
std::set<uint32_t>& seenCrossTileIDs) {
if (symbolInstance.crossTileID == SymbolInstance::invalidCrossTileID() ||
seenCrossTileIDs.count(symbolInstance.crossTileID) != 0u)
return;
if (ctx.getRenderTile().holdForFade()) {
// Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
// know yet if we have a duplicate in a parent tile that _should_ be placed.
placements.emplace(symbolInstance.crossTileID, JointPlacement(false, false, false));
return;
}
const SymbolBucket& bucket = ctx.getBucket();
const mat4& posMatrix = ctx.getRenderTile().matrix;
const auto& collisionGroup = ctx.collisionGroup;
auto variableTextAnchors = ctx.getVariableTextAnchors();
textBoxes.clear();
iconBoxes.clear();
bool placeText = false;
bool placeIcon = false;
bool offscreen = true;
std::pair<bool, bool> placed{false, false};
std::pair<bool, bool> placedVerticalText{false, false};
std::pair<bool, bool> placedVerticalIcon{false, false};
Point<float> shift{0.0f, 0.0f};
optional<size_t> horizontalTextIndex = symbolInstance.getDefaultHorizontalPlacedTextIndex();
if (horizontalTextIndex) {
const PlacedSymbol& placedSymbol = bucket.text.placedSymbols.at(*horizontalTextIndex);
const float fontSize = evaluateSizeForFeature(ctx.partiallyEvaluatedTextSize, placedSymbol);
const auto updatePreviousOrientationIfNotPlaced = [&](bool isPlaced) {
if (bucket.allowVerticalPlacement && !isPlaced && getPrevPlacement()) {
auto prevOrientation = getPrevPlacement()->placedOrientations.find(symbolInstance.crossTileID);
if (prevOrientation != getPrevPlacement()->placedOrientations.end()) {
placedOrientations[symbolInstance.crossTileID] = prevOrientation->second;
}
}
};
const auto placeTextForPlacementModes = [&](auto& placeHorizontalFn, auto& placeVerticalFn) {
if (bucket.allowVerticalPlacement && symbolInstance.writingModes & WritingModeType::Vertical) {
assert(!bucket.placementModes.empty());
for (auto& placementMode : bucket.placementModes) {
if (placementMode == TextWritingModeType::Vertical) {
placedVerticalText = placed = placeVerticalFn();
} else {
placed = placeHorizontalFn();
}
if (placed.first) {
break;
}
}
} else {
placed = placeHorizontalFn();
}
};
// Line or point label placement
if (variableTextAnchors.empty()) {
const auto placeFeature = [&](const CollisionFeature& collisionFeature,
style::TextWritingModeType orientation) {
textBoxes.clear();
auto placedFeature = collisionIndex.placeFeature(collisionFeature,
{},
posMatrix,
ctx.textLabelPlaneMatrix,
ctx.pixelRatio,
placedSymbol,
ctx.scale,
fontSize,
ctx.textAllowOverlap,
ctx.pitchTextWithMap,
showCollisionBoxes,
ctx.avoidEdges,
collisionGroup.second,
textBoxes);
if (placedFeature.first) {
placedOrientations.emplace(symbolInstance.crossTileID, orientation);
}
return placedFeature;
};
const auto placeHorizontal = [&] {
return placeFeature(symbolInstance.textCollisionFeature, style::TextWritingModeType::Horizontal);
};
const auto placeVertical = [&] {
if (bucket.allowVerticalPlacement && symbolInstance.verticalTextCollisionFeature) {
return placeFeature(*symbolInstance.verticalTextCollisionFeature,
style::TextWritingModeType::Vertical);
}
return std::pair<bool, bool>{false, false};
};
placeTextForPlacementModes(placeHorizontal, placeVertical);
updatePreviousOrientationIfNotPlaced(placed.first);
placeText = placed.first;
offscreen &= placed.second;
} else if (!symbolInstance.textCollisionFeature.alongLine &&
!symbolInstance.textCollisionFeature.boxes.empty()) {
// If this symbol was in the last placement, shift the previously used
// anchor to the front of the anchor list, only if the previous anchor
// is still in the anchor list.
if (getPrevPlacement()) {
auto prevOffset = getPrevPlacement()->variableOffsets.find(symbolInstance.crossTileID);
if (prevOffset != getPrevPlacement()->variableOffsets.end()) {
const auto prevAnchor = prevOffset->second.anchor;
auto found = std::find(variableTextAnchors.begin(), variableTextAnchors.end(), prevAnchor);
if (found != variableTextAnchors.begin() && found != variableTextAnchors.end()) {
std::vector<style::TextVariableAnchorType> filtered{prevAnchor};
if (!isTiltedView()) {
for (auto anchor : variableTextAnchors) {
if (anchor != prevAnchor) {
filtered.push_back(anchor);
}
}
}
variableTextAnchors = std::move(filtered);
}
}
}
const bool doVariableIconPlacement =
ctx.hasIconTextFit && !ctx.iconAllowOverlap && symbolInstance.placedIconIndex;
const auto placeFeatureForVariableAnchors = [&](const CollisionFeature& textCollisionFeature,
style::TextWritingModeType orientation,
const CollisionFeature& iconCollisionFeature) {
const CollisionBox& textBox = textCollisionFeature.boxes[0];
const float width = textBox.x2 - textBox.x1;
const float height = textBox.y2 - textBox.y1;
const float textBoxScale = symbolInstance.textBoxScale;
std::pair<bool, bool> placedFeature = {false, false};
const size_t anchorsSize = variableTextAnchors.size();
const size_t placementAttempts = ctx.textAllowOverlap ? anchorsSize * 2 : anchorsSize;
for (size_t i = 0u; i < placementAttempts; ++i) {
auto anchor = variableTextAnchors[i % anchorsSize];
const bool isFirstAnchor = (anchor == variableTextAnchors.front());
const bool allowOverlap = (i >= anchorsSize);
shift = calculateVariableLayoutOffset(anchor,
width,
height,
symbolInstance.variableTextOffset,
textBoxScale,
ctx.rotateTextWithMap,
ctx.pitchTextWithMap,
ctx.getTransformState().getBearing());
textBoxes.clear();
if (!isFirstAnchor &&
stickToFirstVariableAnchor(
symbolInstance.textCollisionFeature.boxes.front(), shift, posMatrix, ctx.pixelRatio)) {
continue;
}
placedFeature = collisionIndex.placeFeature(textCollisionFeature,
shift,
posMatrix,
mat4(),
ctx.pixelRatio,
placedSymbol,
ctx.scale,
fontSize,
allowOverlap,
ctx.pitchTextWithMap,
showCollisionBoxes,
ctx.avoidEdges,
collisionGroup.second,
textBoxes);
if (doVariableIconPlacement) {
auto placedIconFeature =
collisionIndex.placeFeature(iconCollisionFeature,
shift,
posMatrix,
ctx.iconLabelPlaneMatrix,
ctx.pixelRatio,
placedSymbol,
ctx.scale,
fontSize,
ctx.iconAllowOverlap,
ctx.pitchTextWithMap, // TODO: shall it be pitchIconWithMap?
showCollisionBoxes,
ctx.avoidEdges,
collisionGroup.second,
iconBoxes);
iconBoxes.clear();
if (!placedIconFeature.first) continue;
}
if (placedFeature.first) {
assert(symbolInstance.crossTileID != 0u);
optional<style::TextVariableAnchorType> prevAnchor;
// If this label was placed in the previous placement, record the anchor position
// to allow us to animate the transition
if (getPrevPlacement()) {
auto prevOffset = getPrevPlacement()->variableOffsets.find(symbolInstance.crossTileID);
auto prevPlacements = getPrevPlacement()->placements.find(symbolInstance.crossTileID);
if (prevOffset != getPrevPlacement()->variableOffsets.end() &&
prevPlacements != getPrevPlacement()->placements.end() && prevPlacements->second.text) {
// TODO: The prevAnchor seems to be unused, needs to be fixed.
prevAnchor = prevOffset->second.anchor;
}
}
variableOffsets.insert(std::make_pair(
symbolInstance.crossTileID,
VariableOffset{
symbolInstance.variableTextOffset, width, height, anchor, textBoxScale, prevAnchor}));
if (bucket.allowVerticalPlacement) {
placedOrientations.emplace(symbolInstance.crossTileID, orientation);
}
break;
}
}
return placedFeature;
};
const auto placeHorizontal = [&] {
return placeFeatureForVariableAnchors(symbolInstance.textCollisionFeature,
style::TextWritingModeType::Horizontal,
symbolInstance.iconCollisionFeature);
};
const auto placeVertical = [&] {
if (bucket.allowVerticalPlacement && !placed.first && symbolInstance.verticalTextCollisionFeature) {
return placeFeatureForVariableAnchors(*symbolInstance.verticalTextCollisionFeature,
style::TextWritingModeType::Vertical,
symbolInstance.verticalIconCollisionFeature
? *symbolInstance.verticalIconCollisionFeature
: symbolInstance.iconCollisionFeature);
}
return std::pair<bool, bool>{false, false};
};
placeTextForPlacementModes(placeHorizontal, placeVertical);
placeText = placed.first;
offscreen &= placed.second;
updatePreviousOrientationIfNotPlaced(placed.first);
// If we didn't get placed, we still need to copy our position from the last placement for
// fade animations
if (!placeText && getPrevPlacement()) {
auto prevOffset = getPrevPlacement()->variableOffsets.find(symbolInstance.crossTileID);
if (prevOffset != getPrevPlacement()->variableOffsets.end()) {
variableOffsets[symbolInstance.crossTileID] = prevOffset->second;
}
}
}
}
if (symbolInstance.placedIconIndex) {
if (!ctx.hasIconTextFit || !placeText || variableTextAnchors.empty()) {
shift = {0.0f, 0.0f};
}
const auto& iconBuffer = symbolInstance.hasSdfIcon() ? bucket.sdfIcon : bucket.icon;
const PlacedSymbol& placedSymbol = iconBuffer.placedSymbols.at(*symbolInstance.placedIconIndex);
const float fontSize = evaluateSizeForFeature(ctx.partiallyEvaluatedIconSize, placedSymbol);
const auto& placeIconFeature = [&](const CollisionFeature& collisionFeature) {
return collisionIndex.placeFeature(collisionFeature,
shift,
posMatrix,
ctx.iconLabelPlaneMatrix,
ctx.pixelRatio,
placedSymbol,
ctx.scale,
fontSize,
ctx.iconAllowOverlap,
ctx.pitchTextWithMap,
showCollisionBoxes,
ctx.avoidEdges,
collisionGroup.second,
iconBoxes);
};
std::pair<bool, bool> placedIcon = {false, false};
if (placedVerticalText.first && symbolInstance.verticalIconCollisionFeature) {
placedIcon = placedVerticalIcon = placeIconFeature(*symbolInstance.verticalIconCollisionFeature);
} else {
placedIcon = placeIconFeature(symbolInstance.iconCollisionFeature);
}
placeIcon = placedIcon.first;
offscreen &= placedIcon.second;
}
const bool iconWithoutText = !symbolInstance.hasText() || ctx.getLayout().get<TextOptional>();
const bool textWithoutIcon = !symbolInstance.hasIcon() || ctx.getLayout().get<IconOptional>();
// combine placements for icon and text
if (!iconWithoutText && !textWithoutIcon) {
placeText = placeIcon = placeText && placeIcon;
} else if (!textWithoutIcon) {
placeText = placeText && placeIcon;
} else if (!iconWithoutText) {
placeIcon = placeText && placeIcon;
}
if (placeText) {
if (placedVerticalText.first && symbolInstance.verticalTextCollisionFeature) {
collisionIndex.insertFeature(*symbolInstance.verticalTextCollisionFeature,
textBoxes,
ctx.getLayout().get<TextIgnorePlacement>(),
bucket.bucketInstanceId,
collisionGroup.first);
} else {
collisionIndex.insertFeature(symbolInstance.textCollisionFeature,
textBoxes,
ctx.getLayout().get<TextIgnorePlacement>(),
bucket.bucketInstanceId,
collisionGroup.first);
}
}
if (placeIcon) {
if (placedVerticalIcon.first && symbolInstance.verticalIconCollisionFeature) {
collisionIndex.insertFeature(*symbolInstance.verticalIconCollisionFeature,
iconBoxes,
ctx.getLayout().get<IconIgnorePlacement>(),
bucket.bucketInstanceId,
collisionGroup.first);
} else {
collisionIndex.insertFeature(symbolInstance.iconCollisionFeature,
iconBoxes,
ctx.getLayout().get<IconIgnorePlacement>(),
bucket.bucketInstanceId,
collisionGroup.first);
}
}
const bool hasIconCollisionCircleData = bucket.hasIconCollisionCircleData();
const bool hasTextCollisionCircleData = bucket.hasTextCollisionCircleData();
if (hasIconCollisionCircleData && symbolInstance.iconCollisionFeature.alongLine && !iconBoxes.empty()) {
collisionCircles[&symbolInstance.iconCollisionFeature] = iconBoxes;
}
if (hasTextCollisionCircleData && symbolInstance.textCollisionFeature.alongLine && !textBoxes.empty()) {
collisionCircles[&symbolInstance.textCollisionFeature] = textBoxes;
}
assert(symbolInstance.crossTileID != 0);
if (placements.find(symbolInstance.crossTileID) != placements.end()) {
// If there's a previous placement with this ID, it comes from a tile that's fading out
// Erase it so that the placement result from the non-fading tile supersedes it
placements.erase(symbolInstance.crossTileID);
}
auto pair = placements.emplace(
symbolInstance.crossTileID,
JointPlacement(
placeText || ctx.alwaysShowText, placeIcon || ctx.alwaysShowIcon, offscreen || bucket.justReloaded));
assert(pair.second);
newSymbolPlaced(symbolInstance, pair.first->second, ctx.placementType, textBoxes, iconBoxes);
seenCrossTileIDs.insert(symbolInstance.crossTileID);
}
namespace {
@@ -1363,21 +1419,21 @@ bool TilePlacement::stickToFirstVariableAnchor(const CollisionBox& box,
void TilePlacement::newSymbolPlaced(const SymbolInstance& symbol,
const JointPlacement& placement,
style::SymbolPlacementType placementType,
const std::vector<ProjectedCollisionBox>& textBoxes,
const std::vector<ProjectedCollisionBox>& iconBoxes) {
const std::vector<ProjectedCollisionBox>& textCollisionBoxes,
const std::vector<ProjectedCollisionBox>& iconCollisionBoxes) {
if (!collectData || placementType != style::SymbolPlacementType::Point) return;
optional<mapbox::geometry::box<float>> textCollisionBox;
if (!textBoxes.empty()) {
assert(textBoxes.size() == 1u);
auto& box = textBoxes.front();
if (!textCollisionBoxes.empty()) {
assert(textCollisionBoxes.size() == 1u);
auto& box = textCollisionBoxes.front();
assert(box.isBox());
textCollisionBox = box.box();
}
optional<mapbox::geometry::box<float>> iconCollisionBox;
if (!iconBoxes.empty()) {
assert(iconBoxes.size() == 1u);
auto& box = iconBoxes.front();
if (!iconCollisionBoxes.empty()) {
assert(iconCollisionBoxes.size() == 1u);
auto& box = iconCollisionBoxes.front();
assert(box.isBox());
iconCollisionBox = box.box();
}

View File

@@ -91,7 +91,7 @@ private:
};
class Placement;
class PlacementContext;
class PlacementController {
public:
PlacementController();
@@ -142,6 +142,9 @@ public:
protected:
friend SymbolBucket;
void placeSymbolBucket(const BucketPlacementData&, std::set<uint32_t>& seenCrossTileIDs);
void placeSymbol(const SymbolInstance& symbolInstance,
const PlacementContext&,
std::set<uint32_t>& seenCrossTileIDs);
void placeLayer(const RenderLayer&, std::set<uint32_t>&);
virtual void commit();
virtual void newSymbolPlaced(const SymbolInstance&,
@@ -191,6 +194,10 @@ protected:
CollisionGroups collisionGroups;
mutable optional<Immutable<Placement>> prevPlacement;
bool showCollisionBoxes = false;
// Cache being used by placeSymbol()
std::vector<ProjectedCollisionBox> textBoxes;
std::vector<ProjectedCollisionBox> iconBoxes;
// Used for debug purposes.
std::unordered_map<const CollisionFeature*, std::vector<ProjectedCollisionBox>> collisionCircles;
};