1 #include <mbgl/text/placement.hpp>
2 #include <mbgl/renderer/render_layer.hpp>
3 #include <mbgl/renderer/layers/render_symbol_layer.hpp>
4 #include <mbgl/renderer/render_tile.hpp>
5 #include <mbgl/tile/geometry_tile.hpp>
6 #include <mbgl/renderer/buckets/symbol_bucket.hpp>
7 #include <mbgl/renderer/bucket.hpp>
8
9 namespace mbgl {
10
OpacityState(bool placed_,bool skipFade)11 OpacityState::OpacityState(bool placed_, bool skipFade)
12 : opacity((skipFade && placed_) ? 1 : 0)
13 , placed(placed_)
14 {
15 }
16
OpacityState(const OpacityState & prevState,float increment,bool placed_)17 OpacityState::OpacityState(const OpacityState& prevState, float increment, bool placed_) :
18 opacity(::fmax(0, ::fmin(1, prevState.opacity + (prevState.placed ? increment : -increment)))),
19 placed(placed_) {}
20
isHidden() const21 bool OpacityState::isHidden() const {
22 return opacity == 0 && !placed;
23 }
24
JointOpacityState(bool placedText,bool placedIcon,bool skipFade)25 JointOpacityState::JointOpacityState(bool placedText, bool placedIcon, bool skipFade) :
26 icon(OpacityState(placedIcon, skipFade)),
27 text(OpacityState(placedText, skipFade)) {}
28
JointOpacityState(const JointOpacityState & prevOpacityState,float increment,bool placedText,bool placedIcon)29 JointOpacityState::JointOpacityState(const JointOpacityState& prevOpacityState, float increment, bool placedText, bool placedIcon) :
30 icon(OpacityState(prevOpacityState.icon, increment, placedIcon)),
31 text(OpacityState(prevOpacityState.text, increment, placedText)) {}
32
isHidden() const33 bool JointOpacityState::isHidden() const {
34 return icon.isHidden() && text.isHidden();
35 }
36
Placement(const TransformState & state_,MapMode mapMode_)37 Placement::Placement(const TransformState& state_, MapMode mapMode_)
38 : collisionIndex(state_)
39 , state(state_)
40 , mapMode(mapMode_)
41 {}
42
placeLayer(RenderSymbolLayer & symbolLayer,const mat4 & projMatrix,bool showCollisionBoxes)43 void Placement::placeLayer(RenderSymbolLayer& symbolLayer, const mat4& projMatrix, bool showCollisionBoxes) {
44
45 std::unordered_set<uint32_t> seenCrossTileIDs;
46
47 for (RenderTile& renderTile : symbolLayer.renderTiles) {
48 if (!renderTile.tile.isRenderable()) {
49 continue;
50 }
51 assert(dynamic_cast<GeometryTile*>(&renderTile.tile));
52 GeometryTile& geometryTile = static_cast<GeometryTile&>(renderTile.tile);
53
54 auto bucket = renderTile.tile.getBucket<SymbolBucket>(*symbolLayer.baseImpl);
55 if (!bucket) {
56 continue;
57 }
58 SymbolBucket& symbolBucket = *bucket;
59
60 if (symbolBucket.bucketLeaderID != symbolLayer.getID()) {
61 // Only place this layer if it's the "group leader" for the bucket
62 continue;
63 }
64
65 auto& layout = symbolBucket.layout;
66
67 const float pixelsToTileUnits = renderTile.id.pixelsToTileUnits(1, state.getZoom());
68
69 const float scale = std::pow(2, state.getZoom() - geometryTile.id.overscaledZ);
70 const float textPixelRatio = (util::tileSize * geometryTile.id.overscaleFactor()) / util::EXTENT;
71
72 mat4 posMatrix;
73 state.matrixFor(posMatrix, renderTile.id);
74 matrix::multiply(posMatrix, projMatrix, posMatrix);
75
76 mat4 textLabelPlaneMatrix = getLabelPlaneMatrix(posMatrix,
77 layout.get<style::TextPitchAlignment>() == style::AlignmentType::Map,
78 layout.get<style::TextRotationAlignment>() == style::AlignmentType::Map,
79 state,
80 pixelsToTileUnits);
81
82 mat4 iconLabelPlaneMatrix = getLabelPlaneMatrix(posMatrix,
83 layout.get<style::IconPitchAlignment>() == style::AlignmentType::Map,
84 layout.get<style::IconRotationAlignment>() == style::AlignmentType::Map,
85 state,
86 pixelsToTileUnits);
87
88
89 // As long as this placement lives, we have to hold onto this bucket's
90 // matching FeatureIndex/data for querying purposes
91 retainedQueryData.emplace(std::piecewise_construct,
92 std::forward_as_tuple(symbolBucket.bucketInstanceId),
93 std::forward_as_tuple(symbolBucket.bucketInstanceId, geometryTile.getFeatureIndex(), geometryTile.id));
94
95 placeLayerBucket(symbolBucket, posMatrix, textLabelPlaneMatrix, iconLabelPlaneMatrix, scale, textPixelRatio, showCollisionBoxes, seenCrossTileIDs, renderTile.tile.holdForFade());
96 }
97 }
98
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)99 void Placement::placeLayerBucket(
100 SymbolBucket& bucket,
101 const mat4& posMatrix,
102 const mat4& textLabelPlaneMatrix,
103 const mat4& iconLabelPlaneMatrix,
104 const float scale,
105 const float textPixelRatio,
106 const bool showCollisionBoxes,
107 std::unordered_set<uint32_t>& seenCrossTileIDs,
108 const bool holdingForFade) {
109
110 auto partiallyEvaluatedTextSize = bucket.textSizeBinder->evaluateForZoom(state.getZoom());
111 auto partiallyEvaluatedIconSize = bucket.iconSizeBinder->evaluateForZoom(state.getZoom());
112
113 for (auto& symbolInstance : bucket.symbolInstances) {
114
115 if (seenCrossTileIDs.count(symbolInstance.crossTileID) == 0) {
116 if (holdingForFade) {
117 // Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
118 // know yet if we have a duplicate in a parent tile that _should_ be placed.
119 placements.emplace(symbolInstance.crossTileID, JointPlacement(false, false, false));
120 continue;
121 }
122
123 bool placeText = false;
124 bool placeIcon = false;
125 bool offscreen = true;
126
127 if (symbolInstance.placedTextIndex) {
128 PlacedSymbol& placedSymbol = bucket.text.placedSymbols.at(*symbolInstance.placedTextIndex);
129 const float fontSize = evaluateSizeForFeature(partiallyEvaluatedTextSize, placedSymbol);
130
131 auto placed = collisionIndex.placeFeature(symbolInstance.textCollisionFeature,
132 posMatrix, textLabelPlaneMatrix, textPixelRatio,
133 placedSymbol, scale, fontSize,
134 bucket.layout.get<style::TextAllowOverlap>(),
135 bucket.layout.get<style::TextPitchAlignment>() == style::AlignmentType::Map,
136 showCollisionBoxes);
137 placeText = placed.first;
138 offscreen &= placed.second;
139 }
140
141 if (symbolInstance.placedIconIndex) {
142 PlacedSymbol& placedSymbol = bucket.icon.placedSymbols.at(*symbolInstance.placedIconIndex);
143 const float fontSize = evaluateSizeForFeature(partiallyEvaluatedIconSize, placedSymbol);
144
145 auto placed = collisionIndex.placeFeature(symbolInstance.iconCollisionFeature,
146 posMatrix, iconLabelPlaneMatrix, textPixelRatio,
147 placedSymbol, scale, fontSize,
148 bucket.layout.get<style::IconAllowOverlap>(),
149 bucket.layout.get<style::IconPitchAlignment>() == style::AlignmentType::Map,
150 showCollisionBoxes);
151 placeIcon = placed.first;
152 offscreen &= placed.second;
153 }
154
155 const bool iconWithoutText = !symbolInstance.hasText || bucket.layout.get<style::TextOptional>();
156 const bool textWithoutIcon = !symbolInstance.hasIcon || bucket.layout.get<style::IconOptional>();
157
158 // combine placements for icon and text
159 if (!iconWithoutText && !textWithoutIcon) {
160 placeText = placeIcon = placeText && placeIcon;
161 } else if (!textWithoutIcon) {
162 placeText = placeText && placeIcon;
163 } else if (!iconWithoutText) {
164 placeIcon = placeText && placeIcon;
165 }
166
167 if (placeText) {
168 collisionIndex.insertFeature(symbolInstance.textCollisionFeature, bucket.layout.get<style::TextIgnorePlacement>(), bucket.bucketInstanceId);
169 }
170
171 if (placeIcon) {
172 collisionIndex.insertFeature(symbolInstance.iconCollisionFeature, bucket.layout.get<style::IconIgnorePlacement>(), bucket.bucketInstanceId);
173 }
174
175 assert(symbolInstance.crossTileID != 0);
176
177 if (placements.find(symbolInstance.crossTileID) != placements.end()) {
178 // If there's a previous placement with this ID, it comes from a tile that's fading out
179 // Erase it so that the placement result from the non-fading tile supersedes it
180 placements.erase(symbolInstance.crossTileID);
181 }
182
183 placements.emplace(symbolInstance.crossTileID, JointPlacement(placeText, placeIcon, offscreen || bucket.justReloaded));
184 seenCrossTileIDs.insert(symbolInstance.crossTileID);
185 }
186 }
187
188 bucket.justReloaded = false;
189 }
190
commit(const Placement & prevPlacement,TimePoint now)191 void Placement::commit(const Placement& prevPlacement, TimePoint now) {
192 commitTime = now;
193
194 bool placementChanged = false;
195
196 float increment = mapMode == MapMode::Continuous ?
197 std::chrono::duration<float>(commitTime - prevPlacement.commitTime) / Duration(std::chrono::milliseconds(300)) :
198 1.0;
199
200 // add the opacities from the current placement, and copy their current values from the previous placement
201 for (auto& jointPlacement : placements) {
202 auto prevOpacity = prevPlacement.opacities.find(jointPlacement.first);
203 if (prevOpacity != prevPlacement.opacities.end()) {
204 opacities.emplace(jointPlacement.first, JointOpacityState(prevOpacity->second, increment, jointPlacement.second.text, jointPlacement.second.icon));
205 placementChanged = placementChanged ||
206 jointPlacement.second.icon != prevOpacity->second.icon.placed ||
207 jointPlacement.second.text != prevOpacity->second.text.placed;
208 } else {
209 opacities.emplace(jointPlacement.first, JointOpacityState(jointPlacement.second.text, jointPlacement.second.icon, jointPlacement.second.skipFade));
210 placementChanged = placementChanged || jointPlacement.second.icon || jointPlacement.second.text;
211 }
212 }
213
214 // copy and update values from the previous placement that aren't in the current placement but haven't finished fading
215 for (auto& prevOpacity : prevPlacement.opacities) {
216 if (opacities.find(prevOpacity.first) == opacities.end()) {
217 JointOpacityState jointOpacity(prevOpacity.second, increment, false, false);
218 if (!jointOpacity.isHidden()) {
219 opacities.emplace(prevOpacity.first, jointOpacity);
220 placementChanged = placementChanged || prevOpacity.second.icon.placed || prevOpacity.second.text.placed;
221 }
222 }
223 }
224
225 fadeStartTime = placementChanged ? commitTime : prevPlacement.fadeStartTime;
226 }
227
updateLayerOpacities(RenderSymbolLayer & symbolLayer)228 void Placement::updateLayerOpacities(RenderSymbolLayer& symbolLayer) {
229 std::set<uint32_t> seenCrossTileIDs;
230 for (RenderTile& renderTile : symbolLayer.renderTiles) {
231 if (!renderTile.tile.isRenderable()) {
232 continue;
233 }
234
235 auto bucket = renderTile.tile.getBucket<SymbolBucket>(*symbolLayer.baseImpl);
236 if (!bucket) {
237 continue;
238 }
239 SymbolBucket& symbolBucket = *bucket;
240
241 if (symbolBucket.bucketLeaderID != symbolLayer.getID()) {
242 // Only update opacities this layer if it's the "group leader" for the bucket
243 continue;
244 }
245 updateBucketOpacities(symbolBucket, seenCrossTileIDs);
246 }
247 }
248
updateBucketOpacities(SymbolBucket & bucket,std::set<uint32_t> & seenCrossTileIDs)249 void Placement::updateBucketOpacities(SymbolBucket& bucket, std::set<uint32_t>& seenCrossTileIDs) {
250 if (bucket.hasTextData()) bucket.text.opacityVertices.clear();
251 if (bucket.hasIconData()) bucket.icon.opacityVertices.clear();
252 if (bucket.hasCollisionBoxData()) bucket.collisionBox.dynamicVertices.clear();
253 if (bucket.hasCollisionCircleData()) bucket.collisionCircle.dynamicVertices.clear();
254
255 JointOpacityState duplicateOpacityState(false, false, true);
256
257 JointOpacityState defaultOpacityState(
258 bucket.layout.get<style::TextAllowOverlap>(),
259 bucket.layout.get<style::IconAllowOverlap>(),
260 true);
261
262 for (SymbolInstance& symbolInstance : bucket.symbolInstances) {
263 bool isDuplicate = seenCrossTileIDs.count(symbolInstance.crossTileID) > 0;
264
265 auto it = opacities.find(symbolInstance.crossTileID);
266 auto opacityState = defaultOpacityState;
267 if (isDuplicate) {
268 opacityState = duplicateOpacityState;
269 } else if (it != opacities.end()) {
270 opacityState = it->second;
271 }
272
273 if (it == opacities.end()) {
274 opacities.emplace(symbolInstance.crossTileID, defaultOpacityState);
275 }
276
277 seenCrossTileIDs.insert(symbolInstance.crossTileID);
278
279 if (symbolInstance.hasText) {
280 auto opacityVertex = SymbolOpacityAttributes::vertex(opacityState.text.placed, opacityState.text.opacity);
281 for (size_t i = 0; i < symbolInstance.horizontalGlyphQuads.size() * 4; i++) {
282 bucket.text.opacityVertices.emplace_back(opacityVertex);
283 }
284 for (size_t i = 0; i < symbolInstance.verticalGlyphQuads.size() * 4; i++) {
285 bucket.text.opacityVertices.emplace_back(opacityVertex);
286 }
287 if (symbolInstance.placedTextIndex) {
288 bucket.text.placedSymbols[*symbolInstance.placedTextIndex].hidden = opacityState.isHidden();
289 }
290 if (symbolInstance.placedVerticalTextIndex) {
291 bucket.text.placedSymbols[*symbolInstance.placedVerticalTextIndex].hidden = opacityState.isHidden();
292 }
293 }
294 if (symbolInstance.hasIcon) {
295 auto opacityVertex = SymbolOpacityAttributes::vertex(opacityState.icon.placed, opacityState.icon.opacity);
296 if (symbolInstance.iconQuad) {
297 bucket.icon.opacityVertices.emplace_back(opacityVertex);
298 bucket.icon.opacityVertices.emplace_back(opacityVertex);
299 bucket.icon.opacityVertices.emplace_back(opacityVertex);
300 bucket.icon.opacityVertices.emplace_back(opacityVertex);
301 }
302 if (symbolInstance.placedIconIndex) {
303 bucket.icon.placedSymbols[*symbolInstance.placedIconIndex].hidden = opacityState.isHidden();
304 }
305 }
306
307 auto updateCollisionBox = [&](const auto& feature, const bool placed) {
308 if (feature.alongLine) {
309 return;
310 }
311 auto dynamicVertex = CollisionBoxDynamicAttributes::vertex(placed, false);
312 for (size_t i = 0; i < feature.boxes.size() * 4; i++) {
313 bucket.collisionBox.dynamicVertices.emplace_back(dynamicVertex);
314 }
315 };
316
317 auto updateCollisionCircles = [&](const auto& feature, const bool placed) {
318 if (!feature.alongLine) {
319 return;
320 }
321 for (const CollisionBox& box : feature.boxes) {
322 auto dynamicVertex = CollisionBoxDynamicAttributes::vertex(placed, !box.used);
323 bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex);
324 bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex);
325 bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex);
326 bucket.collisionCircle.dynamicVertices.emplace_back(dynamicVertex);
327 }
328 };
329
330 if (bucket.hasCollisionBoxData()) {
331 updateCollisionBox(symbolInstance.textCollisionFeature, opacityState.text.placed);
332 updateCollisionBox(symbolInstance.iconCollisionFeature, opacityState.icon.placed);
333 }
334 if (bucket.hasCollisionCircleData()) {
335 updateCollisionCircles(symbolInstance.textCollisionFeature, opacityState.text.placed);
336 updateCollisionCircles(symbolInstance.iconCollisionFeature, opacityState.icon.placed);
337 }
338 }
339
340 bucket.updateOpacity();
341 bucket.sortFeatures(state.getAngle());
342 auto retainedData = retainedQueryData.find(bucket.bucketInstanceId);
343 if (retainedData != retainedQueryData.end()) {
344 retainedData->second.featureSortOrder = bucket.featureSortOrder;
345 }
346 }
347
symbolFadeChange(TimePoint now) const348 float Placement::symbolFadeChange(TimePoint now) const {
349 if (mapMode == MapMode::Continuous) {
350 return std::chrono::duration<float>(now - commitTime) / Duration(std::chrono::milliseconds(300));
351 } else {
352 return 1.0;
353 }
354 }
355
hasTransitions(TimePoint now) const356 bool Placement::hasTransitions(TimePoint now) const {
357 if (mapMode == MapMode::Continuous) {
358 return stale || std::chrono::duration<float>(now - fadeStartTime) < Duration(std::chrono::milliseconds(300));
359 } else {
360 return false;
361 }
362 }
363
stillRecent(TimePoint now) const364 bool Placement::stillRecent(TimePoint now) const {
365 return mapMode == MapMode::Continuous && commitTime + Duration(std::chrono::milliseconds(300)) > now;
366 }
367
setStale()368 void Placement::setStale() {
369 stale = true;
370 }
371
getCollisionIndex() const372 const CollisionIndex& Placement::getCollisionIndex() const {
373 return collisionIndex;
374 }
375
getQueryData(uint32_t bucketInstanceId) const376 const RetainedQueryData& Placement::getQueryData(uint32_t bucketInstanceId) const {
377 auto it = retainedQueryData.find(bucketInstanceId);
378 if (it == retainedQueryData.end()) {
379 throw std::runtime_error("Placement::getQueryData with unrecognized bucketInstanceId");
380 }
381 return it->second;
382 }
383
384 } // namespace mbgl
385