TEST(Buckets, SymbolBucket) { HeadlessBackend backend({ 512, 256 }); BackendScope scope { backend }; style::SymbolLayoutProperties::PossiblyEvaluated layout; bool sdfIcons = false; bool iconsNeedLinear = false; bool sortFeaturesByY = false; std::vector<SymbolInstance> symbolInstances; gl::Context context; SymbolBucket bucket { layout, {}, 16.0f, 1.0f, 0, sdfIcons, iconsNeedLinear, sortFeaturesByY, std::move(symbolInstances) }; ASSERT_FALSE(bucket.hasIconData()); ASSERT_FALSE(bucket.hasTextData()); ASSERT_FALSE(bucket.hasCollisionBoxData()); ASSERT_FALSE(bucket.hasData()); ASSERT_FALSE(bucket.needsUpload()); // SymbolBucket::addFeature() is a no-op. GeometryCollection point { { { 0, 0 } } }; bucket.addFeature(StubGeometryTileFeature { {}, FeatureType::Point, point, properties }, point); ASSERT_FALSE(bucket.hasData()); ASSERT_FALSE(bucket.needsUpload()); bucket.text.segments.emplace_back(0, 0); ASSERT_TRUE(bucket.hasTextData()); ASSERT_TRUE(bucket.hasData()); ASSERT_TRUE(bucket.needsUpload()); bucket.upload(context); ASSERT_FALSE(bucket.needsUpload()); }
void Painter::renderSymbol(SymbolBucket &bucket, std::shared_ptr<StyleLayer> layer_desc, const Tile::ID &id, const mat4 &matrix) { // Abort early. if (pass == RenderPass::Opaque) { return; } const SymbolProperties &properties = layer_desc->getProperties<SymbolProperties>(); glDisable(GL_STENCIL_TEST); if (bucket.hasTextData()) { GlyphAtlas &glyphAtlas = *map.getGlyphAtlas(); glyphAtlas.bind(); renderSDF(bucket, id, matrix, bucket.properties.text, properties.text, 24.0f, {{ float(glyphAtlas.width) / 4, float(glyphAtlas.height) / 4 }}, *sdfGlyphShader, &SymbolBucket::drawGlyphs); } if (bucket.hasIconData()) { bool sdf = bucket.sdfIcons; const float angleOffset = bucket.properties.icon.rotation_alignment == RotationAlignmentType::Map ? map.getState().getAngle() : 0; // If layerStyle.size > bucket.info.fontSize then labels may collide const float fontSize = properties.icon.size != 0 ? properties.icon.size : bucket.properties.icon.max_size; const float fontScale = fontSize / 1.0f; SpriteAtlas &spriteAtlas = *map.getSpriteAtlas(); spriteAtlas.bind(map.getState().isChanging() || bucket.properties.placement == PlacementType::Line || angleOffset != 0 || fontScale != 1 || sdf); std::array<float, 2> texsize = {{ float(spriteAtlas.getWidth()), float(spriteAtlas.getHeight()) } }; if (sdf) { renderSDF(bucket, id, matrix, bucket.properties.icon, properties.icon, 1.0f, texsize, *sdfIconShader, &SymbolBucket::drawIcons); } else { mat4 vtxMatrix = translatedMatrix(matrix, properties.icon.translate, id, properties.icon.translate_anchor); mat4 exMatrix; matrix::copy(exMatrix, projMatrix); if (angleOffset) { matrix::rotate_z(exMatrix, exMatrix, angleOffset); } matrix::scale(exMatrix, exMatrix, fontScale, fontScale, 1.0f); useProgram(iconShader->program); iconShader->u_matrix = vtxMatrix; iconShader->u_exmatrix = exMatrix; iconShader->u_texsize = texsize; // Convert the -pi..pi to an int8 range. const float angle = std::round(map.getState().getAngle() / M_PI * 128); // adjust min/max zooms for variable font sies float zoomAdjust = std::log(fontSize / bucket.properties.icon.max_size) / std::log(2); iconShader->u_angle = (int32_t)(angle + 256) % 256; iconShader->u_flip = bucket.properties.placement == PlacementType::Line ? 1 : 0; iconShader->u_zoom = (map.getState().getNormalizedZoom() - zoomAdjust) * 10; // current zoom level iconShader->u_fadedist = 0 * 10; iconShader->u_minfadezoom = map.getState().getNormalizedZoom() * 10; iconShader->u_maxfadezoom = map.getState().getNormalizedZoom() * 10; iconShader->u_fadezoom = map.getState().getNormalizedZoom() * 10; iconShader->u_opacity = properties.icon.opacity; depthRange(strata, 1.0f); bucket.drawIcons(*iconShader); } } glEnable(GL_STENCIL_TEST); }
void Painter::renderSymbol(SymbolBucket &bucket, const StyleLayer &layer_desc, const TileID &id, const mat4 &matrix) { // Abort early. if (pass == RenderPass::Opaque) { return; } const auto &properties = layer_desc.getProperties<SymbolProperties>(); const auto &layout = bucket.layout; config.depthTest = true; config.depthMask = GL_FALSE; if (bucket.hasCollisionBoxData() && ( (bucket.hasIconData() && properties.icon.opacity) || (bucket.hasTextData() && properties.text.opacity))) { config.stencilTest = true; useProgram(collisionBoxShader->program); collisionBoxShader->u_matrix = matrix; collisionBoxShader->u_scale = std::pow(2, state.getNormalizedZoom() - id.z); collisionBoxShader->u_zoom = state.getNormalizedZoom() * 10; collisionBoxShader->u_maxzoom = (id.z + 1) * 10; lineWidth(3.0f); config.depthRange = { strata, 1.0f }; bucket.drawCollisionBoxes(*collisionBoxShader); } // TODO remove the `|| true` when #1673 is implemented const bool drawAcrossEdges = !(layout.text.allow_overlap || layout.icon.allow_overlap || layout.text.ignore_placement || layout.icon.ignore_placement) || true; // Disable the stencil test so that labels aren't clipped to tile boundaries. // // Layers with features that may be drawn overlapping aren't clipped. These // layers are sorted in the y direction, and to draw the correct ordering near // tile edges the icons are included in both tiles and clipped when drawing. config.stencilTest = drawAcrossEdges ? false : true; if (bucket.hasIconData()) { bool sdf = bucket.sdfIcons; const float angleOffset = layout.icon.rotation_alignment == RotationAlignmentType::Map ? state.getAngle() : 0; // If layerStyle.size > bucket.info.fontSize then labels may collide const float fontSize = properties.icon.size != 0 ? properties.icon.size : layout.icon.max_size; const float fontScale = fontSize / 1.0f; spriteAtlas->bind(state.isChanging() || layout.placement == PlacementType::Line || angleOffset != 0 || fontScale != 1 || sdf); if (sdf) { renderSDF(bucket, id, matrix, layout.icon, properties.icon, 1.0f, {{ float(spriteAtlas->getWidth()) / 4.0f, float(spriteAtlas->getHeight()) / 4.0f }}, *sdfIconShader, &SymbolBucket::drawIcons); } else { mat4 vtxMatrix = translatedMatrix(matrix, properties.icon.translate, id, properties.icon.translate_anchor); mat4 exMatrix; matrix::copy(exMatrix, projMatrix); if (angleOffset) { matrix::rotate_z(exMatrix, exMatrix, angleOffset); } matrix::scale(exMatrix, exMatrix, fontScale, fontScale, 1.0f); useProgram(iconShader->program); iconShader->u_matrix = vtxMatrix; iconShader->u_exmatrix = exMatrix; iconShader->u_texsize = {{ float(spriteAtlas->getWidth()) / 4.0f, float(spriteAtlas->getHeight()) / 4.0f }}; // adjust min/max zooms for variable font sies float zoomAdjust = std::log(fontSize / layout.icon.max_size) / std::log(2); iconShader->u_zoom = (state.getNormalizedZoom() - zoomAdjust) * 10; // current zoom level iconShader->u_fadedist = 0 * 10; iconShader->u_minfadezoom = state.getNormalizedZoom() * 10; iconShader->u_maxfadezoom = state.getNormalizedZoom() * 10; iconShader->u_fadezoom = state.getNormalizedZoom() * 10; iconShader->u_opacity = properties.icon.opacity; config.depthRange = { strata, 1.0f }; bucket.drawIcons(*iconShader); } } if (bucket.hasTextData()) { glyphAtlas->bind(); renderSDF(bucket, id, matrix, layout.text, properties.text, 24.0f, {{ float(glyphAtlas->width) / 4, float(glyphAtlas->height) / 4 }}, *sdfGlyphShader, &SymbolBucket::drawGlyphs); } }
void Painter::renderSymbol(SymbolBucket& bucket, const SymbolLayer& layer, const TileID& id, const mat4& matrix) { // Abort early. if (pass == RenderPass::Opaque) { return; } const auto& properties = layer.paint; const auto& layout = bucket.layout; config.depthMask = GL_FALSE; if (bucket.hasCollisionBoxData()) { config.stencilOp.reset(); config.stencilTest = GL_TRUE; config.program = collisionBoxShader->program; collisionBoxShader->u_matrix = matrix; collisionBoxShader->u_scale = std::pow(2, state.getNormalizedZoom() - id.z); collisionBoxShader->u_zoom = state.getNormalizedZoom() * 10; collisionBoxShader->u_maxzoom = (id.z + 1) * 10; config.lineWidth = 1.0f; setDepthSublayer(0); bucket.drawCollisionBoxes(*collisionBoxShader); } // TODO remove the `true ||` when #1673 is implemented const bool drawAcrossEdges = true || !(layout.text.allowOverlap || layout.icon.allowOverlap || layout.text.ignorePlacement || layout.icon.ignorePlacement); // Disable the stencil test so that labels aren't clipped to tile boundaries. // // Layers with features that may be drawn overlapping aren't clipped. These // layers are sorted in the y direction, and to draw the correct ordering near // tile edges the icons are included in both tiles and clipped when drawing. if (drawAcrossEdges) { config.stencilTest = GL_FALSE; } else { config.stencilOp.reset(); config.stencilTest = GL_TRUE; } if (bucket.hasIconData()) { if (layout.icon.rotationAlignment == RotationAlignmentType::Map) { config.depthFunc.reset(); config.depthTest = GL_TRUE; } else { config.depthTest = GL_FALSE; } bool sdf = bucket.sdfIcons; const float angleOffset = layout.icon.rotationAlignment == RotationAlignmentType::Map ? state.getAngle() : 0; const float fontSize = properties.icon.size; const float fontScale = fontSize / 1.0f; SpriteAtlas* activeSpriteAtlas = layer.spriteAtlas; activeSpriteAtlas->bind(state.isChanging() || layout.placement == PlacementType::Line || angleOffset != 0 || fontScale != 1 || sdf || state.getPitch() != 0); if (sdf) { renderSDF(bucket, id, matrix, layout.icon, properties.icon, 1.0f, {{ float(activeSpriteAtlas->getWidth()) / 4.0f, float(activeSpriteAtlas->getHeight()) / 4.0f }}, *sdfIconShader, &SymbolBucket::drawIcons); } else { mat4 vtxMatrix = translatedMatrix(matrix, properties.icon.translate, id, properties.icon.translateAnchor); bool skewed = layout.icon.rotationAlignment == RotationAlignmentType::Map; mat4 exMatrix; float s; if (skewed) { matrix::identity(exMatrix); s = 4096.0f / util::tileSize / id.overscaling / std::pow(2, state.getZoom() - id.z); } else { exMatrix = extrudeMatrix; s = state.getAltitude(); } matrix::scale(exMatrix, exMatrix, s, s, 1); matrix::scale(exMatrix, exMatrix, fontScale, fontScale, 1.0f); // calculate how much longer the real world distance is at the top of the screen // than at the middle of the screen. float topedgelength = std::sqrt(std::pow(state.getHeight(), 2) / 4.0f * (1.0f + std::pow(state.getAltitude(), 2))); float x = state.getHeight() / 2.0f * std::tan(state.getPitch()); float extra = (topedgelength + x) / topedgelength - 1; config.program = iconShader->program; iconShader->u_matrix = vtxMatrix; iconShader->u_exmatrix = exMatrix; iconShader->u_texsize = {{ float(spriteAtlas->getWidth()) / 4.0f, float(spriteAtlas->getHeight()) / 4.0f }}; iconShader->u_skewed = skewed; iconShader->u_extra = extra; // adjust min/max zooms for variable font sies float zoomAdjust = std::log(fontSize / layout.icon.size) / std::log(2); iconShader->u_zoom = (state.getNormalizedZoom() - zoomAdjust) * 10; // current zoom level iconShader->u_fadedist = 0 * 10; iconShader->u_minfadezoom = state.getNormalizedZoom() * 10; iconShader->u_maxfadezoom = state.getNormalizedZoom() * 10; iconShader->u_fadezoom = state.getNormalizedZoom() * 10; iconShader->u_opacity = properties.icon.opacity; setDepthSublayer(0); bucket.drawIcons(*iconShader); } } if (bucket.hasTextData()) { if (layout.text.rotationAlignment == RotationAlignmentType::Map) { config.depthFunc.reset(); config.depthTest = GL_TRUE; } else { config.depthTest = GL_FALSE; } glyphAtlas->bind(); renderSDF(bucket, id, matrix, layout.text, properties.text, 24.0f, {{ float(glyphAtlas->width) / 4, float(glyphAtlas->height) / 4 }}, *sdfGlyphShader, &SymbolBucket::drawGlyphs); } }
void Painter::renderSymbol(PaintParameters& parameters, SymbolBucket& bucket, const SymbolLayer& layer, const RenderTile& tile) { // Abort early. if (pass == RenderPass::Opaque) { return; } const auto& paint = layer.impl->paint; const auto& layout = bucket.layout; config.depthMask = GL_FALSE; // TODO remove the `true ||` when #1673 is implemented const bool drawAcrossEdges = (frame.mapMode == MapMode::Continuous) && (true || !(layout.textAllowOverlap || layout.iconAllowOverlap || layout.textIgnorePlacement || layout.iconIgnorePlacement)); // Disable the stencil test so that labels aren't clipped to tile boundaries. // // Layers with features that may be drawn overlapping aren't clipped. These // layers are sorted in the y direction, and to draw the correct ordering near // tile edges the icons are included in both tiles and clipped when drawing. if (drawAcrossEdges) { config.stencilTest = GL_FALSE; } else { config.stencilOp.reset(); config.stencilTest = GL_TRUE; } if (bucket.hasIconData()) { if (layout.iconRotationAlignment == AlignmentType::Map) { config.depthFunc.reset(); config.depthTest = GL_TRUE; } else { config.depthTest = GL_FALSE; } bool sdf = bucket.sdfIcons; const float angleOffset = layout.iconRotationAlignment == AlignmentType::Map ? state.getAngle() : 0; const float fontSize = layer.impl->iconSize; const float fontScale = fontSize / 1.0f; SpriteAtlas* activeSpriteAtlas = layer.impl->spriteAtlas; const bool iconScaled = fontScale != 1 || frame.pixelRatio != activeSpriteAtlas->getPixelRatio() || bucket.iconsNeedLinear; const bool iconTransformed = layout.iconRotationAlignment == AlignmentType::Map || angleOffset != 0 || state.getPitch() != 0; activeSpriteAtlas->bind(sdf || state.isChanging() || iconScaled || iconTransformed, store, config, 0); if (sdf) { renderSDF(bucket, tile, 1.0f, {{ float(activeSpriteAtlas->getWidth()) / 4.0f, float(activeSpriteAtlas->getHeight()) / 4.0f }}, parameters.shaders.sdfIcon, &SymbolBucket::drawIcons, layout.iconRotationAlignment, // icon-pitch-alignment is not yet implemented // and we simply inherit the rotation alignment layout.iconRotationAlignment, layout.iconSize, paint.iconOpacity, paint.iconColor, paint.iconHaloColor, paint.iconHaloWidth, paint.iconHaloBlur, paint.iconTranslate, paint.iconTranslateAnchor, layer.impl->iconSize); } else { mat4 vtxMatrix = tile.translatedMatrix(paint.iconTranslate, paint.iconTranslateAnchor, state); std::array<float, 2> extrudeScale; const bool alignedWithMap = layout.iconRotationAlignment == AlignmentType::Map; if (alignedWithMap) { extrudeScale.fill(tile.id.pixelsToTileUnits(1, state.getZoom()) * fontScale); } else { extrudeScale = {{ pixelsToGLUnits[0] * fontScale * state.getAltitude(), pixelsToGLUnits[1] * fontScale * state.getAltitude() }}; } auto& iconShader = parameters.shaders.icon; config.program = iconShader.getID(); iconShader.u_matrix = vtxMatrix; iconShader.u_extrude_scale = extrudeScale; iconShader.u_texsize = {{ float(activeSpriteAtlas->getWidth()) / 4.0f, float(activeSpriteAtlas->getHeight()) / 4.0f }}; iconShader.u_rotate_with_map = alignedWithMap; iconShader.u_texture = 0; // adjust min/max zooms for variable font sies float zoomAdjust = std::log(fontSize / layout.iconSize) / std::log(2); iconShader.u_zoom = (state.getZoom() - zoomAdjust) * 10; // current zoom level iconShader.u_opacity = paint.iconOpacity; frameHistory.bind(store, config, 1); iconShader.u_fadetexture = 1; setDepthSublayer(0); bucket.drawIcons(iconShader, store, isOverdraw()); } } if (bucket.hasTextData()) { if (layout.textRotationAlignment == AlignmentType::Map) { config.depthFunc.reset(); config.depthTest = GL_TRUE; } else { config.depthTest = GL_FALSE; } glyphAtlas->bind(store, config, 0); renderSDF(bucket, tile, 24.0f, {{ float(glyphAtlas->width) / 4, float(glyphAtlas->height) / 4 }}, parameters.shaders.sdfGlyph, &SymbolBucket::drawGlyphs, layout.textRotationAlignment, layout.textPitchAlignment, layout.textSize, paint.textOpacity, paint.textColor, paint.textHaloColor, paint.textHaloWidth, paint.textHaloBlur, paint.textTranslate, paint.textTranslateAnchor, layer.impl->textSize); } if (bucket.hasCollisionBoxData()) { config.stencilOp.reset(); config.stencilTest = GL_TRUE; auto& collisionBoxShader = shaders->collisionBox; config.program = collisionBoxShader.getID(); collisionBoxShader.u_matrix = tile.matrix; // TODO: This was the overscaled z instead of the canonical z. collisionBoxShader.u_scale = std::pow(2, state.getZoom() - tile.id.canonical.z); collisionBoxShader.u_zoom = state.getZoom() * 10; collisionBoxShader.u_maxzoom = (tile.id.canonical.z + 1) * 10; config.lineWidth = 1.0f; setDepthSublayer(0); bucket.drawCollisionBoxes(collisionBoxShader, store); } }
void Placement::updateBucketOpacities(SymbolBucket& bucket, std::set<uint32_t>& seenCrossTileIDs) { if (bucket.hasTextData()) bucket.text.opacityVertices.clear(); if (bucket.hasIconData()) bucket.icon.opacityVertices.clear(); if (bucket.hasCollisionBoxData()) bucket.collisionBox.dynamicVertices.clear(); if (bucket.hasCollisionCircleData()) bucket.collisionCircle.dynamicVertices.clear(); JointOpacityState duplicateOpacityState(false, false, true); const bool textAllowOverlap = bucket.layout.get<style::TextAllowOverlap>(); const bool iconAllowOverlap = bucket.layout.get<style::IconAllowOverlap>(); // If allow-overlap is true, we can show symbols before placement runs on them // But we have to wait for placement if we potentially depend on a paired icon/text // with allow-overlap: false. // See https://github.com/mapbox/mapbox-gl-native/issues/12483 JointOpacityState defaultOpacityState( textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || bucket.layout.get<style::IconOptional>()), iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || bucket.layout.get<style::TextOptional>()), true); for (SymbolInstance& symbolInstance : bucket.symbolInstances) { bool isDuplicate = seenCrossTileIDs.count(symbolInstance.crossTileID) > 0; auto it = opacities.find(symbolInstance.crossTileID); auto opacityState = defaultOpacityState; if (isDuplicate) { opacityState = duplicateOpacityState; } else if (it != opacities.end()) { opacityState = it->second; } if (it == opacities.end()) { opacities.emplace(symbolInstance.crossTileID, defaultOpacityState); } seenCrossTileIDs.insert(symbolInstance.crossTileID); if (symbolInstance.hasText) { auto opacityVertex = SymbolOpacityAttributes::vertex(opacityState.text.placed, opacityState.text.opacity); for (size_t i = 0; i < symbolInstance.horizontalGlyphQuads.size() * 4; i++) { bucket.text.opacityVertices.emplace_back(opacityVertex); } for (size_t i = 0; i < symbolInstance.verticalGlyphQuads.size() * 4; i++) { bucket.text.opacityVertices.emplace_back(opacityVertex); } if (symbolInstance.placedTextIndex) { bucket.text.placedSymbols[*symbolInstance.placedTextIndex].hidden = opacityState.isHidden(); } if (symbolInstance.placedVerticalTextIndex) { bucket.text.placedSymbols[*symbolInstance.placedVerticalTextIndex].hidden = opacityState.isHidden(); } } if (symbolInstance.hasIcon) { auto opacityVertex = SymbolOpacityAttributes::vertex(opacityState.icon.placed, opacityState.icon.opacity); if (symbolInstance.iconQuad) { bucket.icon.opacityVertices.emplace_back(opacityVertex); bucket.icon.opacityVertices.emplace_back(opacityVertex); bucket.icon.opacityVertices.emplace_back(opacityVertex); bucket.icon.opacityVertices.emplace_back(opacityVertex); } if (symbolInstance.placedIconIndex) { bucket.icon.placedSymbols[*symbolInstance.placedIconIndex].hidden = opacityState.isHidden(); } } auto updateCollisionBox = [&](const auto& feature, const bool placed) { if (feature.alongLine) { return; } auto dynamicVertex = CollisionBoxDynamicAttributes::vertex(placed, false); for (size_t i = 0; i < feature.boxes.size() * 4; i++) { bucket.collisionBox.dynamicVertices.emplace_back(dynamicVertex); } }; auto updateCollisionCircles = [&](const auto& feature, const bool placed) { if (!feature.alongLine) { return; } for (const CollisionBox& box : feature.boxes) { auto dynamicVertex = CollisionBoxDynamicAttributes::vertex(placed, !box.used); bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex); bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex); bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex); bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex); } }; if (bucket.hasCollisionBoxData()) { updateCollisionBox(symbolInstance.textCollisionFeature, opacityState.text.placed); updateCollisionBox(symbolInstance.iconCollisionFeature, opacityState.icon.placed); } if (bucket.hasCollisionCircleData()) { updateCollisionCircles(symbolInstance.textCollisionFeature, opacityState.text.placed); updateCollisionCircles(symbolInstance.iconCollisionFeature, opacityState.icon.placed); } } bucket.updateOpacity(); bucket.sortFeatures(state.getAngle()); auto retainedData = retainedQueryData.find(bucket.bucketInstanceId); if (retainedData != retainedQueryData.end()) { retainedData->second.featureSortOrder = bucket.featureSortOrder; } }
void Placement::placeLayerBucket( SymbolBucket& bucket, const mat4& posMatrix, const mat4& textLabelPlaneMatrix, const mat4& iconLabelPlaneMatrix, const float scale, const float textPixelRatio, const bool showCollisionBoxes, std::unordered_set<uint32_t>& seenCrossTileIDs, const bool holdingForFade, const CollisionGroups::CollisionGroup& collisionGroup) { auto partiallyEvaluatedTextSize = bucket.textSizeBinder->evaluateForZoom(state.getZoom()); auto partiallyEvaluatedIconSize = bucket.iconSizeBinder->evaluateForZoom(state.getZoom()); optional<CollisionTileBoundaries> avoidEdges; if (mapMode == MapMode::Tile && (bucket.layout.get<style::SymbolAvoidEdges>() || bucket.layout.get<style::SymbolPlacement>() == style::SymbolPlacementType::Line)) { avoidEdges = collisionIndex.projectTileBoundaries(posMatrix); } const bool textAllowOverlap = bucket.layout.get<style::TextAllowOverlap>(); const bool iconAllowOverlap = bucket.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.layout.get<style::IconOptional>()); const bool alwaysShowIcon = iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || bucket.layout.get<style::TextOptional>()); for (auto& symbolInstance : bucket.symbolInstances) { if (seenCrossTileIDs.count(symbolInstance.crossTileID) == 0) { if (holdingForFade) { // 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)); continue; } bool placeText = false; bool placeIcon = false; bool offscreen = true; if (symbolInstance.placedTextIndex) { PlacedSymbol& placedSymbol = bucket.text.placedSymbols.at(*symbolInstance.placedTextIndex); const float fontSize = evaluateSizeForFeature(partiallyEvaluatedTextSize, placedSymbol); auto placed = collisionIndex.placeFeature(symbolInstance.textCollisionFeature, posMatrix, textLabelPlaneMatrix, textPixelRatio, placedSymbol, scale, fontSize, bucket.layout.get<style::TextAllowOverlap>(), bucket.layout.get<style::TextPitchAlignment>() == style::AlignmentType::Map, showCollisionBoxes, avoidEdges, collisionGroup.second); placeText = placed.first; offscreen &= placed.second; } if (symbolInstance.placedIconIndex) { PlacedSymbol& placedSymbol = bucket.icon.placedSymbols.at(*symbolInstance.placedIconIndex); const float fontSize = evaluateSizeForFeature(partiallyEvaluatedIconSize, placedSymbol); auto placed = collisionIndex.placeFeature(symbolInstance.iconCollisionFeature, posMatrix, iconLabelPlaneMatrix, textPixelRatio, placedSymbol, scale, fontSize, bucket.layout.get<style::IconAllowOverlap>(), bucket.layout.get<style::IconPitchAlignment>() == style::AlignmentType::Map, showCollisionBoxes, avoidEdges, collisionGroup.second); placeIcon = placed.first; offscreen &= placed.second; } const bool iconWithoutText = !symbolInstance.hasText || bucket.layout.get<style::TextOptional>(); const bool textWithoutIcon = !symbolInstance.hasIcon || bucket.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) { collisionIndex.insertFeature(symbolInstance.textCollisionFeature, bucket.layout.get<style::TextIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first); } if (placeIcon) { collisionIndex.insertFeature(symbolInstance.iconCollisionFeature, bucket.layout.get<style::IconIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first); } 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); } placements.emplace(symbolInstance.crossTileID, JointPlacement(placeText || alwaysShowText, placeIcon || alwaysShowIcon, offscreen || bucket.justReloaded)); seenCrossTileIDs.insert(symbolInstance.crossTileID); } } bucket.justReloaded = false; }