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