298 lines
12 KiB
C++
298 lines
12 KiB
C++
#include <mbgl/text/collision.hpp>
|
|
#include <mbgl/text/rotation_range.hpp>
|
|
#include <mbgl/util/math.hpp>
|
|
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
|
#ifndef __clang__
|
|
#pragma GCC diagnostic ignored "-Wunused-local-typedefs"
|
|
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
|
|
#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;
|
|
typedef bgm::point<float, 2, bg::cs::cartesian> Point;
|
|
typedef bgm::box<Point> Box;
|
|
typedef std::pair<Box, PlacementBox> PlacementValue;
|
|
typedef bgi::rtree<PlacementValue, bgi::rstar<16>> Tree;
|
|
}
|
|
|
|
using namespace mbgl;
|
|
|
|
Collision::~Collision() {
|
|
delete ((Tree *)cTree);
|
|
delete ((Tree *)hTree);
|
|
}
|
|
|
|
Collision::Collision(float zoom, float tileExtent, float tileSize, float placementDepth)
|
|
: hTree(new Tree()), // tree for horizontal labels
|
|
cTree(new Tree()), // tree for glyphs from curved labels
|
|
|
|
// tile pixels per screen pixels at the tile's zoom level
|
|
tilePixelRatio(tileExtent / tileSize),
|
|
|
|
zoom(zoom),
|
|
|
|
// Calculate the maximum scale we can go down in our fake-3d rtree so that
|
|
// placement still makes sense. This is calculated so that the minimum
|
|
// placement zoom can be at most 25.5 (we use an unsigned integer x10 to
|
|
// store the minimum zoom).
|
|
//
|
|
// We don't want to place labels all the way to 25.5. This lets too many
|
|
// glyphs be placed, slowing down collision checking. Only place labels if
|
|
// they will show up within the intended zoom range of the tile.
|
|
maxPlacementScale(std::exp(std::log(2) * util::min(3.0f, placementDepth, 25.5f - zoom))) {
|
|
const float m = 4096;
|
|
const float edge = m * tilePixelRatio * 2;
|
|
|
|
// Hack to prevent cross-tile labels
|
|
insert(
|
|
/* glyphs */ {
|
|
GlyphBox{/* box */ CollisionRect{CollisionPoint{-edge, -edge}, CollisionPoint{0, edge}},
|
|
/* minScale */ 0,
|
|
/* padding */ 2},
|
|
GlyphBox{/* box */ CollisionRect{CollisionPoint{-edge, -edge}, CollisionPoint{edge, 0}},
|
|
/* minScale */ 0,
|
|
/* padding */ 2}},
|
|
/* anchor */ CollisionAnchor(0, 0),
|
|
/* placementScale */ 1,
|
|
/* placementRange */ {{M_PI * 2, 0}},
|
|
/* horizontal */ false);
|
|
|
|
insert(
|
|
/* glyphs */ {
|
|
GlyphBox{/* box*/ CollisionRect{CollisionPoint{-edge, 0}, CollisionPoint{edge, edge}},
|
|
/* minScale */ 0,
|
|
/* padding */ 2},
|
|
GlyphBox{/* box */ CollisionRect{CollisionPoint{0, -edge}, CollisionPoint{edge, edge}},
|
|
/* minScale */ 0,
|
|
/* padding */ 2}},
|
|
/* anchor */ CollisionAnchor(m, m),
|
|
/* placementScale */ 1,
|
|
/* placementRange */ {{M_PI * 2, 0}},
|
|
/* horizontal */ false);
|
|
}
|
|
|
|
GlyphBox getMergedGlyphs(const GlyphBoxes &boxes, const CollisionAnchor &anchor) {
|
|
GlyphBox mergedGlyphs;
|
|
const float inf = std::numeric_limits<float>::infinity();
|
|
mergedGlyphs.box = CollisionRect{{inf, inf}, {-inf, -inf}};
|
|
mergedGlyphs.anchor = anchor;
|
|
|
|
CollisionRect &box = mergedGlyphs.box;
|
|
for (const GlyphBox &glyph : boxes) {
|
|
const CollisionRect &gbox = glyph.box;
|
|
box.tl.x = util::min(box.tl.x, gbox.tl.x);
|
|
box.tl.y = util::min(box.tl.y, gbox.tl.y);
|
|
box.br.x = util::max(box.br.x, gbox.br.x);
|
|
box.br.y = util::max(box.br.y, gbox.br.y);
|
|
mergedGlyphs.minScale = util::max(mergedGlyphs.minScale, glyph.minScale);
|
|
}
|
|
|
|
return mergedGlyphs;
|
|
}
|
|
|
|
Box getBox(const CollisionAnchor &anchor, const CollisionRect &bbox, float minScale,
|
|
float maxScale) {
|
|
return Box{
|
|
Point{
|
|
anchor.x + util::min(bbox.tl.x / minScale, bbox.tl.x / maxScale),
|
|
anchor.y + util::min(bbox.tl.y / minScale, bbox.tl.y / maxScale),
|
|
},
|
|
Point{
|
|
anchor.x + util::max(bbox.br.x / minScale, bbox.br.x / maxScale),
|
|
anchor.y + util::max(bbox.br.y / minScale, bbox.br.y / maxScale),
|
|
},
|
|
};
|
|
};
|
|
|
|
float Collision::getPlacementScale(const GlyphBoxes &glyphs, float minPlacementScale) {
|
|
|
|
for (const GlyphBox &glyph : glyphs) {
|
|
const CollisionRect &box = glyph.box;
|
|
const CollisionRect &bbox = glyph.hBox ? glyph.hBox.get() : glyph.box;
|
|
const CollisionAnchor &anchor = glyph.anchor;
|
|
const float pad = glyph.padding;
|
|
|
|
if (anchor.x < 0 || anchor.x > 4096 || anchor.y < 0 || anchor.y > 4096) {
|
|
return 0;
|
|
}
|
|
|
|
float minScale = std::fmax(minPlacementScale, glyph.minScale);
|
|
float maxScale = glyph.maxScale != 0 ? glyph.maxScale : std::numeric_limits<float>::infinity();
|
|
|
|
if (minScale >= maxScale) {
|
|
continue;
|
|
}
|
|
|
|
// Compute the scaled bounding box of the unrotated glyph
|
|
const Box searchBox = getBox(anchor, bbox, minScale, maxScale);
|
|
|
|
std::vector<PlacementValue> blocking;
|
|
((Tree *)cTree)->query(bgi::intersects(searchBox), std::back_inserter(blocking));
|
|
((Tree *)hTree)->query(bgi::intersects(searchBox), std::back_inserter(blocking));
|
|
|
|
if (blocking.size()) {
|
|
const CollisionAnchor &na = anchor; // new anchor
|
|
const CollisionRect &nb = box; // new box
|
|
|
|
for (const PlacementValue &value : blocking) {
|
|
const PlacementBox &placement = std::get<1>(value);
|
|
const CollisionAnchor &oa = placement.anchor; // old anchor
|
|
const CollisionRect &ob = placement.box; // old box
|
|
|
|
// If anchors are identical, we're going to skip the label.
|
|
// NOTE: this isn't right because there can be glyphs with
|
|
// the same anchor but differing box offsets.
|
|
if (na == oa) {
|
|
return 0;
|
|
}
|
|
|
|
// todo: unhardcode the 8 = tileExtent/tileSize
|
|
float padding = std::fmax(pad, placement.padding) * 8.0f;
|
|
|
|
// Original algorithm:
|
|
float s1 = (ob.tl.x - nb.br.x - padding) /
|
|
(na.x - oa.x); // scale at which new box is to the left of old box
|
|
float s2 = (ob.br.x - nb.tl.x + padding) /
|
|
(na.x - oa.x); // scale at which new box is to the right of old box
|
|
float s3 = (ob.tl.y - nb.br.y - padding) /
|
|
(na.y - oa.y); // scale at which new box is to the top of old box
|
|
float s4 = (ob.br.y - nb.tl.y + padding) /
|
|
(na.y - oa.y); // scale at which new box is to the bottom of old box
|
|
|
|
if (isnan(s1) || isnan(s2)) {
|
|
s1 = s2 = 1;
|
|
}
|
|
if (isnan(s3) || isnan(s4)) {
|
|
s3 = s4 = 1;
|
|
}
|
|
|
|
float collisionFreeScale = std::fmin(std::fmax(s1, s2), std::fmax(s3, s4));
|
|
|
|
// Only update label's min scale if the glyph was
|
|
// restricted by a collision
|
|
if (collisionFreeScale > minPlacementScale && collisionFreeScale > minScale &&
|
|
collisionFreeScale < maxScale && collisionFreeScale < placement.maxScale) {
|
|
minPlacementScale = collisionFreeScale;
|
|
}
|
|
|
|
if (minPlacementScale > maxPlacementScale) {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return minPlacementScale;
|
|
}
|
|
|
|
PlacementRange Collision::getPlacementRange(const GlyphBoxes &glyphs, float placementScale,
|
|
bool horizontal) {
|
|
PlacementRange placementRange = {{2.0f * M_PI, 0}};
|
|
|
|
for (const GlyphBox &glyph : glyphs) {
|
|
const CollisionRect &bbox = glyph.hBox ? glyph.hBox.get() : glyph.box;
|
|
const CollisionAnchor &anchor = glyph.anchor;
|
|
|
|
float minPlacedX = anchor.x + bbox.tl.x / placementScale;
|
|
float minPlacedY = anchor.y + bbox.tl.y / placementScale;
|
|
float maxPlacedX = anchor.x + bbox.br.x / placementScale;
|
|
float maxPlacedY = anchor.y + bbox.br.y / placementScale;
|
|
|
|
Box query_box{Point{minPlacedX, minPlacedY}, Point{maxPlacedX, maxPlacedY}};
|
|
|
|
std::vector<PlacementValue> blocking;
|
|
((Tree *)hTree)->query(bgi::intersects(query_box), std::back_inserter(blocking));
|
|
|
|
if (horizontal) {
|
|
((Tree *)cTree)->query(bgi::intersects(query_box), std::back_inserter(blocking));
|
|
}
|
|
|
|
for (const PlacementValue &value : blocking) {
|
|
const Box &s = std::get<0>(value);
|
|
const PlacementBox &b = std::get<1>(value);
|
|
const CollisionRect &bbox2 = b.hBox ? b.hBox.get() : b.box;
|
|
|
|
float x1, x2, y1, y2, intersectX, intersectY;
|
|
|
|
// Adjust and compare bboxes to see if the glyphs might intersect
|
|
if (placementScale > b.placementScale) {
|
|
x1 = b.anchor.x + bbox2.tl.x / placementScale;
|
|
y1 = b.anchor.y + bbox2.tl.y / placementScale;
|
|
x2 = b.anchor.x + bbox2.br.x / placementScale;
|
|
y2 = b.anchor.y + bbox2.br.y / placementScale;
|
|
intersectX = x1 < maxPlacedX && x2 > minPlacedX;
|
|
intersectY = y1 < maxPlacedY && y2 > minPlacedY;
|
|
} else {
|
|
x1 = anchor.x + bbox.tl.x / b.placementScale;
|
|
y1 = anchor.y + bbox.tl.y / b.placementScale;
|
|
x2 = anchor.x + bbox.br.x / b.placementScale;
|
|
y2 = anchor.y + bbox.br.y / b.placementScale;
|
|
|
|
intersectX = x1 < s.max_corner().get<0>() && x2 > s.min_corner().get<0>();
|
|
intersectY = y1 < s.max_corner().get<1>() && y2 > s.min_corner().get<1>();
|
|
}
|
|
|
|
// If they can't intersect, skip more expensive rotation calculation
|
|
if (!(intersectX && intersectY))
|
|
continue;
|
|
|
|
float scale = std::fmax(placementScale, b.placementScale);
|
|
// TODO? glyph.box or glyph.bbox?
|
|
CollisionRange range = rotationRange(glyph, b, scale);
|
|
|
|
placementRange[0] = std::fmin(placementRange[0], range[0]);
|
|
placementRange[1] = std::fmax(placementRange[1], range[1]);
|
|
}
|
|
}
|
|
|
|
return placementRange;
|
|
};
|
|
|
|
void Collision::insert(const GlyphBoxes &glyphs, const CollisionAnchor &anchor,
|
|
float placementScale, const PlacementRange &placementRange,
|
|
bool horizontal) {
|
|
assert(placementScale != std::numeric_limits<float>::infinity());
|
|
|
|
std::vector<PlacementValue> allBounds;
|
|
allBounds.reserve(glyphs.size());
|
|
|
|
for (const GlyphBox &glyph : glyphs) {
|
|
const CollisionRect &box = glyph.box;
|
|
const CollisionRect &bbox = glyph.hBox ? glyph.hBox.get() : glyph.box;
|
|
|
|
const float minScale = util::max(placementScale, glyph.minScale);
|
|
const float maxScale = glyph.maxScale != 0 ? glyph.maxScale : std::numeric_limits<float>::infinity();
|
|
|
|
const Box bounds = getBox(anchor, bbox, minScale, maxScale);
|
|
|
|
PlacementBox placement;
|
|
placement.anchor = anchor;
|
|
placement.box = box;
|
|
if (glyph.hBox) {
|
|
placement.hBox = bbox;
|
|
}
|
|
placement.placementRange = placementRange;
|
|
placement.placementScale = minScale;
|
|
placement.maxScale = maxScale;
|
|
placement.padding = glyph.padding;
|
|
|
|
allBounds.emplace_back(bounds, placement);
|
|
}
|
|
|
|
// Bulk-insert all glyph boxes
|
|
if (horizontal) {
|
|
((Tree *)hTree)->insert(allBounds.begin(), allBounds.end());
|
|
} else {
|
|
((Tree *)cTree)->insert(allBounds.begin(), allBounds.end());
|
|
}
|
|
}
|