gistools/tools/
polylabel.rs

1use crate::data_structures::PriorityQueue;
2use alloc::vec::Vec;
3use core::f64::consts::SQRT_2;
4use libm::{fmax, fmin, sqrt};
5use s2json::{MValueCompatible, VectorMultiPolygon, VectorPoint, VectorPolygon};
6
7/// The metadata inserted into the Vector Feature
8#[derive(Debug, Default, MValueCompatible, Clone)]
9pub struct PolyLabelMetadata {
10    /// The distance to the label
11    pub distance: f64,
12}
13impl PolyLabelMetadata {
14    /// Create a new PolyLabelMetadata
15    pub fn new(distance: f64) -> PolyLabelMetadata {
16        PolyLabelMetadata { distance }
17    }
18}
19
20/// A cell in the polygon label algorithm
21#[derive(Debug, Default, Clone)]
22pub struct PolyLabelCell {
23    /// cell center x
24    pub x: f64,
25    /// cell center y
26    pub y: f64,
27    /// half the cell size
28    pub h: f64,
29    /// distance from cell center to polygon
30    pub d: f64,
31    /// max distance to polygon within a cell
32    pub max: f64,
33}
34
35/// # Polylabels
36///
37/// ## Description
38/// Find the labels for a collection of vector polygons
39///
40/// ## Usage
41///
42/// ```rust
43/// use gistools::tools::{PolyLabelMetadata, polylabels};
44/// use s2json::VectorPoint;
45///
46/// let data: Vec<Vec<Vec<VectorPoint<()>>>> = vec![vec![vec![]]];
47/// let emp = polylabels(&data, None);
48///
49/// assert_eq!(emp, vec![VectorPoint::new_xy(0., 0., Some(PolyLabelMetadata::new(0.)))]);
50/// ```
51///
52/// ## Links
53/// - <https://sites.google.com/site/polesofinaccessibility/>
54///
55/// ## Parameters
56/// - `polygons`: the vector multi-polygon to find the label for
57/// - `precision`: the precision of the label
58///
59/// ## Returns
60/// The collection of label positions and the distances to the labels
61pub fn polylabels<M: Clone>(
62    polygons: &VectorMultiPolygon<M>,
63    precision: Option<f64>,
64) -> Vec<VectorPoint<PolyLabelMetadata>> {
65    polygons.iter().map(|polygon| polylabel(polygon, precision)).collect()
66}
67
68/// # Polylabel
69///
70/// ## Description
71/// Find the label for a vector polygon
72///
73/// ## Usage
74///
75/// ```rust
76/// use gistools::tools::{PolyLabelMetadata, polylabel};
77/// use s2json::VectorPoint;
78///
79/// let data: Vec<Vec<VectorPoint<()>>> = vec![vec![]];
80/// let emp = polylabel(&data, None);
81///
82/// assert_eq!(emp, VectorPoint::new_xy(0., 0., Some(PolyLabelMetadata::new(0.))));
83/// ```
84///
85/// ## Links
86/// - <https://sites.google.com/site/polesofinaccessibility/>
87///
88/// ## Parameters
89/// - `polygon`: the vector polygon to find the label for
90/// - `precision`: the precision of the label
91///
92/// ## Returns
93/// The label position and the distance to the label
94pub fn polylabel<M: Clone>(
95    polygon: &VectorPolygon<M>,
96    precision: Option<f64>,
97) -> VectorPoint<PolyLabelMetadata> {
98    let precision = precision.unwrap_or(1.0);
99    // find the bounding box of the outer ring
100    let mut min_x = f64::MAX;
101    let mut min_y = f64::MAX;
102    let mut max_x = f64::MIN;
103    let mut max_y = f64::MIN;
104
105    if polygon.is_empty() || polygon[0].is_empty() {
106        return VectorPoint::new_xy(0.0, 0.0, Some(PolyLabelMetadata::default()));
107    }
108
109    for VectorPoint { x, y, .. } in &polygon[0] {
110        if *x < min_x {
111            min_x = *x;
112        }
113        if *y < min_y {
114            min_y = *y;
115        }
116        if *x > max_x {
117            max_x = *x;
118        }
119        if *y > max_y {
120            max_y = *y;
121        }
122    }
123
124    let width = max_x - min_x;
125    let height = max_y - min_y;
126    let cell_size = fmax(precision, fmin(width, height));
127
128    if cell_size == precision {
129        return VectorPoint::new_xy(min_x, min_y, Some(PolyLabelMetadata::default()));
130    }
131
132    // a priority queue of cells in order of their "potential" (max distance to polygon)
133    let mut cell_queue =
134        PriorityQueue::<PolyLabelCell>::new(|a: &PolyLabelCell, b: &PolyLabelCell| {
135            b.max.partial_cmp(&a.max).unwrap_or(core::cmp::Ordering::Equal)
136        });
137
138    // take centroid as the first best guess
139    let mut best_cell = get_centroid_cell(polygon);
140
141    // second guess: bounding box centroid
142    let bbox_cell = build_cell(min_x + width / 2., min_y + height / 2., 0., polygon);
143    if bbox_cell.d > best_cell.d {
144        best_cell = bbox_cell;
145    }
146
147    // add a cell to the queue
148    let potentially_queue =
149        |x: f64,
150         y: f64,
151         h: f64,
152         best_cell: &mut PolyLabelCell,
153         cell_queue: &mut PriorityQueue<PolyLabelCell>| {
154            let cell = build_cell(x, y, h, polygon);
155            if cell.max > best_cell.d + precision {
156                cell_queue.push(cell.clone());
157            }
158            // update the best cell if we found a better one
159            if cell.d > best_cell.d {
160                *best_cell = cell;
161            }
162        };
163
164    // cover polygon with initial cells
165    let mut h = cell_size / 2.;
166    let mut x = min_x;
167    while x < max_x {
168        let mut y = min_y;
169        while y < max_y {
170            potentially_queue(x + h, y + h, h, &mut best_cell, &mut cell_queue);
171            y += cell_size;
172        }
173        x += cell_size;
174    }
175
176    loop {
177        // pick the most promising cell from the queue
178        let cell = cell_queue.pop();
179        if cell.is_none() {
180            break;
181        }
182        let PolyLabelCell { max, x, y, h: ch, .. } = &cell.unwrap();
183
184        // do not drill down further if there's no chance of a better solution
185        if max - best_cell.d <= precision {
186            break;
187        }
188
189        // split the cell into four cells
190        h = ch / 2.;
191        potentially_queue(x - h, y - h, h, &mut best_cell, &mut cell_queue);
192        potentially_queue(x + h, y - h, h, &mut best_cell, &mut cell_queue);
193        potentially_queue(x - h, y + h, h, &mut best_cell, &mut cell_queue);
194        potentially_queue(x + h, y + h, h, &mut best_cell, &mut cell_queue);
195    }
196
197    VectorPoint::new_xy(best_cell.x, best_cell.y, Some(PolyLabelMetadata::new(best_cell.d)))
198}
199
200/// build a cell
201///
202/// ## Parameters
203/// - `x`: the cell x coordinate
204/// - `y`: the cell y coordinate
205/// - `h`: half the cell size
206/// - `polygon`: the vector polygon
207///
208/// ## Returns
209/// The cell
210fn build_cell<M: Clone>(x: f64, y: f64, h: f64, polygon: &VectorPolygon<M>) -> PolyLabelCell {
211    let d = point_to_polygon_dist(x, y, polygon);
212    PolyLabelCell { x, y, h, d, max: d + h * SQRT_2 }
213}
214
215/// signed distance from point to polygon outline (negative if point is outside)
216///
217/// ## Parameters
218/// - `x`: the point x coordinate
219/// - `y`: the point y coordinate
220/// - `polygon`: the vector polygon to check
221///
222/// ## Returns
223/// The signed distance
224fn point_to_polygon_dist<M: Clone>(x: f64, y: f64, polygon: &VectorPolygon<M>) -> f64 {
225    let mut inside = false;
226    let mut min_dist_sq = f64::MAX;
227
228    for ring in polygon {
229        let len = ring.len();
230        let mut j = len - 1;
231        for i in 0..len {
232            let a = &ring[i];
233            let b = &ring[j];
234
235            if (a.y > y) != (b.y > y) && x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x {
236                inside = !inside;
237            }
238
239            min_dist_sq = fmin(min_dist_sq, get_seg_dist_sq(x, y, a, b));
240            j = i; // Update j to the previous i (j = i++)
241        }
242    }
243
244    if min_dist_sq == 0. { 0. } else { (if inside { 1. } else { -1. }) * sqrt(min_dist_sq) }
245}
246
247/// get polygon centroid
248/// return the centroid as a cell
249fn get_centroid_cell<M: Clone>(polygon: &VectorPolygon<M>) -> PolyLabelCell {
250    let mut area = 0.;
251    let mut x = 0.;
252    let mut y = 0.;
253    let points = &polygon[0];
254
255    let len = points.len();
256    let mut j = len - 1;
257    for i in 0..len {
258        let a = &points[i];
259        let b = &points[j];
260        let f = a.x * b.y - b.x * a.y;
261        x += (a.x + b.x) * f;
262        y += (a.y + b.y) * f;
263        area += f * 3.;
264        j = i; // Update j to the previous i (j = i++)
265    }
266    let centroid = build_cell(x / area, y / area, 0., polygon);
267    if area == 0. || centroid.d < 0. {
268        build_cell(points[0].x, points[0].y, 0., polygon)
269    } else {
270        centroid
271    }
272}
273
274/// get squared distance from a point to a segment AB
275/// return the squared distance
276fn get_seg_dist_sq<M: Clone>(px: f64, py: f64, a: &VectorPoint<M>, b: &VectorPoint<M>) -> f64 {
277    let mut x = a.x;
278    let mut y = a.y;
279    let mut dx = b.x - x;
280    let mut dy = b.y - y;
281
282    if dx != 0. || dy != 0. {
283        let t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
284
285        if t > 1. {
286            x = b.x;
287            y = b.y;
288        } else if t > 0. {
289            x += dx * t;
290            y += dy * t;
291        }
292    }
293
294    dx = px - x;
295    dy = py - y;
296
297    dx * dx + dy * dy
298}