maplibre/legacy/
collision_feature.rs

1//! Translated from https://github.com/maplibre/maplibre-native/blob/4add9ea/src/mbgl/text/collision_feature.cpp
2
3use crate::{
4    euclid::{Box2D, Point2D, Vector2D},
5    legacy::{
6        geometry::{anchor::Anchor, feature_index::IndexedSubfeature},
7        geometry_tile_data::{GeometryCoordinate, GeometryCoordinates},
8        glyph::Shaping,
9        grid_index::Circle,
10        shaping::{Padding, PositionedIcon},
11        style_types::SymbolPlacementType,
12        util::math::{convert_point_f64, convert_point_i16, deg2radf, rotate, MinMax},
13        ScreenSpace, TileSpace,
14    },
15};
16
17/// maplibre/maplibre-native#4add9ea original name: CollisionFeature
18#[derive(Clone)]
19pub struct CollisionFeature {
20    pub boxes: Vec<CollisionBox>,
21    pub indexed_feature: IndexedSubfeature,
22    pub along_line: bool,
23}
24
25impl CollisionFeature {
26    /// maplibre/maplibre-native#4add9ea original name: new
27    pub fn new(
28        line: &GeometryCoordinates,
29        anchor: &Anchor,
30        top: f64,
31        bottom: f64,
32        left: f64,
33        right: f64,
34        collision_padding: Option<Padding>,
35        box_scale: f64,
36        padding: f64,
37        placement: SymbolPlacementType,
38        indexed_feature: IndexedSubfeature,
39        overscaling: f64,
40        rotate_: f64,
41    ) -> Self {
42        let mut self_ = Self {
43            boxes: vec![],
44            indexed_feature,
45            along_line: placement != SymbolPlacementType::Point,
46        };
47
48        if top == 0. && bottom == 0. && left == 0. && right == 0. {
49            return self_;
50        }
51
52        let mut y1 = top * box_scale - padding;
53        let mut y2 = bottom * box_scale + padding;
54        let mut x1 = left * box_scale - padding;
55        let mut x2 = right * box_scale + padding;
56
57        if let Some(collision_padding) = collision_padding {
58            x1 -= collision_padding.left * box_scale;
59            y1 -= collision_padding.top * box_scale;
60            x2 += collision_padding.right * box_scale;
61            y2 += collision_padding.bottom * box_scale;
62        }
63
64        if self_.along_line {
65            let mut height = y2 - y1;
66            let length = x2 - x1;
67
68            if height <= 0.0 {
69                return self_;
70            }
71
72            height = 10.0 * box_scale.max(height);
73
74            let anchor_point = convert_point_i16(&anchor.point);
75            self_.bboxify_label(
76                line,
77                &anchor_point,
78                anchor.segment.unwrap_or(0),
79                length,
80                height,
81                overscaling,
82            );
83        } else if rotate_ != 0. {
84            // Account for *-rotate in point collision boxes
85            // Doesn't account for icon-text-fit
86            let rotate_radians = deg2radf(rotate_);
87
88            let tl = rotate(&Vector2D::<_, TileSpace>::new(x1, y1), rotate_radians);
89            let tr = rotate(&Vector2D::<_, TileSpace>::new(x2, y1), rotate_radians);
90            let bl = rotate(&Vector2D::<_, TileSpace>::new(x1, y2), rotate_radians);
91            let br = rotate(&Vector2D::<_, TileSpace>::new(x2, y2), rotate_radians);
92
93            // Collision features require an "on-axis" geometry,
94            // so take the envelope of the rotated geometry
95            // (may be quite large for wide labels rotated 45 degrees)
96            let x_min = [tl.x, tr.x, bl.x, br.x].min_value();
97            let x_max = [tl.x, tr.x, bl.x, br.x].max_value();
98            let y_min = [tl.y, tr.y, bl.y, br.y].min_value();
99            let y_max = [tl.y, tr.y, bl.y, br.y].max_value();
100
101            self_.boxes.push(CollisionBox {
102                anchor: anchor.point,
103                x1: x_min,
104                y1: y_min,
105                x2: x_max,
106                y2: y_max,
107                signed_distance_from_anchor: 0.0,
108            });
109        } else {
110            self_.boxes.push(CollisionBox {
111                anchor: anchor.point,
112                x1,
113                y1,
114                x2,
115                y2,
116                signed_distance_from_anchor: 0.0,
117            });
118        }
119        self_
120    }
121
122    // for text
123    /// maplibre/maplibre-native#4add9ea original name: new_from_text
124    pub fn new_from_text(
125        line: &GeometryCoordinates,
126        anchor: &Anchor,
127        shaped_text: Shaping,
128        box_scale: f64,
129        padding: f64,
130        placement: SymbolPlacementType,
131        indexed_feature: IndexedSubfeature,
132        overscaling: f64,
133        rotate: f64,
134    ) -> Self {
135        Self::new(
136            line,
137            anchor,
138            shaped_text.top,
139            shaped_text.bottom,
140            shaped_text.left,
141            shaped_text.right,
142            None,
143            box_scale,
144            padding,
145            placement,
146            indexed_feature,
147            overscaling,
148            rotate,
149        )
150    }
151
152    // for icons
153    // Icons collision features are always SymbolPlacementType::Point, which
154    // means the collision feature will be viewport-rotation-aligned even if the
155    // icon is map-rotation-aligned (e.g. `icon-rotation-alignment: map` _or_
156    // `symbol-placement: line`). We're relying on most icons being "close
157    // enough" to square that having incorrect rotation alignment doesn't throw
158    // off collision detection too much. See:
159    // https://github.com/mapbox/mapbox-gl-js/issues/4861
160    /// maplibre/maplibre-native#4add9ea original name: new_from_icon
161    pub fn new_from_icon(
162        line: &GeometryCoordinates,
163        anchor: &Anchor,
164        shaped_icon: &Option<PositionedIcon>,
165        box_scale: f64,
166        padding: f64,
167        indexed_feature: IndexedSubfeature,
168        rotate: f64,
169    ) -> Self {
170        Self::new(
171            line,
172            anchor,
173            if let Some(shaped_icon) = &shaped_icon {
174                shaped_icon.top
175            } else {
176                0.
177            },
178            if let Some(shaped_icon) = &shaped_icon {
179                shaped_icon.bottom
180            } else {
181                0.
182            },
183            if let Some(shaped_icon) = &shaped_icon {
184                shaped_icon.left
185            } else {
186                0.
187            },
188            if let Some(shaped_icon) = &shaped_icon {
189                shaped_icon.right
190            } else {
191                0.
192            },
193            shaped_icon
194                .as_ref()
195                .map(|shaped_icon| shaped_icon.collision_padding),
196            box_scale,
197            padding,
198            SymbolPlacementType::Point,
199            indexed_feature,
200            1.,
201            rotate,
202        )
203    }
204
205    /// maplibre/maplibre-native#4add9ea original name: bboxifyLabel
206    fn bboxify_label(
207        &mut self,
208        line: &GeometryCoordinates,
209        anchor_point: &GeometryCoordinate,
210        segment: usize,
211        label_length: f64,
212        box_size: f64,
213        overscaling: f64,
214    ) {
215        let step = box_size / 2.;
216        let n_boxes = ((label_length / step).floor() as i32).max(1);
217
218        // We calculate line collision circles out to 300% of what would normally be
219        // our max size, to allow collision detection to work on labels that expand
220        // as they move into the distance Vertically oriented labels in the distant
221        // field can extend past this padding This is a noticeable problem in
222        // overscaled tiles where the pitch 0-based symbol spacing will put labels
223        // very close together in a pitched map. To reduce the cost of adding extra
224        // collision circles, we slowly increase them for overscaled tiles.
225        let overscaling_padding_factor = 1. + 0.4 * overscaling.log2();
226        let n_pitch_padding_boxes =
227            ((n_boxes as f64 * overscaling_padding_factor / 2.).floor()) as i32;
228
229        // offset the center of the first box by half a box so that the edge of the
230        // box is at the edge of the label.
231        let first_box_offset = -box_size / 2.;
232
233        let mut p = anchor_point;
234        let mut index = segment + 1;
235        let mut anchor_distance = first_box_offset;
236        let label_start_distance = -label_length / 2.;
237        let padding_start_distance = label_start_distance - label_length / 8.;
238
239        // move backwards along the line to the first segment the label appears on
240        loop {
241            if index == 0 {
242                if anchor_distance > label_start_distance {
243                    // there isn't enough room for the label after the beginning of
244                    // the line checkMaxAngle should have already caught this
245                    return;
246                } else {
247                    // The line doesn't extend far enough back for all of our padding,
248                    // but we got far enough to show the label under most conditions.
249                    index = 0;
250                    break;
251                }
252            }
253
254            index -= 1;
255            anchor_distance -= convert_point_f64(&line[index]).distance_to(convert_point_f64(p));
256            p = &line[index];
257
258            if !(anchor_distance > padding_start_distance) {
259                break;
260            }
261        }
262
263        let mut segment_length =
264            convert_point_f64(&line[index]).distance_to(convert_point_f64(&line[index + 1]));
265
266        for i in -n_pitch_padding_boxes..n_boxes + n_pitch_padding_boxes {
267            // the distance the box will be from the anchor
268            let box_offset = i as f64 * step;
269            let mut box_distance_to_anchor = label_start_distance + box_offset;
270
271            // make the distance between pitch padding boxes bigger
272            if box_offset < 0. {
273                box_distance_to_anchor += box_offset;
274            }
275            if box_offset > label_length {
276                box_distance_to_anchor += box_offset - label_length;
277            }
278
279            if box_distance_to_anchor < anchor_distance {
280                // The line doesn't extend far enough back for this box, skip it
281                // (This could allow for line collisions on distant tiles)
282                continue;
283            }
284
285            // the box is not on the current segment. Move to the next segment.
286            while anchor_distance + segment_length < box_distance_to_anchor {
287                anchor_distance += segment_length;
288                index += 1;
289
290                // There isn't enough room before the end of the line.
291                if index + 1 >= line.len() {
292                    return;
293                }
294
295                segment_length = convert_point_f64(&line[index])
296                    .distance_to(convert_point_f64(&line[index + 1]));
297            }
298
299            // the distance the box will be from the beginning of the segment
300            let segment_box_distance = box_distance_to_anchor - anchor_distance;
301
302            let p0 = line[index];
303            let p1 = line[index + 1];
304
305            let box_anchor = Point2D::new(
306                p0.x as f64 + segment_box_distance / segment_length * (p1.x - p0.x) as f64,
307                p0.y as f64 + segment_box_distance / segment_length * (p1.y - p0.y) as f64,
308            );
309
310            // If the box is within boxSize of the anchor, force the box to be used
311            // (so even 0-width labels use at least one box)
312            // Otherwise, the .8 multiplication gives us a little bit of conservative
313            // padding in choosing which boxes to use (see CollisionIndex#placedCollisionCircles)
314            let padded_anchor_distance = if (box_distance_to_anchor - first_box_offset).abs() < step
315            {
316                0.0
317            } else {
318                (box_distance_to_anchor - first_box_offset) * 0.8
319            };
320
321            self.boxes.push(CollisionBox {
322                anchor: box_anchor,
323                x1: -box_size / 2.,
324                y1: -box_size / 2.,
325                x2: box_size / 2.,
326                y2: box_size / 2.,
327                signed_distance_from_anchor: padded_anchor_distance,
328            });
329        }
330    }
331}
332
333/// maplibre/maplibre-native#4add9ea original name: CollisionBox
334#[derive(Default, Clone, Copy, Debug)]
335pub struct CollisionBox {
336    // the box is centered around the anchor point
337    pub anchor: Point2D<f64, TileSpace>,
338
339    // the offset of the box from the label's anchor point.
340    // TODO: might be needed for #13526
341    // Point<f64> offset;
342
343    // distances to the edges from the anchor
344    pub x1: f64,
345    pub y1: f64,
346    pub x2: f64,
347    pub y2: f64,
348
349    pub signed_distance_from_anchor: f64,
350}
351
352/// maplibre/maplibre-native#4add9ea original name: ProjectedCollisionBox
353#[derive(Clone, Copy, Debug)]
354pub enum ProjectedCollisionBox {
355    Circle(Circle<f64>),
356    Box(Box2D<f64, ScreenSpace>),
357}
358
359impl Default for ProjectedCollisionBox {
360    /// maplibre/maplibre-native#4add9ea original name: default
361    fn default() -> Self {
362        Self::Box(Box2D::zero())
363    }
364}
365
366impl ProjectedCollisionBox {
367    /// maplibre/maplibre-native#4add9ea original name: box_
368    pub fn box_(&self) -> &Box2D<f64, ScreenSpace> {
369        match self {
370            ProjectedCollisionBox::Circle(_) => panic!("not a box"),
371            ProjectedCollisionBox::Box(box_) => box_,
372        }
373    }
374
375    /// maplibre/maplibre-native#4add9ea original name: circle
376    pub fn circle(&self) -> &Circle<f64> {
377        match self {
378            ProjectedCollisionBox::Circle(circle) => circle,
379            ProjectedCollisionBox::Box(_) => panic!("not a circle"),
380        }
381    }
382
383    /// maplibre/maplibre-native#4add9ea original name: isBox
384    pub fn is_box(&self) -> bool {
385        match self {
386            ProjectedCollisionBox::Circle(_) => false,
387            ProjectedCollisionBox::Box(_) => true,
388        }
389    }
390
391    /// maplibre/maplibre-native#4add9ea original name: isCircle
392    pub fn is_circle(&self) -> bool {
393        match self {
394            ProjectedCollisionBox::Circle(_) => true,
395            ProjectedCollisionBox::Box(_) => false,
396        }
397    }
398}