1 #include <mbgl/text/collision_index.hpp>
2 #include <mbgl/layout/symbol_instance.hpp>
3 #include <mbgl/geometry/feature_index.hpp>
4 #include <mbgl/math/log2.hpp>
5 #include <mbgl/util/constants.hpp>
6 #include <mbgl/util/math.hpp>
7 #include <mbgl/math/minmax.hpp>
8 #include <mbgl/util/intersection_tests.hpp>
9 #include <mbgl/layout/symbol_projection.hpp>
10
11 #include <mapbox/geometry/envelope.hpp>
12
13 #include <mbgl/renderer/buckets/symbol_bucket.hpp> // For PlacedSymbol: pull out to another location
14
15 #include <cmath>
16
17 namespace mbgl {
18
19 // When a symbol crosses the edge that causes it to be included in
20 // collision detection, it will cause changes in the symbols around
21 // it. This constant specifies how many pixels to pad the edge of
22 // the viewport for collision detection so that the bulk of the changes
23 // occur offscreen. Making this constant greater increases label
24 // stability, but it's expensive.
25 static const float viewportPadding = 100;
26
CollisionIndex(const TransformState & transformState_)27 CollisionIndex::CollisionIndex(const TransformState& transformState_)
28 : transformState(transformState_)
29 , collisionGrid(transformState.getSize().width + 2 * viewportPadding, transformState.getSize().height + 2 * viewportPadding, 25)
30 , ignoredGrid(transformState.getSize().width + 2 * viewportPadding, transformState.getSize().height + 2 * viewportPadding, 25)
31 , screenRightBoundary(transformState.getSize().width + viewportPadding)
32 , screenBottomBoundary(transformState.getSize().height + viewportPadding)
33 , gridRightBoundary(transformState.getSize().width + 2 * viewportPadding)
34 , gridBottomBoundary(transformState.getSize().height + 2 * viewportPadding)
35 , pitchFactor(std::cos(transformState.getPitch()) * transformState.getCameraToCenterDistance())
36 {}
37
approximateTileDistance(const TileDistance & tileDistance,const float lastSegmentAngle,const float pixelsToTileUnits,const float cameraToAnchorDistance,const bool pitchWithMap)38 float CollisionIndex::approximateTileDistance(const TileDistance& tileDistance, const float lastSegmentAngle, const float pixelsToTileUnits, const float cameraToAnchorDistance, const bool pitchWithMap) {
39 // This is a quick and dirty solution for chosing which collision circles to use (since collision circles are
40 // laid out in tile units). Ideally, I think we should generate collision circles on the fly in viewport coordinates
41 // at the time we do collision detection.
42
43 // incidenceStretch is the ratio of how much y space a label takes up on a tile while drawn perpendicular to the viewport vs
44 // how much space it would take up if it were drawn flat on the tile
45 // Using law of sines, camera_to_anchor/sin(ground_angle) = camera_to_center/sin(incidence_angle)
46 // Incidence angle 90 -> head on, sin(incidence_angle) = 1, no stretch
47 // Incidence angle 1 -> very oblique, sin(incidence_angle) =~ 0, lots of stretch
48 // ground_angle = u_pitch + PI/2 -> sin(ground_angle) = cos(u_pitch)
49 // incidenceStretch = 1 / sin(incidenceAngle)
50
51 const float incidenceStretch = pitchWithMap ? 1 : cameraToAnchorDistance / pitchFactor;
52 const float lastSegmentTile = tileDistance.lastSegmentViewportDistance * pixelsToTileUnits;
53 return tileDistance.prevTileDistance +
54 lastSegmentTile +
55 (incidenceStretch - 1) * lastSegmentTile * std::abs(std::sin(lastSegmentAngle));
56 }
57
isOffscreen(const CollisionBox & box) const58 bool CollisionIndex::isOffscreen(const CollisionBox& box) const {
59 return box.px2 < viewportPadding || box.px1 >= screenRightBoundary || box.py2 < viewportPadding || box.py1 >= screenBottomBoundary;
60 }
61
isInsideGrid(const CollisionBox & box) const62 bool CollisionIndex::isInsideGrid(const CollisionBox& box) const {
63 return box.px2 >= 0 && box.px1 < gridRightBoundary && box.py2 >= 0 && box.py1 < gridBottomBoundary;
64 }
65
66
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)67 std::pair<bool,bool> CollisionIndex::placeFeature(CollisionFeature& feature,
68 const mat4& posMatrix,
69 const mat4& labelPlaneMatrix,
70 const float textPixelRatio,
71 PlacedSymbol& symbol,
72 const float scale,
73 const float fontSize,
74 const bool allowOverlap,
75 const bool pitchWithMap,
76 const bool collisionDebug) {
77 if (!feature.alongLine) {
78 CollisionBox& box = feature.boxes.front();
79 const auto projectedPoint = projectAndGetPerspectiveRatio(posMatrix, box.anchor);
80 const float tileToViewport = textPixelRatio * projectedPoint.second;
81 box.px1 = box.x1 * tileToViewport + projectedPoint.first.x;
82 box.py1 = box.y1 * tileToViewport + projectedPoint.first.y;
83 box.px2 = box.x2 * tileToViewport + projectedPoint.first.x;
84 box.py2 = box.y2 * tileToViewport + projectedPoint.first.y;
85
86 if (!isInsideGrid(box) ||
87 (!allowOverlap && collisionGrid.hitTest({{ box.px1, box.py1 }, { box.px2, box.py2 }}))) {
88 return { false, false };
89 }
90
91 return {true, isOffscreen(box)};
92 } else {
93 return placeLineFeature(feature, posMatrix, labelPlaneMatrix, textPixelRatio, symbol, scale, fontSize, allowOverlap, pitchWithMap, collisionDebug);
94 }
95 }
96
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)97 std::pair<bool,bool> CollisionIndex::placeLineFeature(CollisionFeature& feature,
98 const mat4& posMatrix,
99 const mat4& labelPlaneMatrix,
100 const float textPixelRatio,
101 PlacedSymbol& symbol,
102 const float scale,
103 const float fontSize,
104 const bool allowOverlap,
105 const bool pitchWithMap,
106 const bool collisionDebug) {
107
108 const auto tileUnitAnchorPoint = symbol.anchorPoint;
109 const auto projectedAnchor = projectAnchor(posMatrix, tileUnitAnchorPoint);
110
111 const float fontScale = fontSize / 24;
112 const float lineOffsetX = symbol.lineOffset[0] * fontSize;
113 const float lineOffsetY = symbol.lineOffset[1] * fontSize;
114
115 const auto labelPlaneAnchorPoint = project(tileUnitAnchorPoint, labelPlaneMatrix).first;
116
117 const auto firstAndLastGlyph = placeFirstAndLastGlyph(
118 fontScale,
119 lineOffsetX,
120 lineOffsetY,
121 /*flip*/ false,
122 labelPlaneAnchorPoint,
123 tileUnitAnchorPoint,
124 symbol,
125 labelPlaneMatrix,
126 /*return tile distance*/ true);
127
128 bool collisionDetected = false;
129 bool inGrid = false;
130 bool entirelyOffscreen = true;
131
132 const auto tileToViewport = projectedAnchor.first * textPixelRatio;
133 // pixelsToTileUnits is used for translating line geometry to tile units
134 // ... so we care about 'scale' but not 'perspectiveRatio'
135 // equivalent to pixel_to_tile_units
136 const auto pixelsToTileUnits = 1 / (textPixelRatio * scale);
137
138 float firstTileDistance = 0, lastTileDistance = 0;
139 if (firstAndLastGlyph) {
140 firstTileDistance = approximateTileDistance(*(firstAndLastGlyph->first.tileDistance), firstAndLastGlyph->first.angle, pixelsToTileUnits, projectedAnchor.second, pitchWithMap);
141 lastTileDistance = approximateTileDistance(*(firstAndLastGlyph->second.tileDistance), firstAndLastGlyph->second.angle, pixelsToTileUnits, projectedAnchor.second, pitchWithMap);
142 }
143
144 bool atLeastOneCirclePlaced = false;
145 for (size_t i = 0; i < feature.boxes.size(); i++) {
146 CollisionBox& circle = feature.boxes[i];
147 const float boxSignedDistanceFromAnchor = circle.signedDistanceFromAnchor;
148 if (!firstAndLastGlyph ||
149 (boxSignedDistanceFromAnchor < -firstTileDistance) ||
150 (boxSignedDistanceFromAnchor > lastTileDistance)) {
151 // The label either doesn't fit on its line or we
152 // don't need to use this circle because the label
153 // doesn't extend this far. Either way, mark the circle unused.
154 circle.used = false;
155 continue;
156 }
157
158 const auto projectedPoint = projectPoint(posMatrix, circle.anchor);
159 const float tileUnitRadius = (circle.x2 - circle.x1) / 2;
160 const float radius = tileUnitRadius * tileToViewport;
161
162 if (atLeastOneCirclePlaced) {
163 const CollisionBox& previousCircle = feature.boxes[i - 1];
164 const float dx = projectedPoint.x - previousCircle.px;
165 const float dy = projectedPoint.y - previousCircle.py;
166 // The circle edges touch when the distance between their centers is 2x the radius
167 // When the distance is 1x the radius, they're doubled up, and we could remove
168 // every other circle while keeping them all in touch.
169 // We actually start removing circles when the distance is √2x the radius:
170 // thinning the number of circles as much as possible is a major performance win,
171 // and the small gaps introduced don't make a very noticeable difference.
172 const bool placedTooDensely = radius * radius * 2 > dx * dx + dy * dy;
173 if (placedTooDensely) {
174 const bool atLeastOneMoreCircle = (i + 1) < feature.boxes.size();
175 if (atLeastOneMoreCircle) {
176 const CollisionBox& nextCircle = feature.boxes[i + 1];
177 const float nextBoxDistanceFromAnchor = nextCircle.signedDistanceFromAnchor;
178 if ((nextBoxDistanceFromAnchor > -firstTileDistance) &&
179 (nextBoxDistanceFromAnchor < lastTileDistance)) {
180 // Hide significantly overlapping circles, unless this is the last one we can
181 // use, in which case we want to keep it in place even if it's tightly packed
182 // with the one before it.
183 circle.used = false;
184 continue;
185 }
186 }
187 }
188 }
189
190 atLeastOneCirclePlaced = true;
191 circle.px1 = projectedPoint.x - radius;
192 circle.px2 = projectedPoint.x + radius;
193 circle.py1 = projectedPoint.y - radius;
194 circle.py2 = projectedPoint.y + radius;
195
196 circle.used = true;
197
198 circle.px = projectedPoint.x;
199 circle.py = projectedPoint.y;
200 circle.radius = radius;
201
202 entirelyOffscreen &= isOffscreen(circle);
203 inGrid |= isInsideGrid(circle);
204
205 if (!allowOverlap) {
206 if (collisionGrid.hitTest({{circle.px, circle.py}, circle.radius})) {
207 if (!collisionDebug) {
208 return {false, false};
209 } else {
210 // Don't early exit if we're showing the debug circles because we still want to calculate
211 // which circles are in use
212 collisionDetected = true;
213 }
214 }
215 }
216 }
217
218 return {!collisionDetected && firstAndLastGlyph && inGrid, entirelyOffscreen};
219 }
220
221
insertFeature(CollisionFeature & feature,bool ignorePlacement,uint32_t bucketInstanceId)222 void CollisionIndex::insertFeature(CollisionFeature& feature, bool ignorePlacement, uint32_t bucketInstanceId) {
223 if (feature.alongLine) {
224 for (auto& circle : feature.boxes) {
225 if (!circle.used) {
226 continue;
227 }
228
229 if (ignorePlacement) {
230 ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ circle.px, circle.py }, circle.radius});
231 } else {
232 collisionGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ circle.px, circle.py }, circle.radius});
233 }
234 }
235 } else {
236 assert(feature.boxes.size() == 1);
237 auto& box = feature.boxes[0];
238 if (ignorePlacement) {
239 ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ box.px1, box.py1 }, { box.px2, box.py2 }});
240 } else {
241 collisionGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ box.px1, box.py1 }, { box.px2, box.py2 }});
242 }
243 }
244 }
245
polygonIntersectsBox(const LineString<float> & polygon,const GridIndex<IndexedSubfeature>::BBox & bbox)246 bool polygonIntersectsBox(const LineString<float>& polygon, const GridIndex<IndexedSubfeature>::BBox& bbox) {
247 // This is just a wrapper that allows us to use the integer-based util::polygonIntersectsPolygon
248 // Conversion limits our query accuracy to single-pixel resolution
249 GeometryCoordinates integerPolygon;
250 for (const auto& point : polygon) {
251 integerPolygon.push_back(convertPoint<int16_t>(point));
252 }
253 int16_t minX1 = bbox.min.x;
254 int16_t maxY1 = bbox.max.y;
255 int16_t minY1 = bbox.min.y;
256 int16_t maxX1 = bbox.max.x;
257
258 auto bboxPoints = GeometryCoordinates {
259 { minX1, minY1 }, { maxX1, minY1 }, { maxX1, maxY1 }, { minX1, maxY1 }
260 };
261
262 return util::polygonIntersectsPolygon(integerPolygon, bboxPoints);
263 }
264
queryRenderedSymbols(const ScreenLineString & queryGeometry) const265 std::unordered_map<uint32_t, std::vector<IndexedSubfeature>> CollisionIndex::queryRenderedSymbols(const ScreenLineString& queryGeometry) const {
266 std::unordered_map<uint32_t, std::vector<IndexedSubfeature>> result;
267 if (queryGeometry.empty() || (collisionGrid.empty() && ignoredGrid.empty())) {
268 return result;
269 }
270
271 LineString<float> gridQuery;
272 for (const auto& point : queryGeometry) {
273 gridQuery.emplace_back(point.x + viewportPadding, point.y + viewportPadding);
274 }
275
276 auto envelope = mapbox::geometry::envelope(gridQuery);
277
278 using QueryResult = std::pair<IndexedSubfeature, GridIndex<IndexedSubfeature>::BBox>;
279
280 std::vector<QueryResult> features = collisionGrid.queryWithBoxes(envelope);
281 std::vector<QueryResult> ignoredFeatures = ignoredGrid.queryWithBoxes(envelope);
282 features.insert(features.end(), ignoredFeatures.begin(), ignoredFeatures.end());
283
284 std::unordered_map<uint32_t, std::unordered_set<size_t>> seenBuckets;
285 for (auto& queryResult : features) {
286 auto& feature = queryResult.first;
287 auto& bbox = queryResult.second;
288
289 // Skip already seen features.
290 auto& seenFeatures = seenBuckets[feature.bucketInstanceId];
291 if (seenFeatures.find(feature.index) != seenFeatures.end())
292 continue;
293
294 if (!polygonIntersectsBox(gridQuery, bbox)) {
295 continue;
296 }
297
298 seenFeatures.insert(feature.index);
299 result[feature.bucketInstanceId].push_back(feature);
300 }
301
302 return result;
303
304 }
305
projectAnchor(const mat4 & posMatrix,const Point<float> & point) const306 std::pair<float,float> CollisionIndex::projectAnchor(const mat4& posMatrix, const Point<float>& point) const {
307 vec4 p = {{ point.x, point.y, 0, 1 }};
308 matrix::transformMat4(p, p, posMatrix);
309 return std::make_pair(
310 0.5 + 0.5 * (transformState.getCameraToCenterDistance() / p[3]),
311 p[3]
312 );
313 }
314
projectAndGetPerspectiveRatio(const mat4 & posMatrix,const Point<float> & point) const315 std::pair<Point<float>,float> CollisionIndex::projectAndGetPerspectiveRatio(const mat4& posMatrix, const Point<float>& point) const {
316 vec4 p = {{ point.x, point.y, 0, 1 }};
317 matrix::transformMat4(p, p, posMatrix);
318 return std::make_pair(
319 Point<float>(
320 (((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding,
321 (((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding
322 ),
323 // See perspective ratio comment in symbol_sdf.vertex
324 // We're doing collision detection in viewport space so we need
325 // to scale down boxes in the distance
326 0.5 + 0.5 * (transformState.getCameraToCenterDistance() / p[3])
327 );
328 }
329
projectPoint(const mat4 & posMatrix,const Point<float> & point) const330 Point<float> CollisionIndex::projectPoint(const mat4& posMatrix, const Point<float>& point) const {
331 vec4 p = {{ point.x, point.y, 0, 1 }};
332 matrix::transformMat4(p, p, posMatrix);
333 return Point<float>(
334 (((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding,
335 (((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding
336 );
337 }
338
339 } // namespace mbgl
340