1use std::collections::HashMap;
4
5use bitflags::bitflags;
6use cgmath::{Matrix4, Vector4};
7
8use crate::{
9 coords::EXTENT,
10 euclid::{Box2D, Point2D},
11 legacy::{
12 buckets::symbol_bucket::PlacedSymbol,
13 collision_feature::{CollisionBox, CollisionFeature, ProjectedCollisionBox},
14 geometry::feature_index::IndexedSubfeature,
15 grid_index::{Circle, GridIndex},
16 layout::symbol_projection::{place_first_and_last_glyph, project, TileDistance},
17 util::geo::ScreenLineString,
18 MapMode, ScreenSpace, TileSpace,
19 },
20 render::{camera::ModelViewProjection, view_state::ViewState},
21};
22
23type TransformState = ViewState;
25
26type CollisionBoundaries = Box2D<f64, ScreenSpace>; bitflags! {
30 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33 struct IntersectStatusFlags: u8 {
34 const None = 0;
35 const HorizontalBorders = 1 << 0;
36 const VerticalBorders = 1 << 1;
37
38 }
39}
40
41impl Default for IntersectStatusFlags {
42 fn default() -> Self {
44 Self::None
45 }
46}
47
48#[derive(Default)]
50struct IntersectStatus {
51 flags: IntersectStatusFlags,
52 min_section_length: i32,
54}
55
56const VIEWPORT_PADDING_DEFAULT: f64 = 100.;
64const VIEWPORT_PADDING_FOR_STATIC_TILES: f64 = 1024.;
66
67fn find_viewport_padding(transform_state: &TransformState, map_mode: MapMode) -> f64 {
69 if map_mode == MapMode::Tile {
70 return VIEWPORT_PADDING_FOR_STATIC_TILES;
71 }
72 if transform_state.camera().get_pitch().0 != 0.0 {
73 VIEWPORT_PADDING_DEFAULT * 2.
74 } else {
75 VIEWPORT_PADDING_DEFAULT
76 }
77}
78
79type CollisionGrid = GridIndex<IndexedSubfeature>;
81pub struct CollisionIndex {
83 transform_state: TransformState,
84 viewport_padding: f64,
85 collision_grid: CollisionGrid,
86 ignored_grid: CollisionGrid,
87 screen_right_boundary: f64,
88 screen_bottom_boundary: f64,
89 grid_right_boundary: f64,
90 grid_bottom_boundary: f64,
91 pitch_factor: f64,
92}
93
94impl CollisionIndex {
95 pub fn new(transform_state: &TransformState, map_mode: MapMode) -> Self {
97 let viewport_padding = find_viewport_padding(transform_state, map_mode);
98 Self {
99 transform_state: transform_state.clone(),
100 viewport_padding,
101 collision_grid: CollisionGrid::new(
102 transform_state.width() + 2. * viewport_padding,
103 transform_state.height() + 2. * viewport_padding,
104 25,
105 ),
106 ignored_grid: CollisionGrid::new(
107 transform_state.width() + 2. * viewport_padding,
108 transform_state.height() + 2. * viewport_padding,
109 25,
110 ),
111 screen_right_boundary: transform_state.width() + viewport_padding,
112 screen_bottom_boundary: transform_state.height() + viewport_padding,
113 grid_right_boundary: transform_state.width() + 2. * viewport_padding,
114 grid_bottom_boundary: transform_state.height() + 2. * viewport_padding,
115 pitch_factor: transform_state.camera().get_pitch().0.cos()
116 * transform_state.camera_to_center_distance(),
117 }
118 }
119
120 pub fn intersects_tile_edges(
122 &self,
123 box_: &CollisionBox,
124 shift: Point2D<f64, ScreenSpace>,
125 pos_matrix: &ModelViewProjection,
126 text_pixel_ratio: f64,
127 tile_edges: CollisionBoundaries,
128 ) -> IntersectStatus {
129 let boundaries =
130 self.get_projected_collision_boundaries(pos_matrix, shift, text_pixel_ratio, box_);
131 let mut result: IntersectStatus = IntersectStatus::default();
132 let x1 = boundaries.min.x;
133 let y1 = boundaries.min.y;
134 let x2 = boundaries.max.x;
135 let y2 = boundaries.max.y;
136
137 let tile_x1 = tile_edges.min.x;
138 let tile_y1 = tile_edges.min.y;
139 let tile_x2 = tile_edges.max.x;
140 let tile_y2 = tile_edges.max.y;
141
142 let mut min_section_length = ((tile_x1 - x1).min(x2 - tile_x1)) as i32;
144 if min_section_length <= 0 {
145 min_section_length = ((tile_x2 - x1).min(x2 - tile_x2)) as i32;
147 }
148 if min_section_length > 0 {
149 result.flags |= IntersectStatusFlags::VerticalBorders;
150 result.min_section_length = min_section_length;
151 }
152 min_section_length = ((tile_y1 - y1).min(y2 - tile_y1)) as i32;
154 if min_section_length <= 0 {
155 min_section_length = ((tile_y2 - y1).min(y2 - tile_y2)) as i32;
157 }
158 if min_section_length > 0 {
159 result.flags |= IntersectStatusFlags::HorizontalBorders;
160 result.min_section_length = (result.min_section_length).min(min_section_length);
161 }
162 result
163 }
164
165 pub fn place_feature<F>(
167 &self,
168 feature: &CollisionFeature,
169 shift: Point2D<f64, ScreenSpace>,
170 pos_matrix: &ModelViewProjection,
171 label_plane_matrix: &Matrix4<f64>,
172 text_pixel_ratio: f64,
173 symbol: &PlacedSymbol,
174 scale: f64,
175 font_size: f64,
176 allow_overlap: bool,
177 pitch_with_map: bool,
178 collision_debug: bool,
179 avoid_edges: Option<CollisionBoundaries>,
180 collision_group_predicate: Option<F>,
181 projected_boxes: &mut Vec<ProjectedCollisionBox>, ) -> (bool, bool)
183 where
184 F: Fn(&IndexedSubfeature) -> bool,
185 {
186 assert!(projected_boxes.is_empty());
187 if !feature.along_line {
188 let box_ = feature.boxes.first().unwrap();
189 let collision_boundaries =
190 self.get_projected_collision_boundaries(pos_matrix, shift, text_pixel_ratio, box_);
191 projected_boxes.push(ProjectedCollisionBox::Box(collision_boundaries));
192
193 if let Some(avoid_edges) = avoid_edges {
194 if !self.is_inside_tile(&collision_boundaries, &avoid_edges) {
195 return (false, false);
196 }
197 }
198
199 if !self.is_inside_grid(&collision_boundaries)
200 || (!allow_overlap
201 && self.collision_grid.hit_test(
202 projected_boxes.last().unwrap().box_(),
203 collision_group_predicate,
204 ))
205 {
206 return (false, false);
207 }
208
209 (true, self.is_offscreen(&collision_boundaries))
210 } else {
211 self.place_line_feature(
212 feature,
213 pos_matrix,
214 label_plane_matrix,
215 text_pixel_ratio,
216 symbol,
217 scale,
218 font_size,
219 allow_overlap,
220 pitch_with_map,
221 collision_debug,
222 avoid_edges,
223 collision_group_predicate,
224 projected_boxes,
225 )
226 }
227 }
228
229 pub fn insert_feature(
231 &mut self,
232 feature: CollisionFeature,
233 projected_boxes: &Vec<ProjectedCollisionBox>,
234 ignore_placement: bool,
235 bucket_instance_id: u32,
236 collision_group_id: u16,
237 ) {
238 if feature.along_line {
239 for circle in projected_boxes {
240 if !circle.is_circle() {
241 continue;
242 }
243
244 if ignore_placement {
245 self.ignored_grid.insert_circle(
246 IndexedSubfeature::new(
247 feature.indexed_feature.clone(),
248 bucket_instance_id,
249 collision_group_id,
250 ), *circle.circle(),
252 );
253 } else {
254 self.collision_grid.insert_circle(
255 IndexedSubfeature::new(
256 feature.indexed_feature.clone(),
257 bucket_instance_id,
258 collision_group_id,
259 ), *circle.circle(),
261 );
262 }
263 }
264 } else if !projected_boxes.is_empty() {
265 assert!(projected_boxes.len() == 1);
266 let box_ = projected_boxes[0];
267 if ignore_placement {
269 self.ignored_grid.insert(
270 IndexedSubfeature::new(
271 feature.indexed_feature,
272 bucket_instance_id,
273 collision_group_id,
274 ),
275 *box_.box_(),
276 );
277 } else {
278 self.collision_grid.insert(
279 IndexedSubfeature::new(
280 feature.indexed_feature,
281 bucket_instance_id,
282 collision_group_id,
283 ),
284 *box_.box_(),
285 );
286 }
287 }
288 }
289
290 pub fn query_rendered_symbols(
292 &self,
293 line_string: &ScreenLineString,
294 ) -> HashMap<u32, Vec<IndexedSubfeature>> {
295 todo!()
296 }
297
298 pub fn project_tile_boundaries(&self, pos_matrix: &ModelViewProjection) -> CollisionBoundaries {
300 let top_left = self.project_point(pos_matrix, &Point2D::zero());
301 let bottom_right = self.project_point(pos_matrix, &Point2D::new(EXTENT, EXTENT)); CollisionBoundaries::new(
304 Point2D::new(top_left.x, top_left.y),
305 Point2D::new(bottom_right.x, bottom_right.y),
306 )
307 }
308
309 pub fn get_transform_state(&self) -> &TransformState {
311 &self.transform_state
312 }
313
314 pub fn get_viewport_padding(&self) -> f64 {
316 self.viewport_padding
317 }
318}
319
320impl CollisionIndex {
321 fn is_offscreen(&self, boundaries: &CollisionBoundaries) -> bool {
323 boundaries.max.x < self.viewport_padding
324 || boundaries.min.x >= self.screen_right_boundary
325 || boundaries.max.y < self.viewport_padding
326 || boundaries.min.y >= self.screen_bottom_boundary
327 }
328 fn is_inside_grid(&self, boundaries: &CollisionBoundaries) -> bool {
330 boundaries.max.x >= 0.
331 && boundaries.min.x < self.grid_right_boundary
332 && boundaries.max.y >= 0.
333 && boundaries.min.y < self.grid_bottom_boundary
334 }
335
336 fn is_inside_tile(
338 &self,
339 boundaries: &CollisionBoundaries,
340 tile_boundaries: &CollisionBoundaries,
341 ) -> bool {
342 boundaries.min.x >= tile_boundaries.min.x
343 && boundaries.min.y >= tile_boundaries.min.y
344 && boundaries.max.x < tile_boundaries.max.x
345 && boundaries.max.y < tile_boundaries.max.y
346 }
347
348 fn overlaps_tile(
350 &self,
351 boundaries: &CollisionBoundaries,
352 tile_boundaries: &CollisionBoundaries,
353 ) -> bool {
354 boundaries.min.x < tile_boundaries.max.x
355 && boundaries.max.x > tile_boundaries.min.x
356 && boundaries.min.y < tile_boundaries.max.y
357 && boundaries.max.y > tile_boundaries.min.y
358 }
359
360 fn place_line_feature<F>(
362 &self,
363 feature: &CollisionFeature,
364 pos_matrix: &ModelViewProjection,
365 label_plane_matrix: &Matrix4<f64>,
366 text_pixel_ratio: f64,
367 symbol: &PlacedSymbol,
368 scale: f64,
369 font_size: f64,
370 allow_overlap: bool,
371 pitch_with_map: bool,
372 collision_debug: bool,
373 avoid_edges: Option<CollisionBoundaries>,
374 collision_group_predicate: Option<F>,
375 projected_boxes: &mut Vec<ProjectedCollisionBox>, ) -> (bool, bool)
377 where
378 F: Fn(&IndexedSubfeature) -> bool,
379 {
380 assert!(feature.along_line);
381 assert!(projected_boxes.is_empty());
382 let tile_unit_anchor_point = symbol.anchor_point;
383 let projected_anchor = self.project_anchor(pos_matrix, &tile_unit_anchor_point);
384
385 let font_scale = font_size / 24.;
386 let line_offset_x = symbol.line_offset[0] * font_size;
387 let line_offset_y = symbol.line_offset[1] * font_size;
388
389 let label_plane_anchor_point = project(tile_unit_anchor_point, label_plane_matrix).0;
390
391 let first_and_last_glyph = place_first_and_last_glyph(
392 font_scale,
393 line_offset_x,
394 line_offset_y,
395 false,
396 label_plane_anchor_point,
397 tile_unit_anchor_point,
398 symbol,
399 label_plane_matrix,
400 true,
401 );
402
403 let mut collision_detected = false;
404 let mut in_grid = false;
405 let mut entirely_offscreen = true;
406
407 let tile_to_viewport = projected_anchor.0 * text_pixel_ratio;
408 let pixels_to_tile_units = 1. / (text_pixel_ratio * scale);
412
413 let mut first_tile_distance = 0.;
414 let mut last_tile_distance = 0.;
415 if let Some(first_and_last_glyph) = &first_and_last_glyph {
416 first_tile_distance = self.approximate_tile_distance(
417 first_and_last_glyph.0.tile_distance.as_ref().unwrap(),
418 first_and_last_glyph.0.angle,
419 pixels_to_tile_units,
420 projected_anchor.1,
421 pitch_with_map,
422 );
423 last_tile_distance = self.approximate_tile_distance(
424 first_and_last_glyph.1.tile_distance.as_ref().unwrap(),
425 first_and_last_glyph.1.angle,
426 pixels_to_tile_units,
427 projected_anchor.1,
428 pitch_with_map,
429 );
430 }
431
432 let mut previous_circle_placed = false;
433 projected_boxes.resize(feature.boxes.len(), ProjectedCollisionBox::default());
434 for i in 0..feature.boxes.len() {
435 let circle = feature.boxes[i];
436 let box_signed_distance_from_anchor = circle.signed_distance_from_anchor;
437 if first_and_last_glyph.is_none()
438 || (box_signed_distance_from_anchor < -first_tile_distance)
439 || (box_signed_distance_from_anchor > last_tile_distance)
440 {
441 previous_circle_placed = false;
445 continue;
446 }
447
448 let projected_point = self.project_point(pos_matrix, &circle.anchor);
449 let tile_unit_radius = (circle.x2 - circle.x1) / 2.;
450 let radius = tile_unit_radius * tile_to_viewport;
451
452 if previous_circle_placed {
453 let previous_circle = &projected_boxes[i - 1];
454 assert!(previous_circle.is_circle());
455 let previous_center = previous_circle.circle().center;
456 let dx = projected_point.x - previous_center.x;
457 let dy = projected_point.y - previous_center.y;
458 let placed_too_densely = radius * radius * 2. > dx * dx + dy * dy;
467 if placed_too_densely {
468 let at_least_one_more_circle = (i + 1) < feature.boxes.len();
469 if at_least_one_more_circle {
470 let next_circle = feature.boxes[i + 1];
471 let next_box_distance_from_anchor = next_circle.signed_distance_from_anchor;
472 if (next_box_distance_from_anchor > -first_tile_distance)
473 && (next_box_distance_from_anchor < last_tile_distance)
474 {
475 previous_circle_placed = false;
480 continue;
481 }
482 }
483 }
484 }
485
486 previous_circle_placed = true;
487
488 let collision_boundaries = CollisionBoundaries::new(
489 Point2D::new(projected_point.x - radius, projected_point.y - radius),
490 Point2D::new(projected_point.x + radius, projected_point.y + radius),
491 );
492
493 projected_boxes[i] = ProjectedCollisionBox::Circle(Circle::new(
494 Point2D::new(projected_point.x, projected_point.y),
495 radius,
496 ));
497
498 entirely_offscreen &= self.is_offscreen(&collision_boundaries);
499 in_grid |= self.is_inside_grid(&collision_boundaries);
500
501 if let Some(avoid_edges) = avoid_edges {
502 if !self.is_inside_tile(&collision_boundaries, &avoid_edges) {
503 if !collision_debug {
504 return (false, false);
505 } else {
506 collision_detected = true;
509 }
510 }
511 }
512
513 if !allow_overlap
514 && self.collision_grid.hit_test_circle(
515 projected_boxes[i].circle(),
516 collision_group_predicate.as_ref(),
517 )
518 {
519 if !collision_debug {
520 return (false, false);
521 } else {
522 collision_detected = true;
525 }
526 }
527 }
528
529 (
530 !collision_detected && first_and_last_glyph.is_some() && in_grid,
531 entirely_offscreen,
532 )
533 }
534
535 fn approximate_tile_distance(
537 &self,
538 tile_distance: &TileDistance,
539 last_segment_angle: f64,
540 pixels_to_tile_units: f64,
541 camera_to_anchor_distance: f64,
542 pitch_with_map: bool,
543 ) -> f64 {
544 let incidence_stretch = if pitch_with_map {
560 1.
561 } else {
562 camera_to_anchor_distance / self.pitch_factor
563 };
564 let last_segment_tile = tile_distance.last_segment_viewport_distance * pixels_to_tile_units;
565 tile_distance.prev_tile_distance
566 + last_segment_tile
567 + (incidence_stretch - 1.) * last_segment_tile * last_segment_angle.sin().abs()
568 }
569
570 fn project_anchor(
572 &self,
573 pos_matrix: &ModelViewProjection,
574 point: &Point2D<f64, TileSpace>,
575 ) -> (f64, f64) {
576 let p = Vector4::new(point.x, point.y, 0., 1.);
577 let p = pos_matrix.project(p); (
579 0.5 + 0.5 * (self.transform_state.camera_to_center_distance() / p[3]),
580 p[3],
581 )
582 }
583 fn project_and_get_perspective_ratio(
585 &self,
586 pos_matrix: &ModelViewProjection,
587 point: &Point2D<f64, TileSpace>,
588 ) -> (Point2D<f64, ScreenSpace>, f64) {
589 let p = Vector4::new(point.x, point.y, 0., 1.);
590 let p = pos_matrix.project(p); let width = self.transform_state.width();
592 let height = self.transform_state.height();
593 let ccd = self.transform_state.camera_to_center_distance();
594 (
595 Point2D::new(
596 ((p[0] / p[3] + 1.) / 2.) * width + self.viewport_padding,
597 ((-p[1] / p[3] + 1.) / 2.) * height + self.viewport_padding,
598 ),
599 0.5 + 0.5 * ccd / p[3],
603 )
604 }
605 fn project_point(
607 &self,
608 pos_matrix: &ModelViewProjection,
609 point: &Point2D<f64, TileSpace>,
610 ) -> Point2D<f64, ScreenSpace> {
611 let p = Vector4::new(point.x, point.y, 0., 1.);
612 let p = pos_matrix.project(p); let width = self.transform_state.width();
614 let height = self.transform_state.height();
615 Point2D::new(
616 ((p[0] / p[3] + 1.) / 2.) * width + self.viewport_padding,
617 ((-p[1] / p[3] + 1.) / 2.) * height + self.viewport_padding,
618 )
619 }
620
621 fn get_projected_collision_boundaries(
623 &self,
624 pos_matrix: &ModelViewProjection,
625 shift: Point2D<f64, ScreenSpace>,
626 text_pixel_ratio: f64,
627 box_: &CollisionBox,
628 ) -> CollisionBoundaries {
629 let (projected_point, tile_to_viewport) =
630 self.project_and_get_perspective_ratio(pos_matrix, &box_.anchor);
631 let tile_to_viewport = text_pixel_ratio * tile_to_viewport;
632 let tile_to_viewport = 1.; CollisionBoundaries::new(
634 Point2D::new(
635 (box_.x1 + shift.x) * tile_to_viewport + projected_point.x,
636 (box_.y1 + shift.y) * tile_to_viewport + projected_point.y,
637 ),
638 Point2D::new(
639 (box_.x2 + shift.x) * tile_to_viewport + projected_point.x,
640 (box_.y2 + shift.y) * tile_to_viewport + projected_point.y,
641 ),
642 )
643 }
644}