#include #include #include #include #include #include #include #include #include #include // For PlacedSymbol: pull out to another location #include #include #include 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) { 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::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; GridUnit px1 = box.px1 = box.x1 / tileToViewport + projectedPoint.first.x; GridUnit py1 = box.py1 = box.y1 / tileToViewport + projectedPoint.first.y; GridUnit px2 = box.px2 = box.x2 / tileToViewport + projectedPoint.first.x; GridUnit py2 = box.py2 = box.y2 / tileToViewport + projectedPoint.first.y; if (!allowOverlap) { if (collisionGrid.hitTest({{ px1, py1 }, { px2, py2 }})) { return false; } } return true; } else { return placeLineFeature(feature, posMatrix, labelPlaneMatrix, textPixelRatio, symbol, scale, fontSize, allowOverlap, pitchWithMap, collisionDebug); } } 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; 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; GridUnit px = circle.px = projectedPoint.x; GridUnit py = circle.py = projectedPoint.y; GridUnit r = circle.radius = radius; if (!allowOverlap) { if (collisionGrid.hitTest({{px, py}, r})) { if (!collisionDebug) { return 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; } void CollisionIndex::insertFeature(CollisionFeature& feature, bool ignorePlacement) { if (feature.alongLine) { for (auto& circle : feature.boxes) { if (!circle.used) { continue; } GridUnit px = circle.px; GridUnit py = circle.py; GridUnit radius = circle.radius; if (ignorePlacement) { // TODO: revisit whether GridIndex should move vs. copy on insert ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ px, py }, radius}); } else { collisionGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ px, py }, radius}); } } } else { for (auto& box : feature.boxes) { // TODO: Iteration is vestigial, there should be only one box GridUnit px1 = box.px1; GridUnit py1 = box.py1; GridUnit px2 = box.px2; GridUnit py2 = box.py2; if (ignorePlacement) { // TODO: revisit whether GridIndex should move vs. copy on insert ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ px1, py1 }, { px2, py2 }}); } else { collisionGrid.insert(IndexedSubfeature(feature.indexedFeature), {{ px1, py1 }, { px2, py2 }}); } } } } std::vector CollisionIndex::queryRenderedSymbols(const GeometryCoordinates& queryGeometry, const CanonicalTileID& tileID, const std::string& sourceID) const { std::vector result; if (queryGeometry.empty() || (collisionGrid.empty() && ignoredGrid.empty())) { return result; } mat4 posMatrix; mat4 projMatrix; transformState.getProjMatrix(projMatrix); transformState.matrixFor(posMatrix, UnwrappedTileID(0, tileID)); // TODO: This probably isn't right, I think it's working right now because both the wrapped and unwrapped version are getting queried, but I haven't thought through why it should work. matrix::multiply(posMatrix, projMatrix, posMatrix); GeometryCoordinates polygon; // todo: use envelope? auto minX = std::numeric_limits::max(); auto minY = std::numeric_limits::max(); auto maxX = std::numeric_limits::min(); auto maxY = std::numeric_limits::min(); for (const auto& point : queryGeometry) { auto projected = projectPoint(posMatrix, convertPoint(point)); GridUnit px = projected.x; GridUnit py = projected.y; minX = std::min(minX, px); minY = std::min(minY, py); maxX = std::max(maxX, px); maxY = std::max(maxY, py); polygon.push_back(convertPoint(projected)); } typedef std::pair::BBox> QueryResult; std::vector thisTileFeatures; std::vector features = collisionGrid.query({{minX, minY}, {maxX, maxY}}); for (auto& queryResult : features) { auto& feature = queryResult.first; CanonicalTileID featureTileID(feature.z, feature.x, feature.y); if (feature.sourceID == sourceID && featureTileID == tileID) { thisTileFeatures.push_back(queryResult); } } std::vector ignoredFeatures = ignoredGrid.query({{minX, minY}, {maxX, maxY}}); for (auto& queryResult : ignoredFeatures) { auto& feature = queryResult.first; CanonicalTileID featureTileID(feature.z, feature.x, feature.y); if (feature.sourceID == sourceID && featureTileID == tileID) { thisTileFeatures.push_back(queryResult); } } std::unordered_map> 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); 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 } }; if (!util::polygonIntersectsPolygon(polygon, bboxPoints)) continue; result.push_back(feature); } return result; } std::pair CollisionIndex::projectAnchor(const mat4& posMatrix, const Point& 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,float> CollisionIndex::projectAndGetPerspectiveRatio(const mat4& posMatrix, const Point& point) const { vec4 p = {{ point.x, point.y, 0, 1 }}; matrix::transformMat4(p, p, posMatrix); return std::make_pair( Point( (((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 CollisionIndex::projectPoint(const mat4& posMatrix, const Point& point) const { vec4 p = {{ point.x, point.y, 0, 1 }}; matrix::transformMat4(p, p, posMatrix); return Point( (((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding, (((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding ); } } // namespace mbgl