#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace mbgl { SymbolInstance::SymbolInstance(Anchor& anchor, const std::vector& line, const Shaping& shapedText, const PositionedIcon& shapedIcon, const SymbolLayoutProperties& layout, const bool addToBuffers, const uint32_t index_, const float textBoxScale, const float textPadding, const float textAlongLine, const float iconBoxScale, const float iconPadding, const float iconAlongLine, const GlyphPositions& face) : x(anchor.x), y(anchor.y), index(index_), hasText(shapedText), hasIcon(shapedIcon), // Create the quads used for rendering the glyphs. glyphQuads(addToBuffers && shapedText ? getGlyphQuads(anchor, shapedText, textBoxScale, line, layout, textAlongLine, face) : SymbolQuads()), // Create the quad used for rendering the icon. iconQuads(addToBuffers && shapedIcon ? getIconQuads(anchor, shapedIcon, line, layout, iconAlongLine) : SymbolQuads()), // Create the collision features that will be used to check whether this symbol instance can be placed textCollisionFeature(line, anchor, shapedText, textBoxScale, textPadding, textAlongLine), iconCollisionFeature(line, anchor, shapedIcon, iconBoxScale, iconPadding, iconAlongLine) {}; SymbolBucket::SymbolBucket(float overscaling_, float zoom_, const MapMode mode_) : overscaling(overscaling_), zoom(zoom_), tileSize(512 * overscaling_), tilePixelRatio(util::EXTENT / tileSize), mode(mode_) { } SymbolBucket::~SymbolBucket() { // Do not remove. header file only contains forward definitions to unique pointers. } void SymbolBucket::upload(gl::GLObjectStore& glObjectStore) { if (hasTextData()) { renderData->text.vertices.upload(glObjectStore); renderData->text.triangles.upload(glObjectStore); } if (hasIconData()) { renderData->icon.vertices.upload(glObjectStore); renderData->icon.triangles.upload(glObjectStore); } uploaded = true; } void SymbolBucket::render(Painter& painter, const StyleLayer& layer, const TileID& id, const mat4& matrix) { painter.renderSymbol(*this, *layer.as(), id, matrix); } bool SymbolBucket::hasData() const { return hasTextData() || hasIconData() || !symbolInstances.empty(); } bool SymbolBucket::hasTextData() const { return renderData && !renderData->text.groups.empty(); } bool SymbolBucket::hasIconData() const { return renderData && !renderData->icon.groups.empty(); } bool SymbolBucket::hasCollisionBoxData() const { return renderData && !renderData->collisionBox.groups.empty(); } void SymbolBucket::parseFeatures(const GeometryTileLayer& layer, const FilterExpression& filter) { const bool has_text = !layout.text.field.value.empty() && !layout.text.font.value.empty(); const bool has_icon = !layout.icon.image.value.empty(); if (!has_text && !has_icon) { return; } // Determine and load glyph ranges const GLsizei featureCount = static_cast(layer.featureCount()); for (GLsizei i = 0; i < featureCount; i++) { auto feature = layer.getFeature(i); GeometryTileFeatureExtractor extractor(*feature); if (!evaluate(filter, extractor)) continue; SymbolFeature ft; auto getValue = [&feature](const std::string& key) -> std::string { auto value = feature->getValue(key); return value ? toString(*value) : std::string(); }; if (has_text) { std::string u8string = util::replaceTokens(layout.text.field, getValue); if (layout.text.transform == TextTransformType::Uppercase) { u8string = platform::uppercase(u8string); } else if (layout.text.transform == TextTransformType::Lowercase) { u8string = platform::lowercase(u8string); } ft.label = util::utf8_to_utf32::convert(u8string); if (!ft.label.empty()) { // Loop through all characters of this text and collect unique codepoints. for (char32_t chr : ft.label) { ranges.insert(getGlyphRange(chr)); } } } if (has_icon) { ft.sprite = util::replaceTokens(layout.icon.image, getValue); } if (ft.label.length() || ft.sprite.length()) { auto &multiline = ft.geometry; GeometryCollection geometryCollection = getGeometries(*feature); for (auto& line : geometryCollection) { multiline.emplace_back(); for (auto& point : line) { multiline.back().emplace_back(point.x, point.y); } } features.push_back(std::move(ft)); } } if (layout.placement == PlacementType::Line) { util::mergeLines(features); } } bool SymbolBucket::needsDependencies(GlyphStore& glyphStore, SpriteStore& spriteStore) { if (!layout.text.field.value.empty() && !layout.text.font.value.empty() && !glyphStore.hasGlyphRanges(layout.text.font, ranges)) { return true; } if (!layout.icon.image.value.empty() && !spriteStore.isLoaded()) { return true; } return false; } void SymbolBucket::addFeatures(uintptr_t tileUID, SpriteAtlas& spriteAtlas, GlyphAtlas& glyphAtlas, GlyphStore& glyphStore) { float horizontalAlign = 0.5; float verticalAlign = 0.5; switch (layout.text.anchor) { case TextAnchorType::Top: case TextAnchorType::Bottom: case TextAnchorType::Center: break; case TextAnchorType::Right: case TextAnchorType::TopRight: case TextAnchorType::BottomRight: horizontalAlign = 1; break; case TextAnchorType::Left: case TextAnchorType::TopLeft: case TextAnchorType::BottomLeft: horizontalAlign = 0; break; } switch (layout.text.anchor) { case TextAnchorType::Left: case TextAnchorType::Right: case TextAnchorType::Center: break; case TextAnchorType::Bottom: case TextAnchorType::BottomLeft: case TextAnchorType::BottomRight: verticalAlign = 1; break; case TextAnchorType::Top: case TextAnchorType::TopLeft: case TextAnchorType::TopRight: verticalAlign = 0; break; } const float justify = layout.text.justify == TextJustifyType::Right ? 1 : layout.text.justify == TextJustifyType::Left ? 0 : 0.5; auto fontStack = glyphStore.getFontStack(layout.text.font); for (const auto& feature : features) { if (feature.geometry.empty()) continue; Shaping shapedText; PositionedIcon shapedIcon; GlyphPositions face; // if feature has text, shape the text if (feature.label.length()) { shapedText = fontStack->getShaping( /* string */ feature.label, /* maxWidth: ems */ layout.placement != PlacementType::Line ? layout.text.maxWidth * 24 : 0, /* lineHeight: ems */ layout.text.lineHeight * 24, /* horizontalAlign */ horizontalAlign, /* verticalAlign */ verticalAlign, /* justify */ justify, /* spacing: ems */ layout.text.letterSpacing * 24, /* translate */ vec2(layout.text.offset.value[0], layout.text.offset.value[1])); // Add the glyphs we need for this label to the glyph atlas. if (shapedText) { glyphAtlas.addGlyphs(tileUID, feature.label, layout.text.font, **fontStack, face); } } // if feature has icon, get sprite atlas position if (feature.sprite.length()) { auto image = spriteAtlas.getImage(feature.sprite, false); if (image) { shapedIcon = shapeIcon(*image, layout); assert((*image).spriteImage); if ((*image).spriteImage->sdf) { sdfIcons = true; } if ((*image).relativePixelRatio != 1.0f) { iconsNeedLinear = true; } } } // if either shapedText or icon position is present, add the feature if (shapedText || shapedIcon) { addFeature(feature.geometry, shapedText, shapedIcon, face); } } features.clear(); } void SymbolBucket::addFeature(const std::vector> &lines, const Shaping &shapedText, const PositionedIcon &shapedIcon, const GlyphPositions &face) { const float minScale = 0.5f; const float glyphSize = 24.0f; const float fontScale = layout.text.size / glyphSize; const float textBoxScale = tilePixelRatio * fontScale; const float textMaxBoxScale = tilePixelRatio * layout.textMaxSize / glyphSize; const float iconBoxScale = tilePixelRatio * layout.icon.size; const float symbolSpacing = tilePixelRatio * layout.spacing; const bool avoidEdges = layout.avoidEdges && layout.placement != PlacementType::Line; const float textPadding = layout.text.padding * tilePixelRatio; const float iconPadding = layout.icon.padding * tilePixelRatio; const float textMaxAngle = layout.text.maxAngle * M_PI / 180; const bool textAlongLine = layout.text.rotationAlignment == RotationAlignmentType::Map && layout.placement == PlacementType::Line; const bool iconAlongLine = layout.icon.rotationAlignment == RotationAlignmentType::Map && layout.placement == PlacementType::Line; const bool mayOverlap = layout.text.allowOverlap || layout.icon.allowOverlap || layout.text.ignorePlacement || layout.icon.ignorePlacement; const bool isLine = layout.placement == PlacementType::Line; const float textRepeatDistance = symbolSpacing / 2; auto& clippedLines = isLine ? util::clipLines(lines, 0, 0, util::EXTENT, util::EXTENT) : lines; for (const auto& line : clippedLines) { if (line.empty()) continue; // Calculate the anchor points around which you want to place labels Anchors anchors = isLine ? getAnchors(line, symbolSpacing, textMaxAngle, shapedText.left, shapedText.right, shapedIcon.left, shapedIcon.right, glyphSize, textMaxBoxScale, overscaling) : Anchors({ Anchor(float(line[0].x), float(line[0].y), 0, minScale) }); // For each potential label, create the placement features used to check for collisions, and the quads use for rendering. for (Anchor &anchor : anchors) { if (shapedText && isLine) { if (anchorIsTooClose(shapedText.text, textRepeatDistance, anchor)) { continue; } } const bool inside = !(anchor.x < 0 || anchor.x > util::EXTENT || anchor.y < 0 || anchor.y > util::EXTENT); if (avoidEdges && !inside) continue; // Normally symbol layers are drawn across tile boundaries. Only symbols // with their anchors within the tile boundaries are added to the buffers // to prevent symbols from being drawn twice. // // Symbols in layers with overlap are sorted in the y direction so that // symbols lower on the canvas are drawn on top of symbols near the top. // To preserve this order across tile boundaries these symbols can't // be drawn across tile boundaries. Instead they need to be included in // the buffers for both tiles and clipped to tile boundaries at draw time. // // TODO remove the `&& false` when is #1673 implemented const bool addToBuffers = (mode == MapMode::Still) || inside || (mayOverlap && false); symbolInstances.emplace_back(anchor, line, shapedText, shapedIcon, layout, addToBuffers, symbolInstances.size(), textBoxScale, textPadding, textAlongLine, iconBoxScale, iconPadding, iconAlongLine, face); } } } bool SymbolBucket::anchorIsTooClose(const std::u32string &text, const float repeatDistance, Anchor &anchor) { if (compareText.find(text) == compareText.end()) { compareText.emplace(text, Anchors()); } else { auto otherAnchors = compareText.find(text)->second; for (Anchor &otherAnchor : otherAnchors) { if (util::dist(anchor, otherAnchor) < repeatDistance) { return true; } } } compareText[text].push_back(anchor); return false; } void SymbolBucket::placeFeatures(CollisionTile& collisionTile) { renderDataInProgress = std::make_unique(); // Calculate which labels can be shown and when they can be shown and // create the bufers used for rendering. const bool textAlongLine = layout.text.rotationAlignment == RotationAlignmentType::Map && layout.placement == PlacementType::Line; const bool iconAlongLine = layout.icon.rotationAlignment == RotationAlignmentType::Map && layout.placement == PlacementType::Line; const bool mayOverlap = layout.text.allowOverlap || layout.icon.allowOverlap || layout.text.ignorePlacement || layout.icon.ignorePlacement; // Sort symbols by their y position on the canvas so that they lower symbols // are drawn on top of higher symbols. // Don't sort symbols that won't overlap because it isn't necessary and // because it causes more labels to pop in and out when rotating. if (mayOverlap) { const float sin = std::sin(collisionTile.config.angle); const float cos = std::cos(collisionTile.config.angle); std::sort(symbolInstances.begin(), symbolInstances.end(), [sin, cos](SymbolInstance &a, SymbolInstance &b) { const int32_t aRotated = sin * a.x + cos * a.y; const int32_t bRotated = sin * b.x + cos * b.y; return aRotated != bRotated ? aRotated < bRotated : a.index > b.index; }); } for (SymbolInstance &symbolInstance : symbolInstances) { const bool hasText = symbolInstance.hasText; const bool hasIcon = symbolInstance.hasIcon; const bool iconWithoutText = layout.text.optional || !hasText; const bool textWithoutIcon = layout.icon.optional || !hasIcon; // Calculate the scales at which the text and icon can be placed without collision. float glyphScale = hasText ? collisionTile.placeFeature(symbolInstance.textCollisionFeature, layout.text.allowOverlap, layout.avoidEdges) : collisionTile.minScale; float iconScale = hasIcon ? collisionTile.placeFeature(symbolInstance.iconCollisionFeature, layout.icon.allowOverlap, layout.avoidEdges) : collisionTile.minScale; // Combine the scales for icons and text. if (!iconWithoutText && !textWithoutIcon) { iconScale = glyphScale = util::max(iconScale, glyphScale); } else if (!textWithoutIcon && glyphScale) { glyphScale = util::max(iconScale, glyphScale); } else if (!iconWithoutText && iconScale) { iconScale = util::max(iconScale, glyphScale); } // Insert final placement into collision tree and add glyphs/icons to buffers if (hasText) { if (!layout.text.ignorePlacement) { collisionTile.insertFeature(symbolInstance.textCollisionFeature, glyphScale); } if (glyphScale < collisionTile.maxScale) { addSymbols( renderDataInProgress->text, symbolInstance.glyphQuads, glyphScale, layout.text.keepUpright, textAlongLine, collisionTile.config.angle); } } if (hasIcon) { if (!layout.icon.ignorePlacement) { collisionTile.insertFeature(symbolInstance.iconCollisionFeature, iconScale); } if (iconScale < collisionTile.maxScale) { addSymbols( renderDataInProgress->icon, symbolInstance.iconQuads, iconScale, layout.icon.keepUpright, iconAlongLine, collisionTile.config.angle); } } } if (collisionTile.config.debug) { addToDebugBuffers(collisionTile); } } template void SymbolBucket::addSymbols(Buffer &buffer, const SymbolQuads &symbols, float scale, const bool keepUpright, const bool alongLine, const float placementAngle) { const float placementZoom = ::fmax(std::log(scale) / std::log(2) + zoom, 0); for (const auto& symbol : symbols) { const auto &tl = symbol.tl; const auto &tr = symbol.tr; const auto &bl = symbol.bl; const auto &br = symbol.br; const auto &tex = symbol.tex; float minZoom = util::max(static_cast(zoom + log(symbol.minScale) / log(2)), placementZoom); float maxZoom = util::min(static_cast(zoom + log(symbol.maxScale) / log(2)), 25.0f); const auto &anchorPoint = symbol.anchorPoint; // drop upside down versions of glyphs const float a = std::fmod(symbol.angle + placementAngle + M_PI, M_PI * 2); if (keepUpright && alongLine && (a <= M_PI / 2 || a > M_PI * 3 / 2)) continue; if (maxZoom <= minZoom) continue; // Lower min zoom so that while fading out the label // it can be shown outside of collision-free zoom levels if (minZoom == placementZoom) { minZoom = 0; } const int glyph_vertex_length = 4; if (buffer.groups.empty() || (buffer.groups.back()->vertex_length + glyph_vertex_length > 65535)) { // Move to a new group because the old one can't hold the geometry. buffer.groups.emplace_back(std::make_unique()); } // We're generating triangle fans, so we always start with the first // coordinate in this polygon. assert(buffer.groups.back()); auto &triangleGroup = *buffer.groups.back(); GLsizei triangleIndex = triangleGroup.vertex_length; // coordinates (2 triangles) buffer.vertices.add(anchorPoint.x, anchorPoint.y, tl.x, tl.y, tex.x, tex.y, minZoom, maxZoom, placementZoom); buffer.vertices.add(anchorPoint.x, anchorPoint.y, tr.x, tr.y, tex.x + tex.w, tex.y, minZoom, maxZoom, placementZoom); buffer.vertices.add(anchorPoint.x, anchorPoint.y, bl.x, bl.y, tex.x, tex.y + tex.h, minZoom, maxZoom, placementZoom); buffer.vertices.add(anchorPoint.x, anchorPoint.y, br.x, br.y, tex.x + tex.w, tex.y + tex.h, minZoom, maxZoom, placementZoom); // add the two triangles, referencing the four coordinates we just inserted. buffer.triangles.add(triangleIndex + 0, triangleIndex + 1, triangleIndex + 2); buffer.triangles.add(triangleIndex + 1, triangleIndex + 2, triangleIndex + 3); triangleGroup.vertex_length += glyph_vertex_length; triangleGroup.elements_length += 2; } } void SymbolBucket::addToDebugBuffers(CollisionTile &collisionTile) { const float yStretch = collisionTile.yStretch; const float angle = collisionTile.config.angle; float angle_sin = std::sin(-angle); float angle_cos = std::cos(-angle); std::array matrix = {{angle_cos, -angle_sin, angle_sin, angle_cos}}; for (const SymbolInstance &symbolInstance : symbolInstances) { for (int i = 0; i < 2; i++) { auto& feature = i == 0 ? symbolInstance.textCollisionFeature : symbolInstance.iconCollisionFeature; for (const CollisionBox &box : feature.boxes) { auto& anchor = box.anchor; vec2 tl{box.x1, box.y1 * yStretch}; vec2 tr{box.x2, box.y1 * yStretch}; vec2 bl{box.x1, box.y2 * yStretch}; vec2 br{box.x2, box.y2 * yStretch}; tl = tl.matMul(matrix); tr = tr.matMul(matrix); bl = bl.matMul(matrix); br = br.matMul(matrix); const float maxZoom = util::max(0.0f, util::min(25.0f, static_cast(zoom + log(box.maxScale) / log(2)))); const float placementZoom= util::max(0.0f, util::min(25.0f, static_cast(zoom + log(box.placementScale) / log(2)))); auto& collisionBox = renderDataInProgress->collisionBox; if (collisionBox.groups.empty()) { // Move to a new group because the old one can't hold the geometry. collisionBox.groups.emplace_back(std::make_unique()); } collisionBox.vertices.add(anchor.x, anchor.y, tl.x, tl.y, maxZoom, placementZoom); collisionBox.vertices.add(anchor.x, anchor.y, tr.x, tr.y, maxZoom, placementZoom); collisionBox.vertices.add(anchor.x, anchor.y, tr.x, tr.y, maxZoom, placementZoom); collisionBox.vertices.add(anchor.x, anchor.y, br.x, br.y, maxZoom, placementZoom); collisionBox.vertices.add(anchor.x, anchor.y, br.x, br.y, maxZoom, placementZoom); collisionBox.vertices.add(anchor.x, anchor.y, bl.x, bl.y, maxZoom, placementZoom); collisionBox.vertices.add(anchor.x, anchor.y, bl.x, bl.y, maxZoom, placementZoom); collisionBox.vertices.add(anchor.x, anchor.y, tl.x, tl.y, maxZoom, placementZoom); auto &group= *collisionBox.groups.back(); group.vertex_length += 8; } } } } void SymbolBucket::swapRenderData() { if (renderDataInProgress) { renderData = std::move(renderDataInProgress); } } void SymbolBucket::drawGlyphs(SDFShader& shader, gl::GLObjectStore& glObjectStore) { GLbyte *vertex_index = BUFFER_OFFSET_0; GLbyte *elements_index = BUFFER_OFFSET_0; auto& text = renderData->text; for (auto &group : text.groups) { assert(group); group->array[0].bind(shader, text.vertices, text.triangles, vertex_index, glObjectStore); MBGL_CHECK_ERROR(glDrawElements(GL_TRIANGLES, group->elements_length * 3, GL_UNSIGNED_SHORT, elements_index)); vertex_index += group->vertex_length * text.vertices.itemSize; elements_index += group->elements_length * text.triangles.itemSize; } } void SymbolBucket::drawIcons(SDFShader& shader, gl::GLObjectStore& glObjectStore) { GLbyte *vertex_index = BUFFER_OFFSET_0; GLbyte *elements_index = BUFFER_OFFSET_0; auto& icon = renderData->icon; for (auto &group : icon.groups) { assert(group); group->array[0].bind(shader, icon.vertices, icon.triangles, vertex_index, glObjectStore); MBGL_CHECK_ERROR(glDrawElements(GL_TRIANGLES, group->elements_length * 3, GL_UNSIGNED_SHORT, elements_index)); vertex_index += group->vertex_length * icon.vertices.itemSize; elements_index += group->elements_length * icon.triangles.itemSize; } } void SymbolBucket::drawIcons(IconShader& shader, gl::GLObjectStore& glObjectStore) { GLbyte *vertex_index = BUFFER_OFFSET_0; GLbyte *elements_index = BUFFER_OFFSET_0; auto& icon = renderData->icon; for (auto &group : icon.groups) { assert(group); group->array[1].bind(shader, icon.vertices, icon.triangles, vertex_index, glObjectStore); MBGL_CHECK_ERROR(glDrawElements(GL_TRIANGLES, group->elements_length * 3, GL_UNSIGNED_SHORT, elements_index)); vertex_index += group->vertex_length * icon.vertices.itemSize; elements_index += group->elements_length * icon.triangles.itemSize; } } void SymbolBucket::drawCollisionBoxes(CollisionBoxShader& shader, gl::GLObjectStore& glObjectStore) { GLbyte *vertex_index = BUFFER_OFFSET_0; auto& collisionBox = renderData->collisionBox; for (auto &group : collisionBox.groups) { group->array[0].bind(shader, collisionBox.vertices, vertex_index, glObjectStore); MBGL_CHECK_ERROR(glDrawArrays(GL_LINES, 0, group->vertex_length)); } } } // namespace mbgl