Skip to main content

utiles_cover/
cover_geojson.rs

1//! Tile cover for geojson object(s) based on mapbox's tile-cover alg
2#![expect(
3    clippy::cast_possible_truncation,
4    clippy::cast_possible_wrap,
5    clippy::cast_sign_loss
6)]
7use crate::{Result, UtilesCoverError};
8use geojson::GeoJson;
9use std::collections::{BTreeMap, HashSet};
10use utiles_core::{Tile, lnglat2tile_frac, simplify, tile, utile};
11
12#[expect(clippy::cast_precision_loss)]
13fn line_string_cover(
14    tiles_set: &mut HashSet<Tile>,
15    coords: &[(f64, f64)],
16    maxzoom: u8,
17    mut ring: Option<&mut Vec<(u32, u32)>>,
18) {
19    let mut prev_x: Option<i64> = None;
20    let mut prev_y: Option<i64> = None;
21    let mut y_value: Option<i64> = None;
22    let minxy = (1u32 << maxzoom) - 1; // Maximum valid tile index at this zoom level
23    for segment in coords.windows(2) {
24        let start_coord = segment[0];
25        let stop_coord = segment[1];
26
27        let (x0f, y0f, _) = lnglat2tile_frac(start_coord.0, start_coord.1, maxzoom);
28        let (x1f, y1f, _) = lnglat2tile_frac(stop_coord.0, stop_coord.1, maxzoom);
29
30        let dx = x1f - x0f;
31        let dy = y1f - y0f;
32
33        // Directly check for zero movement
34        if dx == 0.0 && dy == 0.0 {
35            continue;
36        }
37        let sx = dx.signum() as i64;
38        let sy = dy.signum() as i64;
39
40        let mut x = x0f.floor() as i64;
41        let mut y = y0f.floor() as i64;
42        y_value = Some(y);
43
44        let tdx = if dx == 0.0 {
45            f64::INFINITY
46        } else {
47            (sx as f64 / dx).abs()
48        };
49        let tdy = if dy == 0.0 {
50            f64::INFINITY
51        } else {
52            (sy as f64 / dy).abs()
53        };
54
55        let mut t_max_x = if dx == 0.0 {
56            f64::INFINITY
57        } else {
58            ((if dx > 0.0 { 1.0 } else { 0.0 } + x as f64 - x0f) / dx).abs()
59        };
60        let mut t_max_y = if dy == 0.0 {
61            f64::INFINITY
62        } else {
63            ((if dy > 0.0 { 1.0 } else { 0.0 } + y as f64 - y0f) / dy).abs()
64        };
65
66        // Add initial tile
67        if prev_x != Some(x) || prev_y != Some(y) {
68            let tile = utile!(x as u32, y as u32, maxzoom);
69            tiles_set.insert(tile);
70            if let Some(ring) = &mut ring
71                && prev_y != Some(y)
72            {
73                ring.push((x as u32, y as u32));
74            }
75            prev_x = Some(x);
76            prev_y = Some(y);
77        }
78
79        // the MAX number of tiles to check to dx.abs() + dy.abs()
80        let mut max_it = (dx.abs() + dy.abs()) as i64;
81        while (t_max_x < 1.0 || t_max_y < 1.0) && max_it >= 0 {
82            if t_max_x < t_max_y {
83                t_max_x += tdx;
84                x += sx;
85            } else {
86                t_max_y += tdy;
87                y += sy;
88            }
89
90            // Sanity check if x or y is outside valid tile ranges
91            if x > i64::from(minxy) + 1 || y > i64::from(minxy) + 1 {
92                break;
93            }
94
95            if prev_x != Some(x) || prev_y != Some(y) {
96                let tile = utile!(x as u32, y as u32, maxzoom);
97                tiles_set.insert(tile);
98                if let Some(ring) = &mut ring
99                    && prev_y != Some(y)
100                {
101                    ring.push((x as u32, y as u32));
102                }
103                prev_x = Some(x);
104                prev_y = Some(y);
105            }
106
107            max_it -= 1; // Decrement the number of steps remaining
108        }
109    }
110
111    // adjust the ring if needed
112    if let Some(ring) = &mut ring
113        && let (Some(first_ring), Some(y_value)) = (ring.first(), y_value)
114        && y_value == i64::from(first_ring.1)
115    {
116        ring.pop();
117    }
118}
119
120fn polygon_cover(tiles_set: &mut HashSet<Tile>, geom: &[Vec<(f64, f64)>], zoom: u8) {
121    // Collect all x-intersections per scanline (y)
122    let mut scanlines: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
123
124    for ring_pts in geom {
125        // First collect the polygon boundary into `boundary` as tile-edge samples
126        let mut boundary: Vec<(u32, u32)> = Vec::new();
127        line_string_cover(tiles_set, ring_pts, zoom, Some(&mut boundary));
128        if boundary.is_empty() {
129            continue;
130        }
131
132        // Iterate each edge, including the closing edge from last→first
133        let iter = boundary
134            .windows(2)
135            .map(|w| (w[0], w[1]))
136            .chain(std::iter::once((boundary[boundary.len() - 1], boundary[0])));
137
138        for ((x0f, y0f), (x1f, y1f)) in iter {
139            let x0 = x0f as i32;
140            let y0 = y0f as i32;
141            let x1 = x1f as i32;
142            let y1 = y1f as i32;
143
144            // skip fully horizontal edges
145            if y0 == y1 {
146                continue;
147            }
148
149            // y ranges over the scanlines this edge crosses
150            let (ymin, ymax) = (y0.min(y1), y0.max(y1));
151            let dx = x1 - x0;
152            let dy = y1 - y0;
153
154            for y in ymin..ymax {
155                // parametric t along the edge at integer y
156                let t = f64::from(y - y0) / f64::from(dy);
157                let x = t.mul_add(f64::from(dx), f64::from(x0)).floor() as u32;
158                scanlines.entry(y as u32).or_default().push(x);
159            }
160        }
161    }
162
163    // Fill in between pairs of intersections on each scanline
164    for (y, mut xs) in scanlines {
165        xs.sort_unstable();
166        // take (x_start, x_end) from each pair of sorted intersections
167        for pair in xs.chunks(2).filter(|c| c.len() == 2) {
168            let x_start = pair[0];
169            let x_end = pair[1];
170            for x in x_start..x_end {
171                tiles_set.insert(Tile::new(x, y, zoom));
172            }
173        }
174    }
175}
176
177fn geom2tiles(geom: &geojson::Geometry, zoom: u8) -> Result<Vec<Tile>> {
178    let mut tiles_set = HashSet::new();
179    let res = match &geom.value {
180        geojson::Value::Point(coords) => {
181            let tile = tile(coords[0], coords[1], zoom, None)?;
182            tiles_set.insert(tile);
183            Ok::<(), UtilesCoverError>(())
184        }
185        geojson::Value::MultiPoint(coords_list) => {
186            coords_list
187                .iter()
188                .map(|coords| {
189                    tile(coords[0], coords[1], zoom, None)
190                        .map_err(UtilesCoverError::from)
191                })
192                .collect::<Result<HashSet<_>>>()
193                .map(|set| {
194                    tiles_set.extend(set);
195                })?;
196            Ok(())
197        }
198        geojson::Value::LineString(coords_list) => {
199            let coords: Vec<(f64, f64)> =
200                coords_list.iter().map(|c| (c[0], c[1])).collect();
201            line_string_cover(&mut tiles_set, &coords, zoom, None);
202            Ok(())
203        }
204        geojson::Value::MultiLineString(coords_lists) => {
205            for coords_list in coords_lists {
206                let coords: Vec<(f64, f64)> =
207                    coords_list.iter().map(|c| (c[0], c[1])).collect();
208                line_string_cover(&mut tiles_set, &coords, zoom, None);
209            }
210            Ok(())
211        }
212        geojson::Value::Polygon(coords_lists) => {
213            let coords: Vec<Vec<(f64, f64)>> = coords_lists
214                .iter()
215                .map(|ring| ring.iter().map(|c| (c[0], c[1])).collect())
216                .collect();
217            polygon_cover(&mut tiles_set, &coords, zoom);
218            Ok(())
219        }
220        geojson::Value::MultiPolygon(coords_list_of_lists) => {
221            for coords_lists in coords_list_of_lists {
222                let coords: Vec<Vec<(f64, f64)>> = coords_lists
223                    .iter()
224                    .map(|ring| ring.iter().map(|c| (c[0], c[1])).collect())
225                    .collect();
226                polygon_cover(&mut tiles_set, &coords, zoom);
227            }
228            Ok(())
229        }
230        geojson::Value::GeometryCollection(gjcoll) => {
231            for geom in gjcoll {
232                let recurse_res = geom2tiles(geom, zoom)?;
233                tiles_set.extend(recurse_res);
234            }
235            Ok(())
236        }
237    };
238    res?;
239    let tiles_vec = tiles_set.into_iter().collect();
240    Ok(tiles_vec)
241}
242
243pub struct GeojsonCoverOptions {
244    pub zoom: u8,
245    pub minzoom: Option<u8>,
246}
247
248impl From<u8> for GeojsonCoverOptions {
249    fn from(zoom: u8) -> Self {
250        Self {
251            zoom,
252            minzoom: None,
253        }
254    }
255}
256
257impl From<(u8, u8)> for GeojsonCoverOptions {
258    fn from((zoom, minzoom): (u8, u8)) -> Self {
259        Self {
260            zoom,
261            minzoom: Some(minzoom),
262        }
263    }
264}
265
266/// Convert a `GeoJSON` object to a set of tiles at the specified zoom level.
267///
268/// # Errors
269///
270/// If the `GeoJSON` object is invalid or if the conversion fails due to
271/// projecting coordinate issues.
272pub fn geojson2tiles<TOpts>(gj: &GeoJson, opts: TOpts) -> Result<HashSet<Tile>>
273where
274    TOpts: Into<GeojsonCoverOptions>,
275{
276    let opts = opts.into();
277    let mut tiles: HashSet<Tile> = HashSet::new();
278    match gj {
279        GeoJson::FeatureCollection(ctn) => {
280            for feature in &ctn.features {
281                if let Some(ref geom) = feature.geometry {
282                    let cov = geom2tiles(geom, opts.zoom)?;
283                    tiles.extend(cov);
284                }
285            }
286        }
287        GeoJson::Feature(feature) => {
288            if let Some(ref geom) = feature.geometry {
289                let cov = geom2tiles(geom, opts.zoom)?;
290                tiles.extend(cov);
291            }
292        }
293        GeoJson::Geometry(geom) => {
294            let cov = geom2tiles(geom, opts.zoom)?;
295            tiles.extend(cov);
296        }
297    }
298    match opts.minzoom {
299        Some(z) => {
300            let cov = simplify(&tiles, Some(z));
301            Ok(cov)
302        }
303        None => Ok(tiles),
304    }
305}