#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 { using namespace style; template static bool has(const style::SymbolLayoutProperties::PossiblyEvaluated& layout) { return layout.get().match( [&] (const typename Property::Type& t) { return !t.empty(); }, [&] (const auto&) { return true; } ); } SymbolLayout::SymbolLayout(const BucketParameters& parameters, const std::vector& layers, std::unique_ptr sourceLayer_, ImageDependencies& imageDependencies, GlyphDependencies& glyphDependencies) : bucketLeaderID(layers.at(0)->getID()), sourceLayer(std::move(sourceLayer_)), overscaling(parameters.tileID.overscaleFactor()), zoom(parameters.tileID.overscaledZ), mode(parameters.mode), pixelRatio(parameters.pixelRatio), tileSize(util::tileSize * overscaling), tilePixelRatio(float(util::EXTENT) / tileSize), textSize(layers.at(0)->as()->impl().layout.get()), iconSize(layers.at(0)->as()->impl().layout.get()) { const SymbolLayer::Impl& leader = layers.at(0)->as()->impl(); layout = leader.layout.evaluate(PropertyEvaluationParameters(zoom)); if (layout.get() == AlignmentType::Auto) { if (layout.get() == SymbolPlacementType::Line) { layout.get() = AlignmentType::Map; } else { layout.get() = AlignmentType::Viewport; } } if (layout.get() == AlignmentType::Auto) { if (layout.get() == SymbolPlacementType::Line) { layout.get() = AlignmentType::Map; } else { layout.get() = AlignmentType::Viewport; } } // If unspecified `*-pitch-alignment` inherits `*-rotation-alignment` if (layout.get() == AlignmentType::Auto) { layout.get() = layout.get(); } if (layout.get() == AlignmentType::Auto) { layout.get() = layout.get(); } const bool hasText = has(layout) && has(layout); const bool hasIcon = has(layout); if (!hasText && !hasIcon) { return; } for (const auto& layer : layers) { layerPaintProperties.emplace(layer->getID(), std::make_pair( layer->as()->iconPaintProperties(), layer->as()->textPaintProperties() )); } // Determine glyph dependencies const size_t featureCount = sourceLayer->featureCount(); for (size_t i = 0; i < featureCount; ++i) { auto feature = sourceLayer->getFeature(i); if (!leader.filter(expression::EvaluationContext { this->zoom, feature.get() })) continue; SymbolFeature ft(std::move(feature)); ft.index = i; auto getValue = [&ft](const std::string& key) -> std::string { auto value = ft.getValue(key); if (!value) return std::string(); if (value->is()) return value->get(); if (value->is()) return value->get() ? "true" : "false"; if (value->is()) return util::toString(value->get()); if (value->is()) return util::toString(value->get()); if (value->is()) return util::toString(value->get()); return "null"; }; if (hasText) { std::string u8string = layout.evaluate(zoom, ft); if (layout.get().isConstant() && !leader.layout.get().isExpression()) { u8string = util::replaceTokens(u8string, getValue); } auto textTransform = layout.evaluate(zoom, ft); if (textTransform == TextTransformType::Uppercase) { u8string = platform::uppercase(u8string); } else if (textTransform == TextTransformType::Lowercase) { u8string = platform::lowercase(u8string); } ft.text = applyArabicShaping(util::utf8_to_utf16::convert(u8string)); const bool canVerticalizeText = layout.get() == AlignmentType::Map && layout.get() == SymbolPlacementType::Line && util::i18n::allowsVerticalWritingMode(*ft.text); FontStack fontStack = layout.evaluate(zoom, ft); GlyphIDs& dependencies = glyphDependencies[fontStack]; // Loop through all characters of this text and collect unique codepoints. for (char16_t chr : *ft.text) { dependencies.insert(chr); if (canVerticalizeText) { if (char16_t verticalChr = util::i18n::verticalizePunctuation(chr)) { dependencies.insert(verticalChr); } } } } if (hasIcon) { std::string icon = layout.evaluate(zoom, ft); if (layout.get().isConstant() && !leader.layout.get().isExpression()) { icon = util::replaceTokens(icon, getValue); } ft.icon = icon; imageDependencies.insert(*ft.icon); } if (ft.text || ft.icon) { features.push_back(std::move(ft)); } } if (layout.get() == SymbolPlacementType::Line) { util::mergeLines(features); } } bool SymbolLayout::hasSymbolInstances() const { return !symbolInstances.empty(); } void SymbolLayout::prepare(const GlyphMap& glyphMap, const GlyphPositions& glyphPositions, const ImageMap& imageMap, const ImagePositions& imagePositions) { const bool textAlongLine = layout.get() == AlignmentType::Map && layout.get() == SymbolPlacementType::Line; for (auto it = features.begin(); it != features.end(); ++it) { auto& feature = *it; if (feature.geometry.empty()) continue; FontStack fontStack = layout.evaluate(zoom, feature); auto glyphMapIt = glyphMap.find(fontStack); const Glyphs& glyphs = glyphMapIt != glyphMap.end() ? glyphMapIt->second : Glyphs(); auto glyphPositionsIt = glyphPositions.find(fontStack); const GlyphPositionMap& glyphPositionMap = glyphPositionsIt != glyphPositions.end() ? glyphPositionsIt->second : GlyphPositionMap(); std::pair shapedTextOrientations; optional shapedIcon; // if feature has text, shape the text if (feature.text) { auto applyShaping = [&] (const std::u16string& text, WritingModeType writingMode) { const float oneEm = 24.0f; const Shaping result = getShaping( /* string */ text, /* maxWidth: ems */ layout.get() != SymbolPlacementType::Line ? layout.evaluate(zoom, feature) * oneEm : 0, /* lineHeight: ems */ layout.get() * oneEm, /* anchor */ layout.evaluate(zoom, feature), /* justify */ layout.evaluate(zoom, feature), /* spacing: ems */ util::i18n::allowsLetterSpacing(*feature.text) ? layout.evaluate(zoom, feature) * oneEm : 0.0f, /* translate */ Point(layout.evaluate(zoom, feature)[0] * oneEm, layout.evaluate(zoom, feature)[1] * oneEm), /* verticalHeight */ oneEm, /* writingMode */ writingMode, /* bidirectional algorithm object */ bidi, /* glyphs */ glyphs); return result; }; shapedTextOrientations.first = applyShaping(*feature.text, WritingModeType::Horizontal); if (util::i18n::allowsVerticalWritingMode(*feature.text) && textAlongLine) { shapedTextOrientations.second = applyShaping(util::i18n::verticalizePunctuation(*feature.text), WritingModeType::Vertical); } } // if feature has icon, get sprite atlas position if (feature.icon) { auto image = imageMap.find(*feature.icon); if (image != imageMap.end()) { shapedIcon = PositionedIcon::shapeIcon( imagePositions.at(*feature.icon), layout.evaluate(zoom, feature), layout.evaluate(zoom, feature), layout.evaluate(zoom, feature) * util::DEG2RAD); if (image->second->sdf) { sdfIcons = true; } if (image->second->pixelRatio != pixelRatio) { iconsNeedLinear = true; } else if (layout.get().constantOr(1) != 0) { iconsNeedLinear = true; } } } // if either shapedText or icon position is present, add the feature if (shapedTextOrientations.first || shapedIcon) { addFeature(std::distance(features.begin(), it), feature, shapedTextOrientations, shapedIcon, glyphPositionMap); } feature.geometry.clear(); } compareText.clear(); } void SymbolLayout::addFeature(const std::size_t index, const SymbolFeature& feature, const std::pair& shapedTextOrientations, optional shapedIcon, const GlyphPositionMap& glyphPositionMap) { const float minScale = 0.5f; const float glyphSize = 24.0f; const float layoutTextSize = layout.evaluate(zoom + 1, feature); const float layoutIconSize = layout.evaluate(zoom + 1, feature); const std::array textOffset = layout.evaluate(zoom, feature); const std::array iconOffset = layout.evaluate(zoom, feature); // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. // This calculates text-size at a high zoom level so that all tiles can // use the same value when calculating anchor positions. const float textMaxSize = layout.evaluate(18, feature); const float fontScale = layoutTextSize / glyphSize; const float textBoxScale = tilePixelRatio * fontScale; const float textMaxBoxScale = tilePixelRatio * textMaxSize / glyphSize; const float iconBoxScale = tilePixelRatio * layoutIconSize; const float symbolSpacing = tilePixelRatio * layout.get(); const bool avoidEdges = layout.get() && layout.get() != SymbolPlacementType::Line; const float textPadding = layout.get() * tilePixelRatio; const float iconPadding = layout.get() * tilePixelRatio; const float textMaxAngle = layout.get() * util::DEG2RAD; const SymbolPlacementType textPlacement = layout.get() != AlignmentType::Map ? SymbolPlacementType::Point : layout.get(); const float textRepeatDistance = symbolSpacing / 2; IndexedSubfeature indexedFeature(feature.index, sourceLayer->getName(), bucketLeaderID, symbolInstances.size()); auto addSymbolInstance = [&] (const GeometryCoordinates& line, Anchor& anchor) { // https://github.com/mapbox/vector-tile-spec/tree/master/2.1#41-layers // +-------------------+ Symbols with anchors located on tile edges // |(0,0) || are duplicated on neighbor tiles. // | || // | || In continuous mode, to avoid overdraw we // | || skip symbols located on the extent edges. // | Tile || In still mode, we include the features in // | || the buffers for both tiles and clip them // | || at draw time. // | || // +-------------------| In this scenario, the inner bounding box // +-------------------+ is called 'withinPlus0', and the outer // (extent,extent) is called 'inside'. const bool withinPlus0 = anchor.point.x >= 0 && anchor.point.x < util::EXTENT && anchor.point.y >= 0 && anchor.point.y < util::EXTENT; const bool inside = withinPlus0 || anchor.point.x == util::EXTENT || anchor.point.y == util::EXTENT; if (avoidEdges && !inside) return; if (mode == MapMode::Tile || withinPlus0) { symbolInstances.emplace_back(anchor, line, shapedTextOrientations, shapedIcon, layout.evaluate(zoom, feature), layoutTextSize, symbolInstances.size(), textBoxScale, textPadding, textPlacement, textOffset, iconBoxScale, iconPadding, iconOffset, glyphPositionMap, indexedFeature, index, feature.text.value_or(std::u16string()), overscaling); } }; const auto& type = feature.getType(); if (layout.get() == SymbolPlacementType::Line) { auto clippedLines = util::clipLines(feature.geometry, 0, 0, util::EXTENT, util::EXTENT); for (const auto& line : clippedLines) { Anchors anchors = getAnchors(line, symbolSpacing, textMaxAngle, (shapedTextOrientations.second ?: shapedTextOrientations.first).left, (shapedTextOrientations.second ?: shapedTextOrientations.first).right, (shapedIcon ? shapedIcon->left() : 0), (shapedIcon ? shapedIcon->right() : 0), glyphSize, textMaxBoxScale, overscaling); for (auto& anchor : anchors) { if (!feature.text || !anchorIsTooClose(*feature.text, textRepeatDistance, anchor)) { addSymbolInstance(line, anchor); } } } } else if (type == FeatureType::Polygon) { for (const auto& polygon : classifyRings(feature.geometry)) { Polygon poly; for (const auto& ring : polygon) { LinearRing r; for (const auto& p : ring) { r.push_back(convertPoint(p)); } poly.push_back(r); } // 1 pixel worth of precision, in tile coordinates auto poi = mapbox::polylabel(poly, double(util::EXTENT / util::tileSize)); Anchor anchor(poi.x, poi.y, 0, minScale); addSymbolInstance(polygon[0], anchor); } } else if (type == FeatureType::LineString) { for (const auto& line : feature.geometry) { Anchor anchor(line[0].x, line[0].y, 0, minScale); addSymbolInstance(line, anchor); } } else if (type == FeatureType::Point) { for (const auto& points : feature.geometry) { for (const auto& point : points) { Anchor anchor(point.x, point.y, 0, minScale); addSymbolInstance({point}, anchor); } } } } bool SymbolLayout::anchorIsTooClose(const std::u16string& text, const float repeatDistance, const Anchor& anchor) { if (compareText.find(text) == compareText.end()) { compareText.emplace(text, Anchors()); } else { auto otherAnchors = compareText.find(text)->second; for (const Anchor& otherAnchor : otherAnchors) { if (util::dist(anchor.point, otherAnchor.point) < repeatDistance) { return true; } } } compareText[text].push_back(anchor); return false; } // Analog of `addToLineVertexArray` in JS. This version doesn't need to build up a line array like the // JS version does, but it uses the same logic to calculate tile distances. std::vector CalculateTileDistances(const GeometryCoordinates& line, const Anchor& anchor) { std::vector tileDistances(line.size()); if (anchor.segment != -1) { auto sumForwardLength = util::dist(anchor.point, line[anchor.segment + 1]); auto sumBackwardLength = util::dist(anchor.point, line[anchor.segment]); for (size_t i = anchor.segment + 1; i < line.size(); i++) { tileDistances[i] = sumForwardLength; if (i < line.size() - 1) { sumForwardLength += util::dist(line[i + 1], line[i]); } } for (auto i = anchor.segment; i >= 0; i--) { tileDistances[i] = sumBackwardLength; if (i > 0) { sumBackwardLength += util::dist(line[i - 1], line[i]); } } } return tileDistances; } std::unique_ptr SymbolLayout::place(const bool showCollisionBoxes) { const bool mayOverlap = layout.get() || layout.get() || layout.get() || layout.get(); auto bucket = std::make_unique(layout, layerPaintProperties, textSize, iconSize, zoom, sdfIcons, iconsNeedLinear, mayOverlap, bucketLeaderID, std::move(symbolInstances)); for (SymbolInstance &symbolInstance : bucket->symbolInstances) { const bool hasText = symbolInstance.hasText; const bool hasIcon = symbolInstance.hasIcon; const auto& feature = features.at(symbolInstance.featureIndex); // Insert final placement into collision tree and add glyphs/icons to buffers if (hasText) { const Range sizeData = bucket->textSizeBinder->getVertexSizeData(feature); bucket->text.placedSymbols.emplace_back(symbolInstance.anchor.point, symbolInstance.anchor.segment, sizeData.min, sizeData.max, symbolInstance.textOffset, symbolInstance.writingModes, symbolInstance.line, CalculateTileDistances(symbolInstance.line, symbolInstance.anchor)); symbolInstance.placedTextIndex = bucket->text.placedSymbols.size() - 1; PlacedSymbol& horizontalSymbol = bucket->text.placedSymbols.back(); bool firstHorizontal = true; for (const auto& symbol : symbolInstance.horizontalGlyphQuads) { size_t index = addSymbol( bucket->text, sizeData, symbol, symbolInstance.anchor, horizontalSymbol); if (firstHorizontal) { horizontalSymbol.vertexStartIndex = index; firstHorizontal = false; } } if (symbolInstance.writingModes & WritingModeType::Vertical) { bucket->text.placedSymbols.emplace_back(symbolInstance.anchor.point, symbolInstance.anchor.segment, sizeData.min, sizeData.max, symbolInstance.textOffset, WritingModeType::Vertical, symbolInstance.line, CalculateTileDistances(symbolInstance.line, symbolInstance.anchor)); symbolInstance.placedVerticalTextIndex = bucket->text.placedSymbols.size() - 1; PlacedSymbol& verticalSymbol = bucket->text.placedSymbols.back(); bool firstVertical = true; for (const auto& symbol : symbolInstance.verticalGlyphQuads) { size_t index = addSymbol( bucket->text, sizeData, symbol, symbolInstance.anchor, verticalSymbol); if (firstVertical) { verticalSymbol.vertexStartIndex = index; firstVertical = false; } } } } if (hasIcon) { if (symbolInstance.iconQuad) { const Range sizeData = bucket->iconSizeBinder->getVertexSizeData(feature); bucket->icon.placedSymbols.emplace_back(symbolInstance.anchor.point, symbolInstance.anchor.segment, sizeData.min, sizeData.max, symbolInstance.iconOffset, WritingModeType::None, symbolInstance.line, std::vector()); symbolInstance.placedIconIndex = bucket->icon.placedSymbols.size() - 1; PlacedSymbol& iconSymbol = bucket->icon.placedSymbols.back(); iconSymbol.vertexStartIndex = addSymbol( bucket->icon, sizeData, *symbolInstance.iconQuad, symbolInstance.anchor, iconSymbol); } } for (auto& pair : bucket->paintPropertyBinders) { pair.second.first.populateVertexVectors(feature, bucket->icon.vertices.vertexSize()); pair.second.second.populateVertexVectors(feature, bucket->text.vertices.vertexSize()); } } if (showCollisionBoxes) { addToDebugBuffers(*bucket); } return bucket; } template size_t SymbolLayout::addSymbol(Buffer& buffer, const Range sizeData, const SymbolQuad& symbol, const Anchor& labelAnchor, PlacedSymbol& placedSymbol) { constexpr const uint16_t vertexLength = 4; 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; if (buffer.segments.empty() || buffer.segments.back().vertexLength + vertexLength > std::numeric_limits::max()) { buffer.segments.emplace_back(buffer.vertices.vertexSize(), buffer.triangles.indexSize()); } // We're generating triangle fans, so we always start with the first // coordinate in this polygon. auto& segment = buffer.segments.back(); assert(segment.vertexLength <= std::numeric_limits::max()); uint16_t index = segment.vertexLength; // coordinates (2 triangles) buffer.vertices.emplace_back(SymbolLayoutAttributes::vertex(labelAnchor.point, tl, symbol.glyphOffset.y, tex.x, tex.y, sizeData)); buffer.vertices.emplace_back(SymbolLayoutAttributes::vertex(labelAnchor.point, tr, symbol.glyphOffset.y, tex.x + tex.w, tex.y, sizeData)); buffer.vertices.emplace_back(SymbolLayoutAttributes::vertex(labelAnchor.point, bl, symbol.glyphOffset.y, tex.x, tex.y + tex.h, sizeData)); buffer.vertices.emplace_back(SymbolLayoutAttributes::vertex(labelAnchor.point, br, symbol.glyphOffset.y, tex.x + tex.w, tex.y + tex.h, sizeData)); // Dynamic/Opacity vertices are initialized so that the vertex count always agrees with // the layout vertex buffer, but they will always be updated before rendering happens auto dynamicVertex = SymbolDynamicLayoutAttributes::vertex(labelAnchor.point, 0); buffer.dynamicVertices.emplace_back(dynamicVertex); buffer.dynamicVertices.emplace_back(dynamicVertex); buffer.dynamicVertices.emplace_back(dynamicVertex); buffer.dynamicVertices.emplace_back(dynamicVertex); auto opacityVertex = SymbolOpacityAttributes::vertex(1.0, 1.0); buffer.opacityVertices.emplace_back(opacityVertex); buffer.opacityVertices.emplace_back(opacityVertex); buffer.opacityVertices.emplace_back(opacityVertex); buffer.opacityVertices.emplace_back(opacityVertex); // add the two triangles, referencing the four coordinates we just inserted. buffer.triangles.emplace_back(index + 0, index + 1, index + 2); buffer.triangles.emplace_back(index + 1, index + 2, index + 3); segment.vertexLength += vertexLength; segment.indexLength += 6; placedSymbol.glyphOffsets.push_back(symbol.glyphOffset.x); return index; } void SymbolLayout::addToDebugBuffers(SymbolBucket& bucket) { if (!hasSymbolInstances()) { return; } for (const SymbolInstance &symbolInstance : symbolInstances) { auto populateCollisionBox = [&](const auto& feature) { SymbolBucket::CollisionBuffer& collisionBuffer = feature.alongLine ? static_cast(bucket.collisionCircle) : static_cast(bucket.collisionBox); for (const CollisionBox &box : feature.boxes) { auto& anchor = box.anchor; Point tl{box.x1, box.y1}; Point tr{box.x2, box.y1}; Point bl{box.x1, box.y2}; Point br{box.x2, box.y2}; static constexpr std::size_t vertexLength = 4; const std::size_t indexLength = feature.alongLine ? 6 : 8; if (collisionBuffer.segments.empty() || collisionBuffer.segments.back().vertexLength + vertexLength > std::numeric_limits::max()) { collisionBuffer.segments.emplace_back(collisionBuffer.vertices.vertexSize(), feature.alongLine? bucket.collisionCircle.triangles.indexSize() : bucket.collisionBox.lines.indexSize()); } auto& segment = collisionBuffer.segments.back(); uint16_t index = segment.vertexLength; collisionBuffer.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, tl)); collisionBuffer.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, tr)); collisionBuffer.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, br)); collisionBuffer.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, bl)); // Dynamic vertices are initialized so that the vertex count always agrees with // the layout vertex buffer, but they will always be updated before rendering happens auto dynamicVertex = CollisionBoxDynamicAttributes::vertex(false, false); collisionBuffer.dynamicVertices.emplace_back(dynamicVertex); collisionBuffer.dynamicVertices.emplace_back(dynamicVertex); collisionBuffer.dynamicVertices.emplace_back(dynamicVertex); collisionBuffer.dynamicVertices.emplace_back(dynamicVertex); if (feature.alongLine) { bucket.collisionCircle.triangles.emplace_back(index, index + 1, index + 2); bucket.collisionCircle.triangles.emplace_back(index, index + 2, index + 3); } else { bucket.collisionBox.lines.emplace_back(index + 0, index + 1); bucket.collisionBox.lines.emplace_back(index + 1, index + 2); bucket.collisionBox.lines.emplace_back(index + 2, index + 3); bucket.collisionBox.lines.emplace_back(index + 3, index + 0); } segment.vertexLength += vertexLength; segment.indexLength += indexLength; } }; populateCollisionBox(symbolInstance.textCollisionFeature); populateCollisionBox(symbolInstance.iconCollisionFeature); } } } // namespace mbgl