#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 { using namespace style; template static bool has(const style::SymbolLayoutProperties::PossiblyEvaluated& layout) { return layout.get().match( [&] (const std::string& s) { return !s.empty(); }, [&] (const auto&) { return true; } ); } SymbolLayout::SymbolLayout(const BucketParameters& parameters, const std::vector& layers, std::unique_ptr sourceLayer_, ImageDependencies& imageDependencies, GlyphDependencies& glyphDependencies) : sourceLayer(std::move(sourceLayer_)), bucketName(layers.at(0)->getID()), 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 `text-pitch-alignment` inherits `text-rotation-alignment` if (layout.get() == AlignmentType::Auto) { layout.get() = layout.get(); } const bool hasText = has(layout) && !layout.get().empty(); 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(feature->getType(), feature->getID(), [&] (const auto& key) { return feature->getValue(key); })) 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()) { 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); // Loop through all characters of this text and collect unique codepoints. for (char16_t chr : *ft.text) { glyphDependencies[layout.get()].insert(chr); if (canVerticalizeText) { if (char16_t verticalChr = util::i18n::verticalizePunctuation(chr)) { glyphDependencies[layout.get()].insert(verticalChr); } } } } if (hasIcon) { std::string icon = layout.evaluate(zoom, ft); if (layout.get().isConstant()) { 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) { float horizontalAlign = 0.5; float verticalAlign = 0.5; switch (layout.get()) { 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.get()) { 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.get() == TextJustifyType::Right ? 1 : layout.get() == TextJustifyType::Left ? 0 : 0.5; const bool textAlongLine = layout.get() == AlignmentType::Map && layout.get() == SymbolPlacementType::Line; auto glyphMapIt = glyphMap.find(layout.get()); const Glyphs& glyphs = glyphMapIt != glyphMap.end() ? glyphMapIt->second : Glyphs(); auto glyphPositionsIt = glyphPositions.find(layout.get()); const GlyphPositionMap& glyphPositionMap = glyphPositionsIt != glyphPositions.end() ? glyphPositionsIt->second : GlyphPositionMap(); for (auto it = features.begin(); it != features.end(); ++it) { auto& feature = *it; if (feature.geometry.empty()) continue; 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.get() * oneEm : 0, /* lineHeight: ems */ layout.get() * oneEm, /* horizontalAlign */ horizontalAlign, /* verticalAlign */ verticalAlign, /* justify */ justify, /* spacing: ems */ util::i18n::allowsLetterSpacing(*feature.text) ? layout.get() * 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) * 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 SymbolPlacementType iconPlacement = layout.get() != AlignmentType::Map ? SymbolPlacementType::Point : layout.get(); const float textRepeatDistance = symbolSpacing / 2; IndexedSubfeature indexedFeature = { feature.index, sourceLayer->getName(), bucketName, 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; const bool addToBuffers = mode == MapMode::Still || withinPlus0; symbolInstances.emplace_back(anchor, line, shapedTextOrientations, shapedIcon, layout.evaluate(zoom, feature), layoutTextSize, addToBuffers, symbolInstances.size(), textBoxScale, textPadding, textPlacement, textOffset, iconBoxScale, iconPadding, iconPlacement, iconOffset, glyphPositionMap, indexedFeature, index); }; 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; } std::unique_ptr SymbolLayout::place(CollisionTile& collisionTile) { auto bucket = std::make_unique(layout, layerPaintProperties, textSize, iconSize, zoom, sdfIcons, iconsNeedLinear); // Calculate which labels can be shown and when they can be shown and // create the bufers used for rendering. const SymbolPlacementType textPlacement = layout.get() != AlignmentType::Map ? SymbolPlacementType::Point : layout.get(); const SymbolPlacementType iconPlacement = layout.get() != AlignmentType::Map ? SymbolPlacementType::Point : layout.get(); const bool mayOverlap = layout.get() || layout.get() || layout.get() || layout.get(); const bool keepUpright = layout.get(); // 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.anchor.point.x + cos * a.anchor.point.y; const int32_t bRotated = sin * b.anchor.point.x + cos * b.anchor.point.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.get() || !hasText; const bool textWithoutIcon = layout.get() || !hasIcon; // Calculate the scales at which the text and icon can be placed without collision. float glyphScale = hasText ? collisionTile.placeFeature(symbolInstance.textCollisionFeature, layout.get(), layout.get()) : collisionTile.minScale; float iconScale = hasIcon ? collisionTile.placeFeature(symbolInstance.iconCollisionFeature, layout.get(), layout.get()) : 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); } const auto& feature = features.at(symbolInstance.featureIndex); // Insert final placement into collision tree and add glyphs/icons to buffers if (hasText) { const float placementZoom = util::max(util::log2(glyphScale) + zoom, 0.0f); collisionTile.insertFeature(symbolInstance.textCollisionFeature, glyphScale, layout.get()); if (glyphScale < collisionTile.maxScale) { const float labelAngle = std::fmod((symbolInstance.anchor.angle + collisionTile.config.angle) + 2 * M_PI, 2 * M_PI); const bool inVerticalRange = ( (labelAngle > M_PI * 1.0 / 4.0 && labelAngle <= M_PI * 3.0 / 4) || (labelAngle > M_PI * 5.0 / 4.0 && labelAngle <= M_PI * 7.0 / 4)); const bool useVerticalMode = symbolInstance.writingModes & WritingModeType::Vertical && inVerticalRange; const Range sizeData = bucket->textSizeBinder->getVertexSizeData(feature); bucket->text.placedSymbols.emplace_back(symbolInstance.anchor.point, symbolInstance.anchor.segment, sizeData.min, sizeData.max, symbolInstance.textOffset, placementZoom, useVerticalMode, symbolInstance.line); for (const auto& symbol : symbolInstance.glyphQuads) { addSymbol( bucket->text, sizeData, symbol, placementZoom, keepUpright, textPlacement, symbolInstance.anchor, bucket->text.placedSymbols.back()); } } } if (hasIcon) { const float placementZoom = util::max(util::log2(iconScale) + zoom, 0.0f); collisionTile.insertFeature(symbolInstance.iconCollisionFeature, iconScale, layout.get()); if (iconScale < collisionTile.maxScale && 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, placementZoom, false, symbolInstance.line); addSymbol( bucket->icon, sizeData, *symbolInstance.iconQuad, placementZoom, keepUpright, iconPlacement, symbolInstance.anchor, bucket->icon.placedSymbols.back()); } } for (auto& pair : bucket->paintPropertyBinders) { pair.second.first.populateVertexVectors(feature, bucket->icon.vertices.vertexSize()); pair.second.second.populateVertexVectors(feature, bucket->text.vertices.vertexSize()); } } if (collisionTile.config.debug) { addToDebugBuffers(collisionTile, *bucket); } return bucket; } template void SymbolLayout::addSymbol(Buffer& buffer, const Range sizeData, const SymbolQuad& symbol, const float placementZoom, const bool keepUpright, const style::SymbolPlacementType placement, 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 (placement == style::SymbolPlacementType::Line && keepUpright) { // drop incorrectly oriented glyphs if ((symbol.writingMode == WritingModeType::Vertical) != placedSymbol.useVerticalMode) return; } 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)); auto dynamicVertex = SymbolDynamicLayoutAttributes::vertex(labelAnchor.point, 0, placementZoom); buffer.dynamicVertices.emplace_back(dynamicVertex); buffer.dynamicVertices.emplace_back(dynamicVertex); buffer.dynamicVertices.emplace_back(dynamicVertex); buffer.dynamicVertices.emplace_back(dynamicVertex); // 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); } void SymbolLayout::addToDebugBuffers(CollisionTile& collisionTile, SymbolBucket& bucket) { if (!hasSymbolInstances()) { return; } const float yStretch = collisionTile.yStretch; auto& collisionBox = bucket.collisionBox; for (const SymbolInstance &symbolInstance : symbolInstances) { auto populateCollisionBox = [&](const auto& feature) { for (const CollisionBox &box : feature.boxes) { auto& anchor = box.anchor; Point tl{box.x1, box.y1 * yStretch}; Point tr{box.x2, box.y1 * yStretch}; Point bl{box.x1, box.y2 * yStretch}; Point br{box.x2, box.y2 * yStretch}; tl = util::matrixMultiply(collisionTile.reverseRotationMatrix, tl); tr = util::matrixMultiply(collisionTile.reverseRotationMatrix, tr); bl = util::matrixMultiply(collisionTile.reverseRotationMatrix, bl); br = util::matrixMultiply(collisionTile.reverseRotationMatrix, br); const float maxZoom = util::clamp(zoom + util::log2(box.maxScale), util::MIN_ZOOM_F, util::MAX_ZOOM_F); const float placementZoom = util::clamp(zoom + util::log2(box.placementScale), util::MIN_ZOOM_F, util::MAX_ZOOM_F); static constexpr std::size_t vertexLength = 4; static constexpr std::size_t indexLength = 8; if (collisionBox.segments.empty() || collisionBox.segments.back().vertexLength + vertexLength > std::numeric_limits::max()) { collisionBox.segments.emplace_back(collisionBox.vertices.vertexSize(), collisionBox.lines.indexSize()); } auto& segment = collisionBox.segments.back(); uint16_t index = segment.vertexLength; collisionBox.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, tl, maxZoom, placementZoom)); collisionBox.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, tr, maxZoom, placementZoom)); collisionBox.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, br, maxZoom, placementZoom)); collisionBox.vertices.emplace_back(CollisionBoxProgram::vertex(anchor, symbolInstance.anchor.point, bl, maxZoom, placementZoom)); collisionBox.lines.emplace_back(index + 0, index + 1); collisionBox.lines.emplace_back(index + 1, index + 2); collisionBox.lines.emplace_back(index + 2, index + 3); collisionBox.lines.emplace_back(index + 3, index + 0); segment.vertexLength += vertexLength; segment.indexLength += indexLength; } }; populateCollisionBox(symbolInstance.textCollisionFeature); populateCollisionBox(symbolInstance.iconCollisionFeature); } } } // namespace mbgl