gistools/geometry/
simplify.rs

1use crate::{VectorGeometry, VectorLineString, VectorPoint};
2
3use libm::pow;
4
5use alloc::vec;
6
7impl VectorGeometry {
8    /// Build sqdistances for a vector geometry
9    pub fn build_sq_dists(&mut self, tolerance: f64, maxzoom: Option<u8>) {
10        build_sq_dists(self, tolerance, maxzoom);
11    }
12
13    /// Simplify the geometry to have a tolerance which will be relative to the tile's zoom level.
14    pub fn simplify(&mut self, tolerance: f64, zoom: u8, maxzoom: Option<u8>) {
15        simplify(self, tolerance, zoom, maxzoom);
16    }
17}
18
19/// build sqdistances for line vector data
20pub fn build_sq_dists(geometry: &mut VectorGeometry, tolerance: f64, maxzoom: Option<u8>) {
21    let maxzoom = maxzoom.unwrap_or(16);
22    let tol = pow(tolerance / ((1 << maxzoom) as f64 * 4_096.), 2.);
23
24    match geometry {
25        VectorGeometry::LineString(geo) => {
26            let coords = &mut geo.coordinates;
27            build_sq_dist(coords, 0, coords.len() - 1, tol);
28        }
29        VectorGeometry::Polygon(geo) | VectorGeometry::MultiLineString(geo) => {
30            let coords = &mut geo.coordinates;
31            coords.iter_mut().for_each(|line| build_sq_dist(line, 0, line.len() - 1, tol));
32        }
33        VectorGeometry::MultiPolygon(geo) => {
34            let coords = &mut geo.coordinates;
35            coords.iter_mut().for_each(|polygon| {
36                polygon.iter_mut().for_each(|line| build_sq_dist(line, 0, line.len() - 1, tol))
37            });
38        }
39        _ => {}
40    }
41}
42
43/// calculate simplification of line vector data using
44/// optimized Douglas-Peucker algorithm
45fn build_sq_dist(coords: &mut VectorLineString, first: usize, last: usize, sq_tolerance: f64) {
46    coords[first].t = Some(1.);
47    _build_sq_dist(coords, first, last, sq_tolerance);
48    coords[last].t = Some(1.);
49}
50
51/// calculate simplification of line vector data using
52/// optimized Douglas-Peucker algorithm
53fn _build_sq_dist(coords: &mut VectorLineString, first: usize, last: usize, sq_tolerance: f64) {
54    let mid = (last - first) >> 1;
55    let mut max_sq_dist = sq_tolerance;
56    let mut min_pos_to_mid = last - first;
57    let mut index: Option<usize> = None;
58
59    let VectorPoint { x: ax, y: ay, .. } = coords[first];
60    let VectorPoint { x: bx, y: by, .. } = coords[last];
61
62    let mut i = first;
63    while i < last {
64        let VectorPoint { x, y, .. } = coords[i];
65        let d = get_sq_seg_dist(x, y, ax, ay, bx, by);
66
67        if d > max_sq_dist {
68            index = Some(i);
69            max_sq_dist = d;
70        } else if d == max_sq_dist {
71            // a workaround to ensure we choose a pivot close to the middle of the list,
72            // reducing recursion depth, for certain degenerate inputs
73            let pos_to_mid = isize::abs(i as isize - mid as isize) as usize;
74            if pos_to_mid < min_pos_to_mid {
75                index = Some(i);
76                min_pos_to_mid = pos_to_mid;
77            }
78        }
79
80        i += 1;
81    }
82
83    if max_sq_dist > sq_tolerance {
84        if let Some(index) = index {
85            if index - first > 1 {
86                _build_sq_dist(coords, first, index, sq_tolerance);
87            }
88            coords[index].t = Some(max_sq_dist);
89            if last - index > 1 {
90                _build_sq_dist(coords, index, last, sq_tolerance);
91            }
92        }
93    }
94}
95
96/// square distance from a point to a segment
97fn get_sq_seg_dist(ps: f64, pt: f64, s: f64, t: f64, bs: f64, bt: f64) -> f64 {
98    let mut s = s;
99    let mut t = t;
100    let mut ds = bs - s;
101    let mut dt = bt - t;
102
103    if ds != 0. || dt != 0. {
104        let m = ((ps - s) * ds + (pt - t) * dt) / (ds * ds + dt * dt);
105
106        if m > 1. {
107            s = bs;
108            t = bt;
109        } else if m > 0. {
110            s += ds * m;
111            t += dt * m;
112        }
113    }
114
115    ds = ps - s;
116    dt = pt - t;
117
118    ds * ds + dt * dt
119}
120
121/// Simplify a vector geometry
122pub fn simplify(geometry: &mut VectorGeometry, tolerance: f64, zoom: u8, maxzoom: Option<u8>) {
123    let maxzoom = maxzoom.unwrap_or(16);
124    let zoom_tol = if zoom >= maxzoom { 0. } else { tolerance / ((1 << zoom) as f64 * 4_096.) };
125    match geometry {
126        VectorGeometry::LineString(geo) => {
127            geo.coordinates = simplify_line(&geo.coordinates, zoom_tol, false, false);
128        }
129        VectorGeometry::Polygon(geo) | VectorGeometry::MultiLineString(geo) => {
130            geo.coordinates = geo
131                .coordinates
132                .iter()
133                .map(|line| simplify_line(line, zoom_tol, false, false))
134                .collect();
135        }
136        VectorGeometry::MultiPolygon(geo) => {
137            geo.coordinates = geo
138                .coordinates
139                .iter()
140                .map(|polygon| {
141                    polygon.iter().map(|line| simplify_line(line, zoom_tol, true, true)).collect()
142                })
143                .collect();
144        }
145        _ => (),
146    }
147}
148
149/// simplified a vector line
150fn simplify_line(
151    line: &VectorLineString,
152    tolerance: f64,
153    is_polygon: bool,
154    is_outer: bool,
155) -> VectorLineString {
156    let sq_tolerance = tolerance * tolerance;
157    let size = line.len() as f64;
158    if tolerance > 0. && size < (if is_polygon { sq_tolerance } else { tolerance }) {
159        return line.clone();
160    }
161
162    let mut ring: VectorLineString = vec![];
163    for point in line {
164        if tolerance == 0. || (point.t.unwrap_or(0.0)) > sq_tolerance {
165            ring.push(point.clone());
166        }
167    }
168    if is_polygon {
169        rewind(&mut ring, is_outer);
170    }
171
172    ring
173}
174
175/// In place adjust the ring if necessary
176pub fn rewind(ring: &mut VectorLineString, clockwise: bool) {
177    let len = ring.len();
178    let mut area: f64 = 0.;
179    let mut i = 0;
180    let mut j = len - 2;
181    while i < len {
182        area += (ring[i].x - ring[j].x) * (ring[i].y + ring[j].y);
183        j = i;
184        i += 2;
185    }
186    i = 0;
187    if (area > 0.) == clockwise {
188        let len_half = len / 2;
189        while i < len_half {
190            ring.swap(i, len - i - 1);
191            i += 2;
192        }
193    }
194}