maplibre/legacy/layout/
symbol_projection.rs

1//! Translated from https://github.com/maplibre/maplibre-native/blob/4add9ea/src/mbgl/layout/symbol_projection.cpp
2
3use std::f64::consts::PI;
4
5use cgmath::{Matrix4, Vector4};
6
7use crate::{
8    euclid::Point2D,
9    legacy::{
10        buckets::symbol_bucket::PlacedSymbol,
11        geometry_tile_data::GeometryCoordinates,
12        util::math::{convert_point_f64, perp},
13        TileSpace,
14    },
15};
16
17/// maplibre/maplibre-native#4add9ea original name: PointAndCameraDistance
18type PointAndCameraDistance = (Point2D<f64, TileSpace>, f64); // TODO is the Unit correct?
19
20/// maplibre/maplibre-native#4add9ea original name: TileDistance
21pub struct TileDistance {
22    pub prev_tile_distance: f64,
23    pub last_segment_viewport_distance: f64,
24}
25
26/// maplibre/maplibre-native#4add9ea original name: project
27pub fn project(point: Point2D<f64, TileSpace>, matrix: &Matrix4<f64>) -> PointAndCameraDistance {
28    let pos = Vector4::new(point.x, point.y, 0., 1.);
29    let pos = matrix * pos; // TODO verify this multiplications
30    (Point2D::new(pos[0] / pos[3], pos[1] / pos[3]), pos[3])
31}
32
33/// maplibre/maplibre-native#4add9ea original name: PlacedGlyph
34pub struct PlacedGlyph {
35    pub point: Point2D<f64, TileSpace>,
36    pub angle: f64,
37    pub tile_distance: Option<TileDistance>,
38}
39
40/// maplibre/maplibre-native#4add9ea original name: placeFirstAndLastGlyph
41pub fn place_first_and_last_glyph(
42    font_scale: f64,
43    line_offset_x: f64,
44    line_offset_y: f64,
45    flip: bool,
46    anchor_point: Point2D<f64, TileSpace>,
47    tile_anchor_point: Point2D<f64, TileSpace>,
48    symbol: &PlacedSymbol,
49    label_plane_matrix: &Matrix4<f64>,
50    return_tile_distance: bool,
51) -> Option<(PlacedGlyph, PlacedGlyph)> {
52    if symbol.glyph_offsets.is_empty() {
53        assert!(false);
54        return None;
55    }
56
57    let first_glyph_offset = *symbol.glyph_offsets.first().unwrap();
58    let last_glyph_offset = *symbol.glyph_offsets.last().unwrap();
59
60    if let (Some(first_placed_glyph), Some(last_placed_glyph)) = (
61        place_glyph_along_line(
62            font_scale * first_glyph_offset,
63            line_offset_x,
64            line_offset_y,
65            flip,
66            &anchor_point,
67            &tile_anchor_point,
68            symbol.segment as i16,
69            &symbol.line,
70            &symbol.tile_distances,
71            label_plane_matrix,
72            return_tile_distance,
73        ),
74        place_glyph_along_line(
75            font_scale * last_glyph_offset,
76            line_offset_x,
77            line_offset_y,
78            flip,
79            &anchor_point,
80            &tile_anchor_point,
81            symbol.segment as i16,
82            &symbol.line,
83            &symbol.tile_distances,
84            label_plane_matrix,
85            return_tile_distance,
86        ),
87    ) {
88        return Some((first_placed_glyph, last_placed_glyph));
89    }
90
91    None
92}
93
94/// maplibre/maplibre-native#4add9ea original name: placeGlyphAlongLine
95fn place_glyph_along_line(
96    offset_x: f64,
97    line_offset_x: f64,
98    line_offset_y: f64,
99    flip: bool,
100    projected_anchor_point: &Point2D<f64, TileSpace>,
101    tile_anchor_point: &Point2D<f64, TileSpace>,
102    anchor_segment: i16,
103    line: &GeometryCoordinates,
104    tile_distances: &Vec<f64>,
105    label_plane_matrix: &Matrix4<f64>,
106    return_tile_distance: bool,
107) -> Option<PlacedGlyph> {
108    let combined_offset_x = if flip {
109        offset_x - line_offset_x
110    } else {
111        offset_x + line_offset_x
112    };
113
114    let mut dir: i16 = if combined_offset_x > 0. { 1 } else { -1 };
115
116    let mut angle = 0.0;
117    if flip {
118        // The label needs to be flipped to keep text upright.
119        // Iterate in the reverse direction.
120        dir *= -1;
121        angle = PI;
122    }
123
124    if dir < 0 {
125        angle += PI;
126    }
127
128    let mut current_index = if dir > 0 {
129        anchor_segment
130    } else {
131        anchor_segment + 1
132    };
133
134    let initial_index = current_index;
135    let mut current = *projected_anchor_point;
136    let mut prev = *projected_anchor_point;
137    let mut distance_to_prev = 0.0;
138    let mut current_segment_distance = 0.0;
139    let abs_offset_x = combined_offset_x.abs();
140
141    while distance_to_prev + current_segment_distance <= abs_offset_x {
142        current_index += dir;
143
144        // offset does not fit on the projected line
145        if current_index < 0 || current_index >= line.len() as i16 {
146            return None;
147        }
148
149        prev = current;
150        let projection = project(
151            convert_point_f64(&line[current_index as usize]),
152            label_plane_matrix,
153        );
154        if projection.1 > 0. {
155            current = projection.0;
156        } else {
157            // The vertex is behind the plane of the camera, so we can't project it
158            // Instead, we'll create a vertex along the line that's far enough to include the glyph
159            let previous_tile_point = if distance_to_prev == 0. {
160                *tile_anchor_point
161            } else {
162                convert_point_f64(&line[(current_index - dir) as usize])
163            };
164
165            let current_tile_point = convert_point_f64(&line[current_index as usize]);
166            current = project_truncated_line_segment(
167                &previous_tile_point,
168                &current_tile_point,
169                &prev,
170                abs_offset_x - distance_to_prev + 1.,
171                label_plane_matrix,
172            );
173        }
174
175        distance_to_prev += current_segment_distance;
176        current_segment_distance = prev.distance_to(current); // TODO verify distance calculation is correct
177    }
178
179    // The point is on the current segment. Interpolate to find it.
180    let segment_interpolation_t = (abs_offset_x - distance_to_prev) / current_segment_distance;
181    let prev_to_current = current - prev;
182    let mut p = prev + (prev_to_current * segment_interpolation_t);
183
184    // offset the point from the line to text-offset and icon-offset
185    p += perp(&prev_to_current) * (line_offset_y * dir as f64 / prev_to_current.length()); // TODO verify if mag impl is correct mag == length?
186
187    let segment_angle = angle + (current.y - prev.y).atan2(current.x - prev.x); // TODO is this atan2 right?
188
189    Some(PlacedGlyph {
190        point: p,
191        angle: segment_angle,
192        tile_distance: if return_tile_distance {
193            Some(TileDistance {
194                // TODO are these the right fields assigned?
195                prev_tile_distance: if (current_index - dir) == initial_index {
196                    0.
197                } else {
198                    tile_distances[(current_index - dir) as usize]
199                },
200                last_segment_viewport_distance: abs_offset_x - distance_to_prev,
201            })
202        } else {
203            None
204        },
205    })
206}
207
208/// maplibre/maplibre-native#4add9ea original name: projectTruncatedLineSegment
209fn project_truncated_line_segment(
210    &previous_tile_point: &Point2D<f64, TileSpace>,
211    current_tile_point: &Point2D<f64, TileSpace>,
212    previous_projected_point: &Point2D<f64, TileSpace>,
213    minimum_length: f64,
214    projection_matrix: &Matrix4<f64>,
215) -> Point2D<f64, TileSpace> {
216    // We are assuming "previousTilePoint" won't project to a point within one
217    // unit of the camera plane If it did, that would mean our label extended
218    // all the way out from within the viewport to a (very distant) point near
219    // the plane of the camera. We wouldn't be able to render the label anyway
220    // once it crossed the plane of the camera.
221    let vec = previous_tile_point - *current_tile_point;
222    let projected_unit_vertex = project(
223        previous_tile_point + vec.try_normalize().unwrap_or(vec),
224        projection_matrix,
225    )
226    .0;
227    let projected_unit_segment = *previous_projected_point - projected_unit_vertex;
228
229    *previous_projected_point
230        + (projected_unit_segment * (minimum_length / projected_unit_segment.length()))
231    // TODO verify if mag impl is correct mag == length?
232}