1use super::neighbors;
2use crate::{CellIndex, LatLng, Resolution, TWO_PI, error::InvalidGeometry};
3use ahash::{HashSet, HashSetExt};
4use either::Either;
5use float_eq::float_eq;
6use geo::{
7 BooleanOps as _, BoundingRect as _, Centroid as _, Coord, CoordsIter as _,
8 Intersects, Line, LineString, MultiPolygon, Polygon, Rect, Relate as _,
9 ToRadians as _,
10 algorithm::{
11 coordinate_position::{CoordPos, coord_pos_relative_to_ring},
12 relate::PreparedGeometry,
13 },
14 coord,
15};
16use std::{
17 cmp,
18 f64::consts::{FRAC_PI_2, PI},
19};
20
21#[derive(Debug, Clone)]
23pub struct Tiler {
24 resolution: Resolution,
25 containment_mode: ContainmentMode,
26 convert_to_rads: bool,
27 transmeridian_heuristic_enabled: bool,
28 geom: MultiPolygon,
29}
30
31pub struct AnnotatedCell {
33 pub cell: CellIndex,
35
36 pub is_fully_contained: bool,
42}
43
44impl Tiler {
45 pub fn add(&mut self, mut polygon: Polygon) -> Result<(), InvalidGeometry> {
51 if self.convert_to_rads {
53 polygon.to_radians_in_place();
54 }
55
56 ring_is_valid(polygon.exterior())?;
58 for interior in polygon.interiors() {
59 ring_is_valid(interior)?;
60 }
61
62 if self.transmeridian_heuristic_enabled && is_transmeridian(&polygon) {
64 for fixed_polygon in fix_transmeridian(polygon).0 {
65 self.geom.0.push(fixed_polygon);
66 }
67 } else {
68 self.geom.0.push(polygon);
69 }
70
71 Ok(())
72 }
73
74 pub fn add_batch(
80 &mut self,
81 geoms: impl IntoIterator<Item = Polygon>,
82 ) -> Result<(), InvalidGeometry> {
83 for polygon in geoms {
84 self.add(polygon)?;
85 }
86 Ok(())
87 }
88
89 #[must_use]
111 pub fn coverage_size_hint(&self) -> usize {
112 const POLYGON_TO_CELLS_BUFFER: usize = 12;
113
114 self.geom
115 .iter()
116 .map(|polygon| {
117 let estimated_count = bbox_hex_estimate(
118 &polygon.bounding_rect().expect("valid polygon"),
119 self.resolution,
120 );
121
122 let vertex_count = polygon
126 .interiors()
127 .iter()
128 .chain(std::iter::once(polygon.exterior()))
129 .map(|line| line.coords_count() - 1)
131 .sum();
132
133 cmp::max(estimated_count, vertex_count)
138 + POLYGON_TO_CELLS_BUFFER
139 })
140 .sum()
141 }
142
143 pub fn into_coverage(self) -> impl Iterator<Item = CellIndex> {
168 self.into_annotated_coverage().map(|value| value.cell)
169 }
170
171 pub fn into_annotated_coverage(
196 self,
197 ) -> impl Iterator<Item = AnnotatedCell> {
198 let predicate =
207 ContainmentPredicate::new(&self.geom, self.containment_mode);
208 let mut seen = HashSet::new();
210 let mut scratchpad = [0; 7];
213
214 let mut outlines = self.hex_outline(
216 self.resolution,
217 &mut seen,
218 &mut scratchpad,
219 &predicate,
220 );
221
222 if outlines.is_empty()
223 && self.containment_mode == ContainmentMode::Covers
224 {
225 let centroid = self.geom.centroid().expect("centroid");
226 return Either::Left(std::iter::once(AnnotatedCell {
227 cell: LatLng::from_radians(centroid.y(), centroid.x())
228 .expect("valid coordinate")
229 .to_cell(self.resolution),
230 is_fully_contained: false,
231 }));
232 }
233
234 let mut candidates = outermost_inner_cells(
237 &outlines,
238 &mut seen,
239 &mut scratchpad,
240 &predicate,
241 );
242 let mut next_gen = Vec::with_capacity(candidates.len() * 7);
243 let mut new_seen = HashSet::with_capacity(seen.len());
244
245 if self.containment_mode == ContainmentMode::ContainsBoundary {
246 outlines.retain(|&(_, is_fully_contained)| is_fully_contained);
247 candidates.retain(|&(_, is_fully_contained)| is_fully_contained);
248 }
249
250 let inward_propagation = std::iter::from_fn(move || {
252 if candidates.is_empty() {
253 return None;
254 }
255
256 for &(cell, _) in &candidates {
257 debug_assert!(
258 self.geom.relate(&cell_boundary(cell)).is_covers(),
259 "cell index {cell} in polygon"
260 );
261
262 let count = neighbors(cell, &mut scratchpad);
263 next_gen.extend(scratchpad[0..count].iter().filter_map(
264 |candidate| {
265 let index = CellIndex::new_unchecked(*candidate);
267 new_seen.insert(index);
268 seen.insert(index).then_some((index, true))
269 },
270 ));
271 }
272
273 let curr_gen = candidates.clone();
274
275 std::mem::swap(&mut next_gen, &mut candidates);
276 next_gen.clear();
277
278 std::mem::swap(&mut new_seen, &mut seen);
279 new_seen.clear();
280
281 Some(curr_gen.into_iter())
282 });
283
284 Either::Right(
285 outlines
286 .into_iter()
287 .chain(inward_propagation.flatten())
288 .map(|(cell, is_fully_contained)| AnnotatedCell {
289 cell,
290 is_fully_contained,
291 }),
292 )
293 }
294
295 fn hex_outline(
297 &self,
298 resolution: Resolution,
299 already_seen: &mut HashSet<CellIndex>,
300 scratchpad: &mut [u64],
301 predicate: &ContainmentPredicate<'_>,
302 ) -> Vec<(CellIndex, bool)> {
303 let outlines = self
305 .interiors()
306 .chain(self.exteriors())
307 .flat_map(|ring| get_edge_cells(ring, resolution))
308 .filter(|cell| already_seen.insert(*cell))
309 .collect::<Vec<_>>();
310 already_seen.clear();
313
314 outlines.into_iter().fold(Vec::new(), |mut acc, cell| {
318 let count = neighbors(cell, scratchpad);
319
320 acc.extend(scratchpad[0..count].iter().filter_map(|candidate| {
321 let index = CellIndex::new_unchecked(*candidate);
323
324 already_seen
325 .insert(index)
326 .then_some(index)
327 .and_then(|index| {
328 let result = predicate.apply(index);
329 result
330 .is_a_match
331 .then_some((index, result.is_fully_contained))
332 })
333 }));
334
335 acc
336 })
337 }
338
339 fn exteriors(&self) -> impl Iterator<Item = &LineString> {
341 self.geom.0.iter().map(Polygon::exterior)
342 }
343
344 fn interiors(&self) -> impl Iterator<Item = &LineString> {
346 self.geom
347 .0
348 .iter()
349 .flat_map(|polygon| polygon.interiors().iter())
350 }
351}
352
353pub struct TilerBuilder {
357 resolution: Resolution,
358 containment_mode: ContainmentMode,
359 convert_to_rads: bool,
360 transmeridian_heuristic_enabled: bool,
361}
362
363impl TilerBuilder {
364 #[must_use]
366 pub const fn new(resolution: Resolution) -> Self {
367 Self {
368 resolution,
369 containment_mode: ContainmentMode::ContainsCentroid,
370 convert_to_rads: true,
371 transmeridian_heuristic_enabled: true,
372 }
373 }
374
375 #[must_use]
377 pub const fn disable_radians_conversion(mut self) -> Self {
378 self.convert_to_rads = false;
379 self
380 }
381
382 #[must_use]
384 pub const fn containment_mode(mut self, mode: ContainmentMode) -> Self {
385 self.containment_mode = mode;
386 self
387 }
388
389 #[must_use]
396 pub const fn disable_transmeridian_heuristic(mut self) -> Self {
397 self.transmeridian_heuristic_enabled = false;
398 self
399 }
400
401 #[must_use]
403 pub fn build(self) -> Tiler {
404 Tiler {
405 resolution: self.resolution,
406 containment_mode: self.containment_mode,
407 convert_to_rads: self.convert_to_rads,
408 transmeridian_heuristic_enabled: self
409 .transmeridian_heuristic_enabled,
410 geom: MultiPolygon::new(Vec::new()),
411 }
412 }
413}
414
415#[non_exhaustive]
419#[derive(Clone, Copy, Debug, Eq, PartialEq)]
420pub enum ContainmentMode {
421 ContainsCentroid,
431
432 ContainsBoundary,
442
443 IntersectsBoundary,
453
454 Covers,
458}
459
460struct PredicateResult {
462 is_a_match: bool,
464 is_fully_contained: bool,
469}
470
471#[expect(clippy::large_enum_variant, reason = "used once, on the stack")]
472enum ContainmentPredicate<'geom> {
473 ContainsCentroid(&'geom MultiPolygon, MultiBBoxes),
474 IntersectsBoundary(PreparedGeometry<'geom, &'geom MultiPolygon>),
475}
476
477impl<'geom> ContainmentPredicate<'geom> {
478 fn new(
480 geom: &'geom MultiPolygon,
481 containment_mode: ContainmentMode,
482 ) -> Self {
483 match containment_mode {
484 ContainmentMode::ContainsCentroid => {
486 let bboxes = MultiBBoxes(
488 geom.iter()
489 .map(|polygon| BBoxes {
490 exterior: polygon
491 .exterior()
492 .bounding_rect()
493 .expect("exterior bbox"),
494 interiors: polygon
495 .interiors()
496 .iter()
497 .map(|ring| {
498 ring.bounding_rect().expect("interior bbox")
499 })
500 .collect(),
501 })
502 .collect(),
503 );
504
505 Self::ContainsCentroid(geom, bboxes)
506 }
507 ContainmentMode::ContainsBoundary
510 | ContainmentMode::IntersectsBoundary
511 | ContainmentMode::Covers => {
512 let prepared_geom = PreparedGeometry::from(geom);
513 Self::IntersectsBoundary(prepared_geom)
514 }
515 }
516 }
517
518 fn apply(&self, cell: CellIndex) -> PredicateResult {
520 match self {
521 Self::ContainsCentroid(geom, bboxes) => {
522 let ll = LatLng::from(cell);
523 let coord = coord! { x: ll.lng_radians(), y: ll.lat_radians() };
524
525 let is_a_match =
526 geom.iter().enumerate().any(|(poly_idx, polygon)| {
527 ring_contains_centroid(
528 polygon.exterior(),
529 &bboxes.0[poly_idx].exterior,
530 coord,
531 ) && !polygon.interiors().iter().enumerate().any(
532 |(ring_idx, ring)| {
533 ring_contains_centroid(
534 ring,
535 &bboxes.0[poly_idx].interiors[ring_idx],
536 coord,
537 )
538 },
539 )
540 });
541
542 PredicateResult {
543 is_a_match,
544 is_fully_contained: true,
545 }
546 }
547 Self::IntersectsBoundary(geom) => {
548 let boundary = cell_boundary(cell);
549 let relation = geom.relate(&boundary);
550
551 PredicateResult {
552 is_a_match: relation.is_intersects(),
553 is_fully_contained: relation.is_covers(),
554 }
555 }
556 }
557 }
558}
559
560fn outermost_inner_cells(
567 outlines: &[(CellIndex, bool)],
568 already_seen: &mut HashSet<CellIndex>,
569 scratchpad: &mut [u64],
570 predicate: &ContainmentPredicate<'_>,
571) -> Vec<(CellIndex, bool)> {
572 outlines.iter().fold(Vec::new(), |mut acc, &(cell, _)| {
573 let count = neighbors(cell, scratchpad);
574
575 acc.extend(scratchpad[0..count].iter().filter_map(|candidate| {
576 let index = CellIndex::new_unchecked(*candidate);
578
579 already_seen
580 .insert(index)
581 .then_some(index)
582 .and_then(|index| {
583 let result = predicate.apply(index);
584 result
585 .is_a_match
586 .then_some((index, result.is_fully_contained))
587 })
588 }));
589 acc
590 })
591}
592
593fn get_edge_cells(
595 ring: &LineString,
596 resolution: Resolution,
597) -> impl Iterator<Item = CellIndex> + '_ {
598 ring.lines().flat_map(move |line @ Line { start, end }| {
599 let count = line_hex_estimate(&line, resolution);
600
601 assert!(count <= 1 << f64::MANTISSA_DIGITS);
602 #[expect(
603 clippy::cast_precision_loss,
604 reason = "cannot happen thanks to assert above"
605 )]
606 (0..count).map(move |i| {
607 let i = i as f64;
608 let count = count as f64;
609
610 let lat = (start.y * (count - i) / count) + (end.y * i / count);
611 let lng = (start.x * (count - i) / count) + (end.x * i / count);
612
613 LatLng::from_radians(lat, lng)
614 .expect("finite line coordinate")
615 .to_cell(resolution)
616 })
617 })
618}
619
620fn line_hex_estimate(line: &Line, resolution: Resolution) -> u64 {
623 const PENT_DIAMETER_RADS: [f64; 16] = [
625 0.32549355508382627,
626 0.11062000431697926,
627 0.0431531246375496,
628 0.015280278825461551,
629 0.006095981694441515,
630 0.00217237586248339,
631 0.0008694532999397082,
632 0.0003101251537809772,
633 0.00012417902430910614,
634 0.00004429922220615181,
635 0.00001773927716796858,
636 0.000006328371112691009,
637 0.0000025341705472716865,
638 0.0000009040511973807097,
639 0.00000036202412300873475,
640 0.00000012915013523209886,
641 ];
642 let pentagon_diameter = PENT_DIAMETER_RADS[usize::from(resolution)];
643
644 let origin = LatLng::from_radians(line.start.y, line.start.x)
645 .expect("finite line-start coordinate");
646 let destination = LatLng::from_radians(line.end.y, line.end.x)
647 .expect("finite line-end coordinate");
648 let distance = origin.distance_rads(destination);
649
650 let dist_ceil = (distance / pentagon_diameter).ceil();
651 assert!(dist_ceil.is_finite());
652
653 #[expect(
654 clippy::cast_possible_truncation,
655 clippy::cast_sign_loss,
656 reason = "truncate on purpose"
657 )]
658 let estimate = dist_ceil as u64;
659
660 cmp::max(estimate, 1)
661}
662
663pub fn bbox_hex_estimate(bbox: &Rect, resolution: Resolution) -> usize {
666 const PENT_AREA_RADS2: [f64; 16] = [
673 0.05505118472518226,
674 0.006358420186890303,
675 0.0009676234334810151,
676 0.00012132336301389888,
677 0.000019309418286620768,
678 0.0000024521770265310696,
679 0.0000003928026439666205,
680 0.00000004997535264470275,
681 0.000000008012690511075445,
682 0.0000000010197039091132572,
683 0.00000000016351353999538285,
684 0.000000000020809697203105007,
685 0.000000000003336979666606075,
686 0.0000000000004246859893033221,
687 0.00000000000006810153522091642,
688 0.000000000000008667056198238203,
689 ];
690 let pentagon_area_rads2 = PENT_AREA_RADS2[usize::from(resolution)];
691
692 let min = bbox.min();
693 let max = bbox.max();
694 let p1 =
695 LatLng::from_radians(min.y, min.x).expect("finite bbox-min coordinate");
696 let p2 =
697 LatLng::from_radians(max.y, max.x).expect("finite bbox-max coordinate");
698 let diagonal = p1.distance_rads(p2);
699 let d1 = (p1.lng_radians() - p2.lng_radians()).abs();
700 let d2 = (p1.lat_radians() - p2.lat_radians()).abs();
701 let (width, length) = if d1 < d2 { (d1, d2) } else { (d2, d1) };
702 #[expect(clippy::suspicious_operation_groupings, reason = "false positive")]
705 let area = (diagonal * diagonal) / (length / width);
706
707 let estimate = (area / pentagon_area_rads2).ceil();
709
710 #[expect(
711 clippy::cast_possible_truncation,
712 clippy::cast_sign_loss,
713 reason = "truncate on purpose"
714 )]
715 let estimate = estimate as usize;
716
717 cmp::max(estimate, 1)
718}
719
720fn is_transmeridian(geom: &Polygon) -> bool {
724 geom.exterior()
725 .lines()
726 .any(|line| (line.start.x - line.end.x).abs() > PI)
727}
728
729fn fix_transmeridian(mut polygon: Polygon) -> MultiPolygon {
732 let west = Rect::new(
733 coord! { x: PI, y: -FRAC_PI_2},
734 coord! { x: TWO_PI, y: FRAC_PI_2},
735 )
736 .to_polygon();
737 let east = Rect::new(
738 coord! { x: 0., y: -FRAC_PI_2},
739 coord! { x: PI, y: FRAC_PI_2},
740 )
741 .to_polygon();
742
743 shift_transmeridian(&mut polygon);
744 let mut fixed = polygon.intersection(&west);
745 unshift_transmeridian(&mut fixed);
746 fix_clipping_boundary(&mut fixed, true);
747
748 let mut other = polygon.intersection(&east);
749 fix_clipping_boundary(&mut other, false);
750 fixed.0.extend(other.0);
751
752 fixed
753}
754
755fn shift_transmeridian(geom: &mut Polygon) {
757 geom.exterior_mut(shift_transmeridian_ring);
758 geom.interiors_mut(|interiors| {
759 for interior in interiors {
760 shift_transmeridian_ring(interior);
761 }
762 });
763}
764
765fn unshift_transmeridian(geom: &mut MultiPolygon) {
767 for polygon in geom.iter_mut() {
768 polygon.exterior_mut(unshift_transmeridian_ring);
769 polygon.interiors_mut(|interiors| {
770 for interior in interiors {
771 unshift_transmeridian_ring(interior);
772 }
773 });
774 }
775}
776
777fn fix_clipping_boundary(geom: &mut MultiPolygon, is_west: bool) {
779 for polygon in geom.iter_mut() {
780 polygon.exterior_mut(|exterior| {
781 fix_ring_clipping_boundary(exterior, is_west);
782 });
783 polygon.interiors_mut(|interiors| {
784 for interior in interiors {
785 fix_ring_clipping_boundary(interior, is_west);
786 }
787 });
788 }
789}
790
791pub fn ring_is_valid(ring: &LineString) -> Result<(), InvalidGeometry> {
793 if ring.0.len() < 4 {
795 return Err(InvalidGeometry::new(
796 "invalid ring (not enough coordinate)",
797 ));
798 }
799 if !ring.coords().all(|coord| super::coord_is_valid(*coord)) {
800 return Err(InvalidGeometry::new(
801 "every coordinate of the exterior must be valid",
802 ));
803 }
804
805 Ok(())
806}
807
808fn shift_transmeridian_ring(ring: &mut LineString) {
810 for coord in ring.coords_mut() {
811 coord.x += f64::from(coord.x < 0.) * TWO_PI;
812 }
813}
814
815fn unshift_transmeridian_ring(ring: &mut LineString) {
817 for coord in ring.coords_mut() {
818 coord.x -= f64::from(coord.x >= PI) * TWO_PI;
819 }
820}
821
822fn fix_ring_clipping_boundary(ring: &mut LineString, is_west: bool) {
828 const ROUNDING_EPSILON: f64 = 1e-6;
829 let (bad_value, fixed_value) = if is_west {
830 let mut bad_value = PI;
831 for coord in ring.coords() {
832 if float_eq!(coord.x, PI, abs <= ROUNDING_EPSILON) {
833 bad_value = coord.x;
834 break;
835 }
836 bad_value = bad_value.min(coord.x);
837 }
838 (bad_value, -PI)
839 } else {
840 let mut bad_value = -PI;
841 for coord in ring.coords() {
842 if float_eq!(coord.x, -PI, abs <= ROUNDING_EPSILON) {
843 bad_value = coord.x;
844 break;
845 }
846 bad_value = bad_value.max(coord.x);
847 }
848 (bad_value, PI)
849 };
850
851 #[expect(clippy::float_cmp, reason = "we want exact equality")]
852 for coord in ring.coords_mut() {
853 if coord.x == bad_value {
854 coord.x = fixed_value;
855 }
856 }
857}
858
859struct MultiBBoxes(Vec<BBoxes>);
861
862struct BBoxes {
864 exterior: Rect,
865 interiors: Vec<Rect>,
866}
867
868fn ring_contains_centroid(
870 ring: &LineString,
871 bbox: &Rect,
872 mut coord: Coord,
873) -> bool {
874 if !bbox.intersects(&coord) {
875 return false;
876 }
877
878 match coord_pos_relative_to_ring(coord, ring) {
879 CoordPos::Inside => true,
880 CoordPos::Outside => false,
881 CoordPos::OnBoundary => {
882 coord.y += f64::EPSILON;
893 coord_pos_relative_to_ring(coord, ring) == CoordPos::Inside
894 }
895 }
896}
897
898pub fn cell_boundary(cell: CellIndex) -> MultiPolygon {
900 let boundary = LineString(
901 cell.boundary()
902 .iter()
903 .copied()
904 .map(|ll| {
905 coord! {
906 x: ll.lng_radians(),
907 y: ll.lat_radians()
908 }
909 })
910 .collect(),
911 );
912 let polygon = Polygon::new(boundary, Vec::new());
913 if is_transmeridian(&polygon) {
914 fix_transmeridian(polygon)
915 } else {
916 MultiPolygon::new(vec![polygon])
917 }
918}