diff options
Diffstat (limited to 'src/mbgl/text')
-rw-r--r-- | src/mbgl/text/collision_feature.cpp | 81 | ||||
-rw-r--r-- | src/mbgl/text/collision_feature.hpp | 48 | ||||
-rw-r--r-- | src/mbgl/text/collision_index.cpp | 359 | ||||
-rw-r--r-- | src/mbgl/text/collision_index.hpp | 70 | ||||
-rw-r--r-- | src/mbgl/text/collision_tile.cpp | 267 | ||||
-rw-r--r-- | src/mbgl/text/collision_tile.hpp | 71 | ||||
-rw-r--r-- | src/mbgl/text/cross_tile_symbol_index.cpp | 165 | ||||
-rw-r--r-- | src/mbgl/text/cross_tile_symbol_index.hpp | 64 | ||||
-rw-r--r-- | src/mbgl/text/glyph.hpp | 8 | ||||
-rw-r--r-- | src/mbgl/text/placement.cpp | 332 | ||||
-rw-r--r-- | src/mbgl/text/placement.hpp | 91 | ||||
-rw-r--r-- | src/mbgl/text/placement_config.hpp | 33 | ||||
-rw-r--r-- | src/mbgl/text/shaping.cpp | 2 |
13 files changed, 1140 insertions, 451 deletions
diff --git a/src/mbgl/text/collision_feature.cpp b/src/mbgl/text/collision_feature.cpp index 3eb08da8d1..6d6f2aabc7 100644 --- a/src/mbgl/text/collision_feature.cpp +++ b/src/mbgl/text/collision_feature.cpp @@ -13,8 +13,9 @@ CollisionFeature::CollisionFeature(const GeometryCoordinates& line, const float padding, const style::SymbolPlacementType placement, IndexedSubfeature indexedFeature_, - const AlignmentType alignment) - : indexedFeature(std::move(indexedFeature_)) { + const float overscaling) + : indexedFeature(std::move(indexedFeature_)) + , alongLine(placement == style::SymbolPlacementType::Line) { if (top == 0 && bottom == 0 && left == 0 && right == 0) return; const float y1 = top * boxScale - padding; @@ -22,7 +23,7 @@ CollisionFeature::CollisionFeature(const GeometryCoordinates& line, const float x1 = left * boxScale - padding; const float x2 = right * boxScale + padding; - if (placement == style::SymbolPlacementType::Line) { + if (alongLine) { float height = y2 - y1; const double length = x2 - x1; @@ -31,29 +32,26 @@ CollisionFeature::CollisionFeature(const GeometryCoordinates& line, height = std::max(10.0f * boxScale, height); GeometryCoordinate anchorPoint = convertPoint<int16_t>(anchor.point); - - if (alignment == AlignmentType::Straight) { - // used for icon labels that are aligned with the line, but don't curve along it - const GeometryCoordinate vector = convertPoint<int16_t>(util::unit(convertPoint<double>(line[anchor.segment + 1] - line[anchor.segment])) * length); - const GeometryCoordinates newLine({ anchorPoint - vector, anchorPoint + vector }); - bboxifyLabel(newLine, anchorPoint, 0, length, height); - } else { - // used for text labels that curve along a line - bboxifyLabel(line, anchorPoint, anchor.segment, length, height); - } + bboxifyLabel(line, anchorPoint, anchor.segment, length, height, overscaling); } else { - boxes.emplace_back(anchor.point, Point<float>{ 0, 0 }, x1, y1, x2, y2, std::numeric_limits<float>::infinity()); + boxes.emplace_back(anchor.point, Point<float>{ 0, 0 }, x1, y1, x2, y2); } } void CollisionFeature::bboxifyLabel(const GeometryCoordinates& line, GeometryCoordinate& anchorPoint, - const int segment, const float labelLength, const float boxSize) { + const int segment, const float labelLength, const float boxSize, const float overscaling) { const float step = boxSize / 2; const int nBoxes = std::floor(labelLength / step); - // We calculate line collision boxes out to 300% of what would normally be our + // We calculate line collision circles out to 300% of what would normally be our // max size, to allow collision detection to work on labels that expand as // they move into the distance - const int nPitchPaddingBoxes = std::floor(nBoxes / 2); + // Vertically oriented labels in the distant field can extend past this padding + // This is a noticeable problem in overscaled tiles where the pitch 0-based + // symbol spacing will put labels very close together in a pitched map. + // To reduce the cost of adding extra collision circles, we slowly increase + // them for overscaled tiles. + const float overscalingPaddingFactor = 1 + .4 * std::log(overscaling) / std::log(2); + const int nPitchPaddingBoxes = std::floor(nBoxes * overscalingPaddingFactor / 2); // offset the center of the first box by half a box so that the edge of the // box is at the edge of the label. @@ -124,47 +122,18 @@ void CollisionFeature::bboxifyLabel(const GeometryCoordinates& line, GeometryCoo p0.x + segmentBoxDistance / segmentLength * (p1.x - p0.x), p0.y + segmentBoxDistance / segmentLength * (p1.y - p0.y) }; - - // Distance from label anchor point to inner (towards center) edge of this box - // The tricky thing here is that box positioning doesn't change with scale, - // but box size does change with scale. - // Technically, distanceToInnerEdge should be: - // Math.max(Math.abs(boxDistanceToAnchor - firstBoxOffset) - (step / scale), 0); - // But using that formula would make solving for maxScale more difficult, so we - // approximate with scale=2. - // This makes our calculation spot-on at scale=2, and on the conservative side for - // lower scales - const float distanceToInnerEdge = std::max(std::fabs(boxDistanceToAnchor - firstBoxOffset) - step / 2, 0.0f); - float maxScale = util::division(labelLength / 2, distanceToInnerEdge, std::numeric_limits<float>::infinity()); - - // The box maxScale calculations are designed to be conservative on collisions in the scale range - // [1,2]. At scale=1, each box has 50% overlap, and at scale=2, the boxes are lined up edge - // to edge (beyond scale 2, gaps start to appear, which could potentially allow missed collisions). - // We add "pitch padding" boxes to the left and right to handle effective underzooming - // (scale < 1) when labels are in the distance. The overlap approximation could cause us to use - // these boxes when the scale is greater than 1, but we prevent that because we know - // they're only necessary for scales less than one. - // This preserves the pre-pitch-padding behavior for unpitched maps. - if (i < 0 || i >= nBoxes) { - maxScale = std::min(maxScale, 0.99f); - } - - boxes.emplace_back(boxAnchor, boxAnchor - convertPoint<float>(anchorPoint), -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, maxScale); + + // If the box is within boxSize of the anchor, force the box to be used + // (so even 0-width labels use at least one box) + // Otherwise, the .8 multiplication gives us a little bit of conservative + // padding in choosing which boxes to use (see CollisionIndex#placedCollisionCircles) + const float paddedAnchorDistance = std::abs(boxDistanceToAnchor - firstBoxOffset) < step ? + 0 : + (boxDistanceToAnchor - firstBoxOffset) * 0.8; + + boxes.emplace_back(boxAnchor, boxAnchor - convertPoint<float>(anchorPoint), -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, paddedAnchorDistance, boxSize / 2); } } -float CollisionBox::adjustedMaxScale(const std::array<float, 4>& rotationMatrix, const float yStretch) const { - // When the map is pitched the distance covered by a line changes. - // Adjust the max scale by (approximatePitchedLength / approximateRegularLength) - // to compensate for this. - const Point<float> rotatedOffset = util::matrixMultiply(rotationMatrix, offset); - const float xSqr = rotatedOffset.x * rotatedOffset.x; - const float ySqr = rotatedOffset.y * rotatedOffset.y; - const float yStretchSqr = ySqr * yStretch * yStretch; - const float adjustmentFactor = xSqr + ySqr != 0 ? - std::sqrt((xSqr + yStretchSqr) / (xSqr + ySqr)) : - 1.0f; - return maxScale * adjustmentFactor; -} } // namespace mbgl diff --git a/src/mbgl/text/collision_feature.hpp b/src/mbgl/text/collision_feature.hpp index 3b6e461a26..df1b12819c 100644 --- a/src/mbgl/text/collision_feature.hpp +++ b/src/mbgl/text/collision_feature.hpp @@ -11,10 +11,8 @@ namespace mbgl { class CollisionBox { public: - CollisionBox(Point<float> _anchor, Point<float> _offset, float _x1, float _y1, float _x2, float _y2, float _maxScale) : - anchor(std::move(_anchor)), offset(_offset), x1(_x1), y1(_y1), x2(_x2), y2(_y2), maxScale(_maxScale) {} - - float adjustedMaxScale(const std::array<float, 4>& rotationMatrix, const float yStretch) const; + CollisionBox(Point<float> _anchor, Point<float> _offset, float _x1, float _y1, float _x2, float _y2, float _signedDistanceFromAnchor = 0, float _radius = 0) : + anchor(std::move(_anchor)), offset(_offset), x1(_x1), y1(_y1), x2(_x2), y2(_y2), used(true), signedDistanceFromAnchor(_signedDistanceFromAnchor), radius(_radius) {} // the box is centered around the anchor point Point<float> anchor; @@ -28,20 +26,23 @@ public: float x2; float y2; - // the box is only valid for scales < maxScale. - // The box does not block other boxes at scales >= maxScale; - float maxScale; + // Projected box geometry: generated/updated at placement time + float px1; + float py1; + float px2; + float py2; + + // Projected circle geometry: generated/updated at placement time + float px; + float py; + bool used; - // the scale at which the label can first be shown - float placementScale = 0.0f; + float signedDistanceFromAnchor; + float radius; }; class CollisionFeature { public: - enum class AlignmentType : bool { - Straight = false, - Curved - }; // for text CollisionFeature(const GeometryCoordinates& line, @@ -50,23 +51,31 @@ public: const float boxScale, const float padding, const style::SymbolPlacementType placement, - const IndexedSubfeature& indexedFeature_) - : CollisionFeature(line, anchor, shapedText.top, shapedText.bottom, shapedText.left, shapedText.right, boxScale, padding, placement, indexedFeature_, AlignmentType::Curved) {} + const IndexedSubfeature& indexedFeature_, + const float overscaling) + : CollisionFeature(line, anchor, shapedText.top, shapedText.bottom, shapedText.left, shapedText.right, boxScale, padding, placement, indexedFeature_, overscaling) {} // for icons + // Icons collision features are always SymbolPlacementType::Point, which means the collision feature + // will be viewport-rotation-aligned even if the icon is map-rotation-aligned (e.g. `icon-rotation-alignment: map` + // _or_ `symbol-placement: line`). We're relying on most icons being "close enough" to square that having + // incorrect rotation alignment doesn't throw off collision detection too much. + // See: https://github.com/mapbox/mapbox-gl-js/issues/4861 CollisionFeature(const GeometryCoordinates& line, const Anchor& anchor, optional<PositionedIcon> shapedIcon, const float boxScale, const float padding, - const style::SymbolPlacementType placement, const IndexedSubfeature& indexedFeature_) : CollisionFeature(line, anchor, (shapedIcon ? shapedIcon->top() : 0), (shapedIcon ? shapedIcon->bottom() : 0), (shapedIcon ? shapedIcon->left() : 0), (shapedIcon ? shapedIcon->right() : 0), - boxScale, padding, placement, indexedFeature_, AlignmentType::Straight) {} + boxScale, + padding, + style::SymbolPlacementType::Point, + indexedFeature_, 1) {} CollisionFeature(const GeometryCoordinates& line, const Anchor&, @@ -78,14 +87,15 @@ public: const float padding, const style::SymbolPlacementType, IndexedSubfeature, - const AlignmentType); + const float overscaling); std::vector<CollisionBox> boxes; IndexedSubfeature indexedFeature; + bool alongLine; private: void bboxifyLabel(const GeometryCoordinates& line, GeometryCoordinate& anchorPoint, - const int segment, const float length, const float height); + const int segment, const float length, const float height, const float overscaling); }; } // namespace mbgl diff --git a/src/mbgl/text/collision_index.cpp b/src/mbgl/text/collision_index.cpp new file mode 100644 index 0000000000..fee28b5873 --- /dev/null +++ b/src/mbgl/text/collision_index.cpp @@ -0,0 +1,359 @@ +#include <mbgl/text/collision_index.hpp> +#include <mbgl/layout/symbol_instance.hpp> +#include <mbgl/geometry/feature_index.hpp> +#include <mbgl/math/log2.hpp> +#include <mbgl/util/constants.hpp> +#include <mbgl/util/math.hpp> +#include <mbgl/math/minmax.hpp> +#include <mbgl/util/intersection_tests.hpp> +#include <mbgl/layout/symbol_projection.hpp> + +#include <mapbox/geometry/envelope.hpp> + +#include <mbgl/renderer/buckets/symbol_bucket.hpp> // For PlacedSymbol: pull out to another location + +#include <cmath> + +namespace mbgl { + +// When a symbol crosses the edge that causes it to be included in +// collision detection, it will cause changes in the symbols around +// it. This constant specifies how many pixels to pad the edge of +// the viewport for collision detection so that the bulk of the changes +// occur offscreen. Making this constant greater increases label +// stability, but it's expensive. +static const float viewportPadding = 100; + +CollisionIndex::CollisionIndex(const TransformState& transformState_) + : transformState(transformState_) + , collisionGrid(transformState.getSize().width + 2 * viewportPadding, transformState.getSize().height + 2 * viewportPadding, 25) + , ignoredGrid(transformState.getSize().width + 2 * viewportPadding, transformState.getSize().height + 2 * viewportPadding, 25) + , screenRightBoundary(transformState.getSize().width + viewportPadding) + , screenBottomBoundary(transformState.getSize().height + viewportPadding) + , gridRightBoundary(transformState.getSize().width + 2 * viewportPadding) + , gridBottomBoundary(transformState.getSize().height + 2 * viewportPadding) + , pitchFactor(std::cos(transformState.getPitch()) * transformState.getCameraToCenterDistance()) +{} + +float CollisionIndex::approximateTileDistance(const TileDistance& tileDistance, const float lastSegmentAngle, const float pixelsToTileUnits, const float cameraToAnchorDistance, const bool pitchWithMap) { + // This is a quick and dirty solution for chosing which collision circles to use (since collision circles are + // laid out in tile units). Ideally, I think we should generate collision circles on the fly in viewport coordinates + // at the time we do collision detection. + + // incidenceStretch is the ratio of how much y space a label takes up on a tile while drawn perpendicular to the viewport vs + // how much space it would take up if it were drawn flat on the tile + // Using law of sines, camera_to_anchor/sin(ground_angle) = camera_to_center/sin(incidence_angle) + // Incidence angle 90 -> head on, sin(incidence_angle) = 1, no stretch + // Incidence angle 1 -> very oblique, sin(incidence_angle) =~ 0, lots of stretch + // ground_angle = u_pitch + PI/2 -> sin(ground_angle) = cos(u_pitch) + // incidenceStretch = 1 / sin(incidenceAngle) + + const float incidenceStretch = pitchWithMap ? 1 : cameraToAnchorDistance / pitchFactor; + const float lastSegmentTile = tileDistance.lastSegmentViewportDistance * pixelsToTileUnits; + return tileDistance.prevTileDistance + + lastSegmentTile + + (incidenceStretch - 1) * lastSegmentTile * std::abs(std::sin(lastSegmentAngle)); +} + +bool CollisionIndex::isOffscreen(const CollisionBox& box) const { + return box.px2 < viewportPadding || box.px1 >= screenRightBoundary || box.py2 < viewportPadding || box.py1 >= screenBottomBoundary; +} + +bool CollisionIndex::isInsideGrid(const CollisionBox& box) const { + return box.px2 >= 0 && box.px1 < gridRightBoundary && box.py2 >= 0 && box.py1 < gridBottomBoundary; +} + + +std::pair<bool,bool> CollisionIndex::placeFeature(CollisionFeature& feature, + const mat4& posMatrix, + const mat4& labelPlaneMatrix, + const float textPixelRatio, + PlacedSymbol& symbol, + const float scale, + const float fontSize, + const bool allowOverlap, + const bool pitchWithMap, + const bool collisionDebug) { + if (!feature.alongLine) { + CollisionBox& box = feature.boxes.front(); + const auto projectedPoint = projectAndGetPerspectiveRatio(posMatrix, box.anchor); + const float tileToViewport = textPixelRatio * projectedPoint.second; + box.px1 = box.x1 / tileToViewport + projectedPoint.first.x; + box.py1 = box.y1 / tileToViewport + projectedPoint.first.y; + box.px2 = box.x2 / tileToViewport + projectedPoint.first.x; + box.py2 = box.y2 / tileToViewport + projectedPoint.first.y; + + if (!isInsideGrid(box) || + (!allowOverlap && collisionGrid.hitTest({{ box.px1, box.py1 }, { box.px2, box.py2 }}))) { + return { false, false }; + } + + return {true, isOffscreen(box)}; + } else { + return placeLineFeature(feature, posMatrix, labelPlaneMatrix, textPixelRatio, symbol, scale, fontSize, allowOverlap, pitchWithMap, collisionDebug); + } +} + +std::pair<bool,bool> CollisionIndex::placeLineFeature(CollisionFeature& feature, + const mat4& posMatrix, + const mat4& labelPlaneMatrix, + const float textPixelRatio, + PlacedSymbol& symbol, + const float scale, + const float fontSize, + const bool allowOverlap, + const bool pitchWithMap, + const bool collisionDebug) { + + const auto tileUnitAnchorPoint = symbol.anchorPoint; + const auto projectedAnchor = projectAnchor(posMatrix, tileUnitAnchorPoint); + + const float fontScale = fontSize / 24; + const float lineOffsetX = symbol.lineOffset[0] * fontSize; + const float lineOffsetY = symbol.lineOffset[1] * fontSize; + + const auto labelPlaneAnchorPoint = project(tileUnitAnchorPoint, labelPlaneMatrix).first; + + const auto firstAndLastGlyph = placeFirstAndLastGlyph( + fontScale, + lineOffsetX, + lineOffsetY, + /*flip*/ false, + labelPlaneAnchorPoint, + tileUnitAnchorPoint, + symbol, + labelPlaneMatrix, + /*return tile distance*/ true); + + bool collisionDetected = false; + bool inGrid = false; + bool entirelyOffscreen = true; + + const auto tileToViewport = projectedAnchor.first * textPixelRatio; + // equivalent to pixel_to_tile_units + const auto pixelsToTileUnits = tileToViewport / scale; + + float firstTileDistance = 0, lastTileDistance = 0; + if (firstAndLastGlyph) { + firstTileDistance = approximateTileDistance(*(firstAndLastGlyph->first.tileDistance), firstAndLastGlyph->first.angle, pixelsToTileUnits, projectedAnchor.second, pitchWithMap); + lastTileDistance = approximateTileDistance(*(firstAndLastGlyph->second.tileDistance), firstAndLastGlyph->second.angle, pixelsToTileUnits, projectedAnchor.second, pitchWithMap); + } + + bool atLeastOneCirclePlaced = false; + for (size_t i = 0; i < feature.boxes.size(); i++) { + CollisionBox& circle = feature.boxes[i]; + const float boxSignedDistanceFromAnchor = circle.signedDistanceFromAnchor; + if (!firstAndLastGlyph || + (boxSignedDistanceFromAnchor < -firstTileDistance) || + (boxSignedDistanceFromAnchor > lastTileDistance)) { + // The label either doesn't fit on its line or we + // don't need to use this circle because the label + // doesn't extend this far. Either way, mark the circle unused. + circle.used = false; + continue; + } + + const auto projectedPoint = projectPoint(posMatrix, circle.anchor); + const float tileUnitRadius = (circle.x2 - circle.x1) / 2; + const float radius = tileUnitRadius / tileToViewport; + + if (atLeastOneCirclePlaced) { + const CollisionBox& previousCircle = feature.boxes[i - 1]; + const float dx = projectedPoint.x - previousCircle.px; + const float dy = projectedPoint.y - previousCircle.py; + // The circle edges touch when the distance between their centers is 2x the radius + // When the distance is 1x the radius, they're doubled up, and we could remove + // every other circle while keeping them all in touch. + // We actually start removing circles when the distance is √2x the radius: + // thinning the number of circles as much as possible is a major performance win, + // and the small gaps introduced don't make a very noticeable difference. + const bool placedTooDensely = radius * radius * 2 > dx * dx + dy * dy; + if (placedTooDensely) { + const bool atLeastOneMoreCircle = (i + 1) < feature.boxes.size(); + if (atLeastOneMoreCircle) { + const CollisionBox& nextCircle = feature.boxes[i + 1]; + const float nextBoxDistanceFromAnchor = nextCircle.signedDistanceFromAnchor; + if ((nextBoxDistanceFromAnchor > -firstTileDistance) && + (nextBoxDistanceFromAnchor < lastTileDistance)) { + // Hide significantly overlapping circles, unless this is the last one we can + // use, in which case we want to keep it in place even if it's tightly packed + // with the one before it. + circle.used = false; + continue; + } + } + } + } + + atLeastOneCirclePlaced = true; + circle.px1 = projectedPoint.x - radius; + circle.px2 = projectedPoint.x + radius; + circle.py1 = projectedPoint.y - radius; + circle.py2 = projectedPoint.y + radius; + + circle.used = true; + + circle.px = projectedPoint.x; + circle.py = projectedPoint.y; + circle.radius = radius; + + entirelyOffscreen &= isOffscreen(circle); + inGrid |= isInsideGrid(circle); + + if (!allowOverlap) { + if (collisionGrid.hitTest({{circle.px, circle.py}, circle.radius})) { + if (!collisionDebug) { + return {false, false}; + } else { + // Don't early exit if we're showing the debug circles because we still want to calculate + // which circles are in use + collisionDetected = true; + } + } + } + } + + return {!collisionDetected && firstAndLastGlyph && inGrid, entirelyOffscreen}; +} + + +void CollisionIndex::insertFeature(CollisionFeature& feature, bool ignorePlacement) { + if (feature.alongLine) { + for (auto& circle : feature.boxes) { + if (!circle.used) { + continue; + } + + if (ignorePlacement) { + ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ circle.px, circle.py }, circle.radius}); + } else { + collisionGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ circle.px, circle.py }, circle.radius}); + } + } + } else { + assert(feature.boxes.size() == 1); + auto& box = feature.boxes[0]; + if (ignorePlacement) { + ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ box.px1, box.py1 }, { box.px2, box.py2 }}); + } else { + collisionGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ box.px1, box.py1 }, { box.px2, box.py2 }}); + } + } +} + +bool polygonIntersectsBox(const LineString<float>& polygon, const GridIndex<IndexedSubfeature>::BBox& bbox) { + // This is just a wrapper that allows us to use the integer-based util::polygonIntersectsPolygon + // Conversion limits our query accuracy to single-pixel resolution + GeometryCoordinates integerPolygon; + for (const auto& point : polygon) { + integerPolygon.push_back(convertPoint<int16_t>(point)); + } + int16_t minX1 = bbox.min.x; + int16_t maxY1 = bbox.max.y; + int16_t minY1 = bbox.min.y; + int16_t maxX1 = bbox.max.x; + + auto bboxPoints = GeometryCoordinates { + { minX1, minY1 }, { maxX1, minY1 }, { maxX1, maxY1 }, { minX1, maxY1 } + }; + + return util::polygonIntersectsPolygon(integerPolygon, bboxPoints); +} + +std::vector<IndexedSubfeature> CollisionIndex::queryRenderedSymbols(const GeometryCoordinates& queryGeometry, const UnwrappedTileID& tileID, const std::string& sourceID) const { + std::vector<IndexedSubfeature> result; + if (queryGeometry.empty() || (collisionGrid.empty() && ignoredGrid.empty())) { + return result; + } + + mat4 posMatrix; + mat4 projMatrix; + transformState.getProjMatrix(projMatrix); + transformState.matrixFor(posMatrix, tileID); + matrix::multiply(posMatrix, projMatrix, posMatrix); + + // queryGeometry is specified in integer tile units, but in projecting we switch to float pixels + LineString<float> projectedQuery; + for (const auto& point : queryGeometry) { + auto projected = projectPoint(posMatrix, convertPoint<float>(point)); + projectedQuery.push_back(projected); + } + + auto envelope = mapbox::geometry::envelope(projectedQuery); + + using QueryResult = std::pair<IndexedSubfeature, GridIndex<IndexedSubfeature>::BBox>; + + std::vector<QueryResult> thisTileFeatures; + std::vector<QueryResult> features = collisionGrid.queryWithBoxes(envelope); + + for (auto& queryResult : features) { + auto& feature = queryResult.first; + if (feature.sourceID == sourceID && feature.tileID == tileID.canonical) { + // We only have to filter on the canonical ID because even if the feature is showing multiple times + // we treat it as one feature. + thisTileFeatures.push_back(queryResult); + } + } + + std::vector<QueryResult> ignoredFeatures = ignoredGrid.queryWithBoxes(envelope); + for (auto& queryResult : ignoredFeatures) { + auto& feature = queryResult.first; + if (feature.sourceID == sourceID && feature.tileID == tileID.canonical) { + thisTileFeatures.push_back(queryResult); + } + } + + std::unordered_map<std::string, std::unordered_set<std::size_t>> sourceLayerFeatures; + for (auto& queryResult : thisTileFeatures) { + auto& feature = queryResult.first; + auto& bbox = queryResult.second; + + // Skip already seen features. + auto& seenFeatures = sourceLayerFeatures[feature.sourceLayerName]; + if (seenFeatures.find(feature.index) != seenFeatures.end()) + continue; + + seenFeatures.insert(feature.index); + + if (!polygonIntersectsBox(projectedQuery, bbox)) { + continue; + } + + result.push_back(feature); + } + + return result; + +} + +std::pair<float,float> CollisionIndex::projectAnchor(const mat4& posMatrix, const Point<float>& point) const { + vec4 p = {{ point.x, point.y, 0, 1 }}; + matrix::transformMat4(p, p, posMatrix); + return std::make_pair( + 0.5 + 0.5 * (p[3] / transformState.getCameraToCenterDistance()), + p[3] + ); +} + +std::pair<Point<float>,float> CollisionIndex::projectAndGetPerspectiveRatio(const mat4& posMatrix, const Point<float>& point) const { + vec4 p = {{ point.x, point.y, 0, 1 }}; + matrix::transformMat4(p, p, posMatrix); + return std::make_pair( + Point<float>( + (((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding, + (((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding + ), + 0.5 + 0.5 * (p[3] / transformState.getCameraToCenterDistance()) + ); +} + +Point<float> CollisionIndex::projectPoint(const mat4& posMatrix, const Point<float>& point) const { + vec4 p = {{ point.x, point.y, 0, 1 }}; + matrix::transformMat4(p, p, posMatrix); + return Point<float>( + (((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding, + (((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding + ); +} + +} // namespace mbgl diff --git a/src/mbgl/text/collision_index.hpp b/src/mbgl/text/collision_index.hpp new file mode 100644 index 0000000000..8653c1d76c --- /dev/null +++ b/src/mbgl/text/collision_index.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include <mbgl/geometry/feature_index.hpp> +#include <mbgl/text/collision_feature.hpp> +#include <mbgl/util/grid_index.hpp> +#include <mbgl/map/transform_state.hpp> + +namespace mbgl { + +class PlacedSymbol; + +struct TileDistance; + +class CollisionIndex { +public: + using CollisionGrid = GridIndex<IndexedSubfeature>; + + explicit CollisionIndex(const TransformState&); + + std::pair<bool,bool> placeFeature(CollisionFeature& feature, + const mat4& posMatrix, + const mat4& labelPlaneMatrix, + const float textPixelRatio, + PlacedSymbol& symbol, + const float scale, + const float fontSize, + const bool allowOverlap, + const bool pitchWithMap, + const bool collisionDebug); + + void insertFeature(CollisionFeature& feature, bool ignorePlacement); + + std::vector<IndexedSubfeature> queryRenderedSymbols(const GeometryCoordinates&, const UnwrappedTileID& tileID, const std::string& sourceID) const; + + +private: + bool isOffscreen(const CollisionBox&) const; + bool isInsideGrid(const CollisionBox&) const; + + std::pair<bool,bool> placeLineFeature(CollisionFeature& feature, + const mat4& posMatrix, + const mat4& labelPlaneMatrix, + const float textPixelRatio, + PlacedSymbol& symbol, + const float scale, + const float fontSize, + const bool allowOverlap, + const bool pitchWithMap, + const bool collisionDebug); + + float approximateTileDistance(const TileDistance& tileDistance, const float lastSegmentAngle, const float pixelsToTileUnits, const float cameraToAnchorDistance, const bool pitchWithMap); + + std::pair<float,float> projectAnchor(const mat4& posMatrix, const Point<float>& point) const; + std::pair<Point<float>,float> projectAndGetPerspectiveRatio(const mat4& posMatrix, const Point<float>& point) const; + Point<float> projectPoint(const mat4& posMatrix, const Point<float>& point) const; + + const TransformState transformState; + + CollisionGrid collisionGrid; + CollisionGrid ignoredGrid; + + const float screenRightBoundary; + const float screenBottomBoundary; + const float gridRightBoundary; + const float gridBottomBoundary; + + const float pitchFactor; +}; + +} // namespace mbgl diff --git a/src/mbgl/text/collision_tile.cpp b/src/mbgl/text/collision_tile.cpp deleted file mode 100644 index cc9b602f08..0000000000 --- a/src/mbgl/text/collision_tile.cpp +++ /dev/null @@ -1,267 +0,0 @@ -#include <mbgl/text/collision_tile.hpp> -#include <mbgl/geometry/feature_index.hpp> -#include <mbgl/math/log2.hpp> -#include <mbgl/util/constants.hpp> -#include <mbgl/util/math.hpp> -#include <mbgl/math/minmax.hpp> -#include <mbgl/util/intersection_tests.hpp> - -#include <mapbox/geometry/envelope.hpp> -#include <mapbox/geometry/multi_point.hpp> - -#include <cmath> - -namespace mbgl { - -CollisionTile::CollisionTile(PlacementConfig config_) : config(std::move(config_)) { - // Compute the transformation matrix. - const float angle_sin = std::sin(config.angle); - const float angle_cos = std::cos(config.angle); - rotationMatrix = { { angle_cos, -angle_sin, angle_sin, angle_cos } }; - reverseRotationMatrix = { { angle_cos, angle_sin, -angle_sin, angle_cos } }; - - perspectiveRatio = - 1.0f + - 0.5f * (util::division(config.cameraToTileDistance, config.cameraToCenterDistance, 1.0f) - - 1.0f); - - minScale /= perspectiveRatio; - maxScale /= perspectiveRatio; - - // We can only approximate here based on the y position of the tile - // The shaders calculate a more accurate "incidence_stretch" - // at render time to calculate an effective scale for collision - // purposes, but we still want to use the yStretch approximation - // here because we can't adjust the aspect ratio of the collision - // boxes at render time. - yStretch = util::max( - 1.0f, util::division(config.cameraToTileDistance, - config.cameraToCenterDistance * std::cos(config.pitch), 1.0f)); -} - -float CollisionTile::findPlacementScale(const Point<float>& anchor, const CollisionBox& box, const float boxMaxScale, const Point<float>& blockingAnchor, const CollisionBox& blocking) { - float minPlacementScale = minScale; - - // Find the lowest scale at which the two boxes can fit side by side without overlapping. - // Original algorithm: - - const float s1 = util::division(blocking.x1 - box.x2, anchor.x - blockingAnchor.x, - 1.0f); // scale at which new box is to the left of old box - const float s2 = util::division(blocking.x2 - box.x1, anchor.x - blockingAnchor.x, - 1.0f); // scale at which new box is to the right of old box - const float s3 = util::division((blocking.y1 - box.y2) * yStretch, anchor.y - blockingAnchor.y, - 1.0f); // scale at which new box is to the top of old box - const float s4 = util::division((blocking.y2 - box.y1) * yStretch, anchor.y - blockingAnchor.y, - 1.0f); // scale at which new box is to the bottom of old box - - float collisionFreeScale = util::min(util::max(s1, s2), util::max(s3, s4)); - - if (collisionFreeScale > blocking.maxScale) { - // After a box's maxScale the label has shrunk enough that the box is no longer needed to cover it, - // so unblock the new box at the scale that the old box disappears. - collisionFreeScale = blocking.maxScale; - } - - if (collisionFreeScale > boxMaxScale) { - // If the box can only be shown after it is visible, then the box can never be shown. - // But the label can be shown after this box is not visible. - collisionFreeScale = boxMaxScale; - } - - if (collisionFreeScale > minPlacementScale && - collisionFreeScale >= blocking.placementScale) { - // If this collision occurs at a lower scale than previously found collisions - // and the collision occurs while the other label is visible - - // this this is the lowest scale at which the label won't collide with anything - minPlacementScale = collisionFreeScale; - } - - return minPlacementScale; -} - -float CollisionTile::placeFeature(const CollisionFeature& feature, bool allowOverlap, bool avoidEdges) { - static const float infinity = std::numeric_limits<float>::infinity(); - static const std::array<CollisionBox, 4> edges {{ - // left - CollisionBox(Point<float>(0, 0), { 0, 0 }, 0, -infinity, 0, infinity, infinity), - // right - CollisionBox(Point<float>(util::EXTENT, 0), { 0, 0 }, 0, -infinity, 0, infinity, infinity), - // top - CollisionBox(Point<float>(0, 0), { 0, 0 }, -infinity, 0, infinity, 0, infinity), - // bottom - CollisionBox(Point<float>(0, util::EXTENT), { 0, 0 }, -infinity, 0, infinity, 0, infinity) - }}; - - float minPlacementScale = minScale; - - for (auto& box : feature.boxes) { - const auto anchor = util::matrixMultiply(rotationMatrix, box.anchor); - - const float boxMaxScale = box.adjustedMaxScale(rotationMatrix, yStretch); - - if (!allowOverlap) { - for (auto it = tree.qbegin(bgi::intersects(getTreeBox(anchor, box))); it != tree.qend(); ++it) { - const CollisionBox& blocking = std::get<1>(*it); - Point<float> blockingAnchor = util::matrixMultiply(rotationMatrix, blocking.anchor); - - minPlacementScale = util::max(minPlacementScale, findPlacementScale(anchor, box, boxMaxScale, blockingAnchor, blocking)); - if (minPlacementScale >= maxScale) return minPlacementScale; - } - } - - if (avoidEdges) { - const Point<float> rtl = util::matrixMultiply(reverseRotationMatrix, { box.x1, box.y1 }); - const Point<float> rtr = util::matrixMultiply(reverseRotationMatrix, { box.x2, box.y1 }); - const Point<float> rbl = util::matrixMultiply(reverseRotationMatrix, { box.x1, box.y2 }); - const Point<float> rbr = util::matrixMultiply(reverseRotationMatrix, { box.x2, box.y2 }); - CollisionBox rotatedBox(box.anchor, - box.offset, - util::min(rtl.x, rtr.x, rbl.x, rbr.x), - util::min(rtl.y, rtr.y, rbl.y, rbr.y), - util::max(rtl.x, rtr.x, rbl.x, rbr.x), - util::max(rtl.y, rtr.y, rbl.y, rbr.y), - boxMaxScale); - - for (auto& blocking : edges) { - minPlacementScale = util::max(minPlacementScale, findPlacementScale(box.anchor, rotatedBox, boxMaxScale, blocking.anchor, blocking)); - if (minPlacementScale >= maxScale) return minPlacementScale; - } - } - } - - return minPlacementScale; -} - -void CollisionTile::insertFeature(CollisionFeature& feature, float minPlacementScale, bool ignorePlacement) { - for (auto& box : feature.boxes) { - box.placementScale = minPlacementScale; - } - - if (minPlacementScale < maxScale) { - std::vector<CollisionTreeBox> treeBoxes; - for (auto& box : feature.boxes) { - CollisionBox adjustedBox = box; - box.maxScale = box.adjustedMaxScale(rotationMatrix, yStretch); - treeBoxes.emplace_back(getTreeBox(util::matrixMultiply(rotationMatrix, box.anchor), box), std::move(adjustedBox), feature.indexedFeature); - } - if (ignorePlacement) { - ignoredTree.insert(treeBoxes.begin(), treeBoxes.end()); - } else { - tree.insert(treeBoxes.begin(), treeBoxes.end()); - } - } - -} - -// +---------------------------+ As you zoom, the size of the symbol changes -// |(x1,y1) | | relative to the tile e.g. when zooming in, -// | | | the symbol gets smaller relative to the tile. -// | (x1',y1') v | -// | +-------+-------+ | The boxes inserted into the tree represents -// | | | | | the bounds at the integer zoom level (where -// | | | | | the symbol is biggest relative to the tile). -// | | | | | -// |---->+-------+-------+<----| This happens because placement is updated -// | | |(xa,ya)| | once every new integer zoom level e.g. -// | | | | | std::floor(oldZoom) != std::floor(newZoom). -// | | | | | -// | +-------+-------+ | Thus, they don't represent the exact bounds -// | ^ (x2',y2') | of the symbol at the current zoom level. For -// | | | calculating the bounds at current zoom level -// | | (x2,y2)| we must unscale the box using its center as -// +---------------------------+ transform origin. -Box CollisionTile::getTreeBox(const Point<float>& anchor, const CollisionBox& box, const float scale) { - assert(box.x1 <= box.x2 && box.y1 <= box.y2); - return Box{ - // When the 'perspectiveRatio' is high, we're effectively underzooming - // the tile because it's in the distance. - // In order to detect collisions that only happen while underzoomed, - // we have to query a larger portion of the grid. - // This extra work is offset by having a lower 'maxScale' bound - // Note that this adjustment ONLY affects the bounding boxes - // in the grid. It doesn't affect the boxes used for the - // minPlacementScale calculations. - CollisionPoint{ - anchor.x + box.x1 / scale * perspectiveRatio, - anchor.y + box.y1 / scale * yStretch * perspectiveRatio, - }, - CollisionPoint{ - anchor.x + box.x2 / scale * perspectiveRatio, - anchor.y + box.y2 / scale * yStretch * perspectiveRatio - } - }; -} - -std::vector<IndexedSubfeature> CollisionTile::queryRenderedSymbols(const GeometryCoordinates& queryGeometry, float scale) const { - std::vector<IndexedSubfeature> result; - if (queryGeometry.empty() || (tree.empty() && ignoredTree.empty())) { - return result; - } - - // Generate a rotated geometry out of the original query geometry. - // Scale has already been handled by the prior conversions. - GeometryCoordinates polygon; - for (const auto& point : queryGeometry) { - auto rotated = util::matrixMultiply(rotationMatrix, convertPoint<float>(point)); - polygon.push_back(convertPoint<int16_t>(rotated)); - } - - // Predicate for ruling out already seen features. - std::unordered_map<std::string, std::unordered_set<std::size_t>> sourceLayerFeatures; - auto seenFeature = [&] (const CollisionTreeBox& treeBox) -> bool { - const IndexedSubfeature& feature = std::get<2>(treeBox); - const auto& seenFeatures = sourceLayerFeatures[feature.sourceLayerName]; - return seenFeatures.find(feature.index) == seenFeatures.end(); - }; - - // "perspectiveRatio" is a tile-based approximation of how much larger symbols will - // be in the distance. It won't line up exactly with the actually rendered symbols - // Being exact would require running the collision detection logic in symbol_sdf.vertex - // in the CPU - const float perspectiveScale = scale / perspectiveRatio; - - // Account for the rounding done when updating symbol shader variables. - const float roundedScale = std::pow(2.0f, std::ceil(util::log2(perspectiveScale) * 10.0f) / 10.0f); - - // Check if feature is rendered (collision free) at current scale. - auto visibleAtScale = [&] (const CollisionTreeBox& treeBox) -> bool { - const CollisionBox& box = std::get<1>(treeBox); - return roundedScale >= box.placementScale && roundedScale <= box.adjustedMaxScale(rotationMatrix, yStretch); - }; - - // Check if query polygon intersects with the feature box at current scale. - auto intersectsAtScale = [&] (const CollisionTreeBox& treeBox) -> bool { - const CollisionBox& collisionBox = std::get<1>(treeBox); - const auto anchor = util::matrixMultiply(rotationMatrix, collisionBox.anchor); - - const int16_t x1 = anchor.x + (collisionBox.x1 / perspectiveScale); - const int16_t y1 = anchor.y + (collisionBox.y1 / perspectiveScale) * yStretch; - const int16_t x2 = anchor.x + (collisionBox.x2 / perspectiveScale); - const int16_t y2 = anchor.y + (collisionBox.y2 / perspectiveScale) * yStretch; - auto bbox = GeometryCoordinates { - { x1, y1 }, { x2, y1 }, { x2, y2 }, { x1, y2 } - }; - return util::polygonIntersectsPolygon(polygon, bbox); - }; - - auto predicates = bgi::satisfies(seenFeature) - && bgi::satisfies(visibleAtScale) - && bgi::satisfies(intersectsAtScale); - - auto queryTree = [&](const auto& tree_) { - for (auto it = tree_.qbegin(predicates); it != tree_.qend(); ++it) { - const IndexedSubfeature& feature = std::get<2>(*it); - auto& seenFeatures = sourceLayerFeatures[feature.sourceLayerName]; - seenFeatures.insert(feature.index); - result.push_back(feature); - } - }; - - queryTree(tree); - queryTree(ignoredTree); - - return result; -} - -} // namespace mbgl diff --git a/src/mbgl/text/collision_tile.hpp b/src/mbgl/text/collision_tile.hpp deleted file mode 100644 index 9868266aa2..0000000000 --- a/src/mbgl/text/collision_tile.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include <mbgl/text/collision_feature.hpp> -#include <mbgl/text/placement_config.hpp> -#include <mbgl/tile/geometry_tile_data.hpp> - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-function" -#pragma GCC diagnostic ignored "-Wunused-parameter" -#pragma GCC diagnostic ignored "-Wunused-variable" -#pragma GCC diagnostic ignored "-Wshadow" -#ifdef __clang__ -#pragma GCC diagnostic ignored "-Wunknown-pragmas" -#endif -#pragma GCC diagnostic ignored "-Wpragmas" -#pragma GCC diagnostic ignored "-Wdeprecated-register" -#pragma GCC diagnostic ignored "-Wshorten-64-to-32" -#pragma GCC diagnostic ignored "-Wunused-local-typedefs" -#ifndef __clang__ -#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" -#pragma GCC diagnostic ignored "-Wmisleading-indentation" -#endif -#include <boost/geometry.hpp> -#include <boost/geometry/geometries/point.hpp> -#include <boost/geometry/geometries/box.hpp> -#include <boost/geometry/index/rtree.hpp> -#pragma GCC diagnostic pop - -namespace mbgl { - -namespace bg = boost::geometry; -namespace bgm = bg::model; -namespace bgi = bg::index; -using CollisionPoint = bgm::point<float, 2, bg::cs::cartesian>; -using Box = bgm::box<CollisionPoint>; -using CollisionTreeBox = std::tuple<Box, CollisionBox, IndexedSubfeature>; -using Tree = bgi::rtree<CollisionTreeBox, bgi::linear<16, 4>>; - -class IndexedSubfeature; - -class CollisionTile { -public: - explicit CollisionTile(PlacementConfig); - - float placeFeature(const CollisionFeature&, bool allowOverlap, bool avoidEdges); - void insertFeature(CollisionFeature&, float minPlacementScale, bool ignorePlacement); - - std::vector<IndexedSubfeature> queryRenderedSymbols(const GeometryCoordinates&, float scale) const; - - const PlacementConfig config; - - float minScale = 0.5f; - float maxScale = 2.0f; - float yStretch; - - std::array<float, 4> rotationMatrix; - std::array<float, 4> reverseRotationMatrix; - -private: - float findPlacementScale( - const Point<float>& anchor, const CollisionBox& box, const float boxMaxScale, - const Point<float>& blockingAnchor, const CollisionBox& blocking); - Box getTreeBox(const Point<float>& anchor, const CollisionBox& box, const float scale = 1.0); - - Tree tree; - Tree ignoredTree; - - float perspectiveRatio; -}; - -} // namespace mbgl diff --git a/src/mbgl/text/cross_tile_symbol_index.cpp b/src/mbgl/text/cross_tile_symbol_index.cpp new file mode 100644 index 0000000000..177615857f --- /dev/null +++ b/src/mbgl/text/cross_tile_symbol_index.cpp @@ -0,0 +1,165 @@ +#include <mbgl/text/cross_tile_symbol_index.hpp> +#include <mbgl/layout/symbol_instance.hpp> +#include <mbgl/renderer/buckets/symbol_bucket.hpp> +#include <mbgl/renderer/render_tile.hpp> +#include <mbgl/tile/tile.hpp> + +namespace mbgl { + + +TileLayerIndex::TileLayerIndex(OverscaledTileID coord_, std::vector<SymbolInstance>& symbolInstances, uint32_t bucketInstanceId_) + : coord(coord_), bucketInstanceId(bucketInstanceId_) { + for (SymbolInstance& symbolInstance : symbolInstances) { + indexedSymbolInstances[symbolInstance.key].emplace_back(symbolInstance.crossTileID, getScaledCoordinates(symbolInstance, coord)); + } + } + +Point<int64_t> TileLayerIndex::getScaledCoordinates(SymbolInstance& symbolInstance, const OverscaledTileID& childTileCoord) { + // Round anchor positions to roughly 4 pixel grid + const double roundingFactor = 512.0 / util::EXTENT / 2.0; + const double scale = roundingFactor / std::pow(2, childTileCoord.canonical.z - coord.canonical.z); + return { + static_cast<int64_t>(std::floor((childTileCoord.canonical.x * util::EXTENT + symbolInstance.anchor.point.x) * scale)), + static_cast<int64_t>(std::floor((childTileCoord.canonical.y * util::EXTENT + symbolInstance.anchor.point.y) * scale)) + }; +} + +void TileLayerIndex::findMatches(std::vector<SymbolInstance>& symbolInstances, const OverscaledTileID& newCoord) { + float tolerance = coord.canonical.z < newCoord.canonical.z ? 1 : std::pow(2, coord.canonical.z - newCoord.canonical.z); + + for (auto& symbolInstance : symbolInstances) { + if (symbolInstance.crossTileID) { + // already has a match, skip + continue; + } + + auto it = indexedSymbolInstances.find(symbolInstance.key); + if (it == indexedSymbolInstances.end()) { + // No symbol with this key in this bucket + continue; + } + + auto scaledSymbolCoord = getScaledCoordinates(symbolInstance, newCoord); + + for (IndexedSymbolInstance& thisTileSymbol: it->second) { + // Return any symbol with the same keys whose coordinates are within 1 + // grid unit. (with a 4px grid, this covers a 12px by 12px area) + if (std::abs(thisTileSymbol.coord.x - scaledSymbolCoord.x) <= tolerance && + std::abs(thisTileSymbol.coord.y - scaledSymbolCoord.y) <= tolerance) { + + symbolInstance.crossTileID = thisTileSymbol.crossTileID; + break; + } + } + } +} + +CrossTileSymbolLayerIndex::CrossTileSymbolLayerIndex() { +} + +void CrossTileSymbolLayerIndex::addBucket(const OverscaledTileID& coord, SymbolBucket& bucket, uint32_t& maxCrossTileID) { + if (bucket.bucketInstanceId) return; + bucket.bucketInstanceId = ++maxBucketInstanceId; + + uint8_t minZoom = 25; + uint8_t maxZoom = 0; + for (auto& it : indexes) { + auto z = it.first; + minZoom = std::min(minZoom, z); + maxZoom = std::max(maxZoom, z); + } + + + // make all higher-res child tiles block duplicate labels in this tile + for (auto z = maxZoom; z > coord.overscaledZ; z--) { + auto zoomIndexes = indexes.find(z); + if (zoomIndexes != indexes.end()) { + for (auto& childIndex : zoomIndexes->second) { + if (!childIndex.second.coord.isChildOf(coord)) { + continue; + } + childIndex.second.findMatches(bucket.symbolInstances, coord); + } + } + if (z == 0) { + break; + } + } + + // make this tile block duplicate labels in lower-res parent tiles + for (auto z = coord.overscaledZ; z >= minZoom; z--) { + auto parentCoord = coord.scaledTo(z); + auto zoomIndexes = indexes.find(z); + if (zoomIndexes != indexes.end()) { + auto parentIndex = zoomIndexes->second.find(parentCoord); + if (parentIndex != zoomIndexes->second.end()) { + parentIndex->second.findMatches(bucket.symbolInstances, coord); + } + } + if (z == 0) { + break; + } + } + + for (auto& symbolInstance : bucket.symbolInstances) { + if (!symbolInstance.crossTileID) { + // symbol did not match any known symbol, assign a new id + symbolInstance.crossTileID = ++maxCrossTileID; + } + } + + indexes[coord.overscaledZ].emplace(coord, TileLayerIndex(coord, bucket.symbolInstances, bucket.bucketInstanceId)); +} + +bool CrossTileSymbolLayerIndex::removeStaleBuckets(const std::unordered_set<uint32_t>& currentIDs) { + bool tilesChanged = false; + for (auto& zoomIndexes : indexes) { + for (auto it = zoomIndexes.second.begin(); it != zoomIndexes.second.end();) { + if (!currentIDs.count(it->second.bucketInstanceId)) { + it = zoomIndexes.second.erase(it); + tilesChanged = true; + } else { + ++it; + } + } + } + return tilesChanged; +} + +CrossTileSymbolIndex::CrossTileSymbolIndex() {} + +bool CrossTileSymbolIndex::addLayer(RenderSymbolLayer& symbolLayer) { + + auto& layerIndex = layerIndexes[symbolLayer.getID()]; + + bool symbolBucketsChanged = false; + std::unordered_set<uint32_t> currentBucketIDs; + + for (RenderTile& renderTile : symbolLayer.renderTiles) { + if (!renderTile.tile.isRenderable()) { + continue; + } + + auto bucket = renderTile.tile.getBucket(*symbolLayer.baseImpl); + assert(dynamic_cast<SymbolBucket*>(bucket)); + SymbolBucket& symbolBucket = *reinterpret_cast<SymbolBucket*>(bucket); + + if (!symbolBucket.bucketInstanceId) { + symbolBucketsChanged = true; + } + layerIndex.addBucket(renderTile.tile.id, symbolBucket, maxCrossTileID); + currentBucketIDs.insert(symbolBucket.bucketInstanceId); + } + + if (layerIndex.removeStaleBuckets(currentBucketIDs)) { + symbolBucketsChanged = true; + } + return symbolBucketsChanged; +} + +void CrossTileSymbolIndex::reset() { + layerIndexes.clear(); +} + +} // namespace mbgl + diff --git a/src/mbgl/text/cross_tile_symbol_index.hpp b/src/mbgl/text/cross_tile_symbol_index.hpp new file mode 100644 index 0000000000..c67cd37e00 --- /dev/null +++ b/src/mbgl/text/cross_tile_symbol_index.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include <mbgl/tile/tile_id.hpp> +#include <mbgl/util/geometry.hpp> +#include <mbgl/util/constants.hpp> +#include <mbgl/util/optional.hpp> + +#include <map> +#include <vector> +#include <string> +#include <memory> +#include <unordered_set> + +namespace mbgl { + +class SymbolInstance; +class RenderSymbolLayer; +class SymbolBucket; + +class IndexedSymbolInstance { +public: + IndexedSymbolInstance(uint32_t crossTileID_, Point<int64_t> coord_) + : crossTileID(crossTileID_), coord(coord_) + {} + + uint32_t crossTileID; + Point<int64_t> coord; +}; + +class TileLayerIndex { +public: + TileLayerIndex(OverscaledTileID coord, std::vector<SymbolInstance>&, uint32_t bucketInstanceId); + + Point<int64_t> getScaledCoordinates(SymbolInstance&, const OverscaledTileID&); + void findMatches(std::vector<SymbolInstance>&, const OverscaledTileID&); + + OverscaledTileID coord; + uint32_t bucketInstanceId; + std::map<std::u16string,std::vector<IndexedSymbolInstance>> indexedSymbolInstances; +}; + +class CrossTileSymbolLayerIndex { +public: + CrossTileSymbolLayerIndex(); + void addBucket(const OverscaledTileID&, SymbolBucket&, uint32_t& maxCrossTileID); + bool removeStaleBuckets(const std::unordered_set<uint32_t>& currentIDs); +private: + std::map<uint8_t,std::map<OverscaledTileID,TileLayerIndex>> indexes; + uint32_t maxBucketInstanceId = 0; +}; + +class CrossTileSymbolIndex { +public: + CrossTileSymbolIndex(); + + bool addLayer(RenderSymbolLayer&); + + void reset(); +private: + std::map<std::string, CrossTileSymbolLayerIndex> layerIndexes; + uint32_t maxCrossTileID = 0; +}; + +} // namespace mbgl diff --git a/src/mbgl/text/glyph.hpp b/src/mbgl/text/glyph.hpp index 6cccb72ebe..08ff82a20a 100644 --- a/src/mbgl/text/glyph.hpp +++ b/src/mbgl/text/glyph.hpp @@ -75,10 +75,10 @@ class Shaping { explicit Shaping(float x, float y, WritingModeType writingMode_) : top(y), bottom(y), left(x), right(x), writingMode(writingMode_) {} std::vector<PositionedGlyph> positionedGlyphs; - int32_t top = 0; - int32_t bottom = 0; - int32_t left = 0; - int32_t right = 0; + float top = 0; + float bottom = 0; + float left = 0; + float right = 0; WritingModeType writingMode; explicit operator bool() const { return !positionedGlyphs.empty(); } diff --git a/src/mbgl/text/placement.cpp b/src/mbgl/text/placement.cpp new file mode 100644 index 0000000000..9284e213c2 --- /dev/null +++ b/src/mbgl/text/placement.cpp @@ -0,0 +1,332 @@ +#include <mbgl/text/placement.hpp> +#include <mbgl/renderer/render_layer.hpp> +#include <mbgl/renderer/layers/render_symbol_layer.hpp> +#include <mbgl/renderer/render_tile.hpp> +#include <mbgl/tile/geometry_tile.cpp> +#include <mbgl/renderer/buckets/symbol_bucket.hpp> +#include <mbgl/renderer/bucket.hpp> + +namespace mbgl { + +OpacityState::OpacityState(bool placed_, bool offscreen) + : opacity((offscreen && placed_) ? 1 : 0) + , placed(placed_) +{ +} + +OpacityState::OpacityState(const OpacityState& prevState, float increment, bool placed_) : + opacity(std::fmax(0, std::fmin(1, prevState.opacity + (prevState.placed ? increment : -increment)))), + placed(placed_) {} + +bool OpacityState::isHidden() const { + return opacity == 0 && !placed; +} + +JointOpacityState::JointOpacityState(bool placedIcon, bool placedText, bool offscreen) : + icon(OpacityState(placedIcon, offscreen)), + text(OpacityState(placedText, offscreen)) {} + +JointOpacityState::JointOpacityState(const JointOpacityState& prevOpacityState, float increment, bool placedIcon, bool placedText) : + icon(OpacityState(prevOpacityState.icon, increment, placedIcon)), + text(OpacityState(prevOpacityState.text, increment, placedText)) {} + +bool JointOpacityState::isHidden() const { + return icon.isHidden() && text.isHidden(); +} + +Placement::Placement(const TransformState& state_, MapMode mapMode_) + : collisionIndex(state_) + , state(state_) + , mapMode(mapMode_) + , recentUntil(TimePoint::min()) +{} + +void Placement::placeLayer(RenderSymbolLayer& symbolLayer, const mat4& projMatrix, bool showCollisionBoxes) { + + std::unordered_set<uint32_t> seenCrossTileIDs; + + for (RenderTile& renderTile : symbolLayer.renderTiles) { + if (!renderTile.tile.isRenderable()) { + continue; + } + + auto bucket = renderTile.tile.getBucket(*symbolLayer.baseImpl); + assert(dynamic_cast<SymbolBucket*>(bucket)); + SymbolBucket& symbolBucket = *reinterpret_cast<SymbolBucket*>(bucket); + + auto& layout = symbolBucket.layout; + + const float pixelsToTileUnits = renderTile.id.pixelsToTileUnits(1, state.getZoom()); + + const float scale = std::pow(2, state.getZoom() - renderTile.tile.id.overscaledZ); + const float textPixelRatio = util::EXTENT / (util::tileSize * renderTile.tile.id.overscaleFactor()); + + mat4 posMatrix; + state.matrixFor(posMatrix, renderTile.id); + matrix::multiply(posMatrix, projMatrix, posMatrix); + + mat4 textLabelPlaneMatrix = getLabelPlaneMatrix(posMatrix, + layout.get<TextPitchAlignment>() == style::AlignmentType::Map, + layout.get<TextRotationAlignment>() == style::AlignmentType::Map, + state, + pixelsToTileUnits); + + mat4 iconLabelPlaneMatrix = getLabelPlaneMatrix(posMatrix, + layout.get<IconPitchAlignment>() == style::AlignmentType::Map, + layout.get<IconRotationAlignment>() == style::AlignmentType::Map, + state, + pixelsToTileUnits); + + placeLayerBucket(symbolBucket, posMatrix, textLabelPlaneMatrix, iconLabelPlaneMatrix, scale, textPixelRatio, showCollisionBoxes, seenCrossTileIDs, renderTile.tile.holdForFade()); + } +} + +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) { + + auto partiallyEvaluatedTextSize = bucket.textSizeBinder->evaluateForZoom(state.getZoom()); + auto partiallyEvaluatedIconSize = bucket.iconSizeBinder->evaluateForZoom(state.getZoom()); + + const bool iconWithoutText = !bucket.hasTextData() || bucket.layout.get<TextOptional>(); + const bool textWithoutIcon = !bucket.hasIconData() || bucket.layout.get<IconOptional>(); + + 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<TextAllowOverlap>(), + bucket.layout.get<TextPitchAlignment>() == style::AlignmentType::Map, + showCollisionBoxes); + 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<IconAllowOverlap>(), + bucket.layout.get<IconPitchAlignment>() == style::AlignmentType::Map, + showCollisionBoxes); + placeIcon = placed.first; + offscreen &= placed.second; + } + + // 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<TextIgnorePlacement>()); + } + + if (placeIcon) { + collisionIndex.insertFeature(symbolInstance.iconCollisionFeature, bucket.layout.get<IconIgnorePlacement>()); + } + + 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, placeIcon, offscreen)); + seenCrossTileIDs.insert(symbolInstance.crossTileID); + } + } +} + +bool Placement::commit(const Placement& prevPlacement, TimePoint now) { + commitTime = now; + + bool placementChanged = false; + + float increment = mapMode == MapMode::Continuous ? + std::chrono::duration<float>(commitTime - prevPlacement.commitTime) / Duration(std::chrono::milliseconds(300)) : + 1.0; + + // add the opacities from the current placement, and copy their current values from the previous placement + for (auto& jointPlacement : placements) { + auto prevOpacity = prevPlacement.opacities.find(jointPlacement.first); + if (prevOpacity != prevPlacement.opacities.end()) { + opacities.emplace(jointPlacement.first, JointOpacityState(prevOpacity->second, increment, jointPlacement.second.icon, jointPlacement.second.text)); + placementChanged = placementChanged || + jointPlacement.second.icon != prevOpacity->second.icon.placed || + jointPlacement.second.text != prevOpacity->second.text.placed; + } else { + opacities.emplace(jointPlacement.first, JointOpacityState(jointPlacement.second.icon, jointPlacement.second.text, jointPlacement.second.offscreen)); + placementChanged = placementChanged || jointPlacement.second.icon || jointPlacement.second.text; + } + } + + // copy and update values from the previous placement that aren't in the current placement but haven't finished fading + for (auto& prevOpacity : prevPlacement.opacities) { + if (opacities.find(prevOpacity.first) == opacities.end()) { + JointOpacityState jointOpacity(prevOpacity.second, increment, false, false); + if (!jointOpacity.isHidden()) { + opacities.emplace(prevOpacity.first, jointOpacity); + placementChanged = placementChanged || prevOpacity.second.icon.placed || prevOpacity.second.text.placed; + } + } + } + + return placementChanged; +} + +void Placement::updateLayerOpacities(RenderSymbolLayer& symbolLayer) { + std::set<uint32_t> seenCrossTileIDs; + for (RenderTile& renderTile : symbolLayer.renderTiles) { + if (!renderTile.tile.isRenderable()) { + continue; + } + + auto bucket = renderTile.tile.getBucket(*symbolLayer.baseImpl); + assert(dynamic_cast<SymbolBucket*>(bucket)); + SymbolBucket& symbolBucket = *reinterpret_cast<SymbolBucket*>(bucket); + updateBucketOpacities(symbolBucket, seenCrossTileIDs); + } +} + +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(); + + for (SymbolInstance& symbolInstance : bucket.symbolInstances) { + auto opacityState = seenCrossTileIDs.count(symbolInstance.crossTileID) == 0 ? + getOpacity(symbolInstance.crossTileID) : + JointOpacityState(false, false, false); + + 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) { + for (const CollisionBox& box : feature.boxes) { + if (feature.alongLine) { + 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); + } else { + auto dynamicVertex = CollisionBoxDynamicAttributes::vertex(placed, false); + bucket.collisionBox.dynamicVertices.emplace_back(dynamicVertex); + bucket.collisionBox.dynamicVertices.emplace_back(dynamicVertex); + bucket.collisionBox.dynamicVertices.emplace_back(dynamicVertex); + bucket.collisionBox.dynamicVertices.emplace_back(dynamicVertex); + } + } + }; + updateCollisionBox(symbolInstance.textCollisionFeature, opacityState.text.placed); + updateCollisionBox(symbolInstance.iconCollisionFeature, opacityState.icon.placed); + } + + bucket.updateOpacity(); + bucket.sortFeatures(state.getAngle()); +} + +JointOpacityState Placement::getOpacity(uint32_t crossTileSymbolID) const { + auto it = opacities.find(crossTileSymbolID); + if (it != opacities.end()) { + return it->second; + } else { + return JointOpacityState(false, false, false); + } + +} + +float Placement::symbolFadeChange(TimePoint now) const { + if (mapMode == MapMode::Continuous) { + return std::chrono::duration<float>(now - commitTime) / Duration(std::chrono::milliseconds(300)); + } else { + return 1.0; + } +} + +bool Placement::hasTransitions(TimePoint now) const { + return symbolFadeChange(now) < 1.0 || stale; +} + +bool Placement::stillRecent(TimePoint now) const { + return mapMode == MapMode::Continuous && recentUntil > now; +} +void Placement::setRecent(TimePoint now) { + stale = false; + if (mapMode == MapMode::Continuous) { + // Only set in continuous mode because "now" isn't defined in still mode + recentUntil = now + Duration(std::chrono::milliseconds(300)); + } +} + +void Placement::setStale() { + stale = true; +} + +const CollisionIndex& Placement::getCollisionIndex() const { + return collisionIndex; +} + +} // namespace mbgl diff --git a/src/mbgl/text/placement.hpp b/src/mbgl/text/placement.hpp new file mode 100644 index 0000000000..bcc20f15a4 --- /dev/null +++ b/src/mbgl/text/placement.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include <string> +#include <unordered_map> +#include <mbgl/util/chrono.hpp> +#include <mbgl/text/collision_index.hpp> +#include <mbgl/layout/symbol_projection.hpp> +#include <unordered_set> + +namespace mbgl { + +class RenderSymbolLayer; +class SymbolBucket; + +class OpacityState { +public: + OpacityState(bool placed, bool offscreen); + OpacityState(const OpacityState& prevOpacityState, float increment, bool placed); + bool isHidden() const; + float opacity; + bool placed; +}; + +class JointOpacityState { +public: + JointOpacityState(bool placedIcon, bool placedText, bool offscreen); + JointOpacityState(const JointOpacityState& prevOpacityState, float increment, bool placedIcon, bool placedText); + bool isHidden() const; + OpacityState icon; + OpacityState text; +}; + +class JointPlacement { +public: + JointPlacement(bool text_, bool icon_, bool offscreen_) + : text(text_), icon(icon_), offscreen(offscreen_) + {} + + const bool text; + const bool icon; + // offscreen = outside viewport, but within CollisionIndex::viewportPadding px of the edge + // Because these symbols aren't onscreen yet, we can skip the "fade in" animation, + // and if a subsequent viewport change brings them into view, they'll be fully + // visible right away. + const bool offscreen; +}; + +class Placement { +public: + Placement(const TransformState&, MapMode mapMode); + void placeLayer(RenderSymbolLayer&, const mat4&, bool showCollisionBoxes); + bool commit(const Placement& prevPlacement, TimePoint); + void updateLayerOpacities(RenderSymbolLayer&); + JointOpacityState getOpacity(uint32_t crossTileSymbolID) const; + float symbolFadeChange(TimePoint now) const; + bool hasTransitions(TimePoint now) const; + + const CollisionIndex& getCollisionIndex() const; + + bool stillRecent(TimePoint now) const; + void setRecent(TimePoint now); + void setStale(); +private: + + void placeLayerBucket( + SymbolBucket&, + const mat4& posMatrix, + const mat4& textLabelPlaneMatrix, + const mat4& iconLabelPlaneMatrix, + const float scale, + const float pixelRatio, + const bool showCollisionBoxes, + std::unordered_set<uint32_t>& seenCrossTileIDs, + const bool holdingForFade); + + void updateBucketOpacities(SymbolBucket&, std::set<uint32_t>&); + + CollisionIndex collisionIndex; + + TransformState state; + MapMode mapMode; + TimePoint commitTime; + + std::unordered_map<uint32_t, JointPlacement> placements; + std::unordered_map<uint32_t, JointOpacityState> opacities; + + TimePoint recentUntil; + bool stale = false; +}; + +} // namespace mbgl diff --git a/src/mbgl/text/placement_config.hpp b/src/mbgl/text/placement_config.hpp deleted file mode 100644 index 48b24b5f41..0000000000 --- a/src/mbgl/text/placement_config.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include <mbgl/util/constants.hpp> - -namespace mbgl { - -class PlacementConfig { -public: - PlacementConfig(float angle_ = 0, float pitch_ = 0, float cameraToCenterDistance_ = 0, float cameraToTileDistance_ = 0, bool debug_ = false) - : angle(angle_), pitch(pitch_), cameraToCenterDistance(cameraToCenterDistance_), cameraToTileDistance(cameraToTileDistance_), debug(debug_) { - } - - bool operator==(const PlacementConfig& rhs) const { - return angle == rhs.angle && - pitch == rhs.pitch && - debug == rhs.debug && - ((pitch * util::RAD2DEG < 25) || - (cameraToCenterDistance == rhs.cameraToCenterDistance && cameraToTileDistance == rhs.cameraToTileDistance)); - } - - bool operator!=(const PlacementConfig& rhs) const { - return !operator==(rhs); - } - -public: - float angle; - float pitch; - float cameraToCenterDistance; - float cameraToTileDistance; - bool debug; -}; - -} // namespace mbgl diff --git a/src/mbgl/text/shaping.cpp b/src/mbgl/text/shaping.cpp index 5d688ea539..a8232836b6 100644 --- a/src/mbgl/text/shaping.cpp +++ b/src/mbgl/text/shaping.cpp @@ -313,7 +313,7 @@ void shapeLines(Shaping& shaping, align(shaping, justify, anchorAlign.horizontalAlign, anchorAlign.verticalAlign, maxLineLength, lineHeight, lines.size()); - const uint32_t height = lines.size() * lineHeight; + const float height = lines.size() * lineHeight; // Calculate the bounding box shaping.top += -anchorAlign.verticalAlign * height; |