polylabel_mini/
lib.rs

1//! This crate provides a Rust implementation of the [Polylabel](https://github.com/mapbox/polylabel) algorithm
2//! for finding the optimum position of a polygon label.
3use std::cmp::Ordering;
4use std::collections::BinaryHeap;
5use std::f64;
6
7// TODO: polygon.contains(point)
8// TODO: point.euclidean_distance(polygon.exterior)
9// area()
10// centroid()
11// bounding_rect()
12
13pub const COORD_PRECISION: f64 = 1e-1; // 0.1m
14
15#[derive(Debug, Copy, Clone)]
16pub struct Point {
17    pub x: f64,
18    pub y: f64,
19}
20
21impl PartialEq for Point {
22    fn eq(&self, other: &Point) -> bool {
23        (self.x - other.x).abs() < COORD_PRECISION &&
24        (self.y - other.y).abs() < COORD_PRECISION
25    }
26}
27
28impl Point {
29    fn euclidean_distance_line_string(&self, line_string: &LineString) -> f64 {
30        // No need to continue if the point is on the LineString, or it's empty
31        if line_string_contains_point(line_string, *self) || line_string.points.is_empty() {
32            return 0.0;
33        }
34
35        line_string.points.windows(2)
36            .map(|line| line_segment_distance(*self, line[0], line[1]))
37            .fold(f64::MAX, |accum, val| accum.min(val))
38    }
39}
40
41fn line_segment_distance(point: Point, start: Point, end: Point) -> f64
42{
43    if start == end {
44        return euclidean_length_2(point, start);
45    }
46    let dx = dx(start, end);
47    let dy = dy(start, end);
48    let r = ((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx.powi(2) + dy.powi(2));
49
50    if r <= 0.0 {
51        euclidean_length_2(point, start)
52    } else if r >= 1.0 {
53        euclidean_length_2(point, end)
54    } else {
55        let s = ((start.y - point.y) * dx - (start.x - point.x) * dy) / (dx * dx + dy * dy);
56        s.abs() * dx.hypot(dy)
57    }
58}
59
60fn line_string_contains_point(line_string: &LineString, point: Point) -> bool {
61    // LineString without points
62    if line_string.points.is_empty() {
63        return false;
64    }
65
66    // Check if point is a vertex
67    if line_string.points.iter().any(|p| point_contains_point(*p, point)) {
68        return true;
69    }
70
71    for line in line_string.points.windows(2) {
72        let start = line[0];
73        let end = line[1];
74        if {
75            (start.y == end.y)
76         && (start.y == point.y)
77         && (point.x > start.x.min(end.x))
78         && (point.x < start.x.max(end.x))
79        } || {
80            (start.x == end.x)
81         && (start.x == point.x)
82         && (point.y > start.y.min(end.y))
83         && (point.y < start.y.max(end.y))
84        } {
85            return true;
86        }
87    }
88
89    false
90}
91
92fn point_contains_point(a: Point, b: Point) -> bool {
93    euclidean_length_2(a, b) < COORD_PRECISION
94}
95
96#[derive(Debug, Clone)]
97pub struct LineString {
98    pub points: Vec<Point>,
99}
100
101fn centroid_points(start: Point, end: Point) -> Point {
102    let two = 2.0;
103    let x = start.x + dx(start, end) / two;
104    let y = start.y + dy(start, end) / two;
105    Point { x, y }
106}
107
108impl LineString {
109    pub fn area(&self) -> f64 {
110        get_linestring_area(&self)
111    }
112
113    pub fn centroid(&self) -> Option<Point> {
114        // The Centroid of a LineString is the mean of the middle of the segment
115        // weighted by the length of the segments.
116
117        if self.points.is_empty() {
118            return None;
119        }
120
121        if self.points.len() == 1 {
122            Some(self.points[0])
123        } else {
124            let (sum_x, sum_y, total_length) =
125                self.points.windows(2)
126                    .fold((0.0_f64, 0.0_f64, 0.0_f64), |accum, line| {
127                        let segment_len = euclidean_length_2(line[0], line[1]);
128                        let line_center = centroid_points(line[0], line[1]);
129                        (
130                            accum.0 + segment_len * line_center.x,
131                            accum.1 + segment_len * line_center.y,
132                            accum.2 + segment_len,
133                        )
134                    });
135            Some(Point { x: sum_x / total_length, y: sum_y / total_length })
136        }
137    }
138}
139
140fn dx(a: Point, b: Point) -> f64 {
141    b.x - a.x
142}
143
144fn dy(a: Point, b: Point) -> f64 {
145    b.y - a.y
146}
147
148fn euclidean_length_2(a: Point, b: Point) -> f64 {
149    dx(a, b).hypot(dy(a, b))
150}
151
152#[derive(Debug, Clone)]
153pub struct Polygon {
154    pub exterior: LineString,
155    pub interiors: Vec<LineString>,
156}
157
158#[derive(PartialEq, Eq)]
159enum PointPosition {
160    Inside,
161    Outside,
162    OnBoundary,
163}
164
165impl Polygon {
166    fn contains(&self, p: &Point) -> bool {
167        match get_position(*p, &self.exterior) {
168            PointPosition::OnBoundary | PointPosition::Outside => false,
169            _ => self
170                .interiors
171                .iter()
172                .all(|ls| get_position(*p, ls) == PointPosition::Outside),
173        }
174    }
175
176    fn area(&self) -> f64 {
177        let area_exterior = get_linestring_area(&self.exterior);
178        let area_interior: f64 = self.interiors.iter().map(|line| get_linestring_area(line)).sum();
179        area_exterior - area_interior
180    }
181
182    // Calculate the centroid of a Polygon.
183    // We distinguish between a simple polygon, which has no interior rings (holes),
184    // and a complex polygon, which has one or more interior rings.
185    // A complex polygon's centroid is the weighted average of its
186    // exterior shell centroid and the centroids of the interior ring(s).
187    // Both the shell and the ring(s) are considered simple polygons for the purposes of
188    // this calculation.
189    //
190    // See here for a formula: http://math.stackexchange.com/a/623849
191    // See here for detail on alternative methods: https://fotino.me/calculating-centroids/
192    fn centroid(&self) -> Option<Point> {
193
194        if self.exterior.points.is_empty() {
195            return None;
196        } else if self.exterior.points.len() == 1 {
197            return Some(self.exterior.points[0]);
198        }
199
200        let external_centroid = simple_polygon_centroid(&self.exterior)?;
201
202        if !self.interiors.is_empty() {
203            let external_area = simple_polygon_area(&self.exterior).abs();
204            // accumulate interior Polygons
205            let (totals_x, totals_y, internal_area) = self
206                .interiors
207                .iter()
208                .filter_map(|ring| {
209                    let area = simple_polygon_area(ring).abs();
210                    let centroid = simple_polygon_centroid(ring)?;
211                    Some((centroid.x * area, centroid.y * area, area))
212                })
213                .fold((0.0_f64, 0.0_f64, 0.0_f64), |accum, val| {
214                    (accum.0 + val.0, accum.1 + val.1, accum.2 + val.2)
215                });
216
217            return Some(Point {
218                x: ((external_centroid.x * external_area) - totals_x) / (external_area - internal_area),
219                y: ((external_centroid.y * external_area) - totals_y) / (external_area - internal_area),
220            });
221        }
222
223        Some(external_centroid)
224    }
225
226    fn bounding_rect(&self) -> Option<Rect> {
227        get_bounding_rect(&self.exterior)
228    }
229}
230
231
232struct Rect {
233    pub min: Point,
234    pub max: Point,
235}
236
237fn get_bounding_rect(line_string: &LineString) -> Option<Rect> {
238    if line_string.points.is_empty() {
239        return None;
240    }
241    let first_point = line_string.points[0];
242
243    let mut min_x = first_point.x;
244    let mut max_x = first_point.x;
245    let mut min_y = first_point.y;
246    let mut max_y = first_point.y;
247
248    for point in line_string.points.iter() {
249        min_x = min_x.min(point.x);
250        max_x = max_x.max(point.x);
251        min_y = min_y.min(point.y);
252        max_y = max_y.max(point.y);
253    }
254
255    Some(Rect {
256        min: Point { x: min_x, y: min_y },
257        max: Point { x: max_x, y: max_y },
258    })
259}
260
261fn simple_polygon_area(line_string: &LineString) -> f64 {
262    if line_string.points.is_empty() || line_string.points.len() == 1 {
263        return 0.0;
264    }
265
266    line_string.points.windows(2).map(|line| determinant(line[0], line[1])).sum::<f64>() / 2.0_f64
267}
268
269fn get_linestring_area(line_string: &LineString) -> f64 {
270    line_string.points.windows(2).map(|line| {
271        determinant(line[0], line[1])
272    }).sum()
273}
274
275fn determinant(start: Point, end: Point) -> f64 {
276    start.x * end.y - start.y * end.x
277}
278
279fn simple_polygon_centroid(line_string: &LineString) -> Option<Point> {
280    let area = get_linestring_area(line_string);
281    if area == 0.0 {
282        // if the polygon is flat (area = 0), it is considered as a linestring
283        return line_string.centroid();
284    }
285    let (sum_x, sum_y) = line_string.points.windows(2)
286        .fold((0.0_f64, 0.0_f64), |accum, line| {
287            let start = line[0];
288            let end = line[1];
289            let tmp = determinant(start, end);
290            (
291                accum.0 + ((end.x + start.x) * tmp),
292                accum.1 + ((end.y + start.y) * tmp),
293            )
294        });
295
296    let six = 6.0;
297    Some(Point { x: sum_x / (six * area), y: sum_y / (six * area) })
298}
299
300/// Calculate the position of `Point` p relative to a linestring
301fn get_position(p: Point, linestring: &LineString) -> PointPosition {
302
303    // See:
304    // http://geospatialpython.com/search?updated-min=2011-01-01T00:00:00-06:00&updated-max=2012-01-01T00:00:00-06:00&max-results=19
305
306    // LineString without points
307    if linestring.points.is_empty() {
308        return PointPosition::Outside;
309    }
310    // Point is on linestring
311    if line_string_contains_point(&linestring, p) {
312        return PointPosition::OnBoundary;
313    }
314
315    let mut xints = 0.0_f64;
316    let mut crossings = 0;
317
318    for line in linestring.points.windows(2) {
319        let start = line[0];
320        let end = line[1];
321        if p.y > start.y.min(end.y)
322            && p.y <= start.y.max(end.y)
323            && p.x <= start.x.max(end.x)
324        {
325            if start.y != end.y {
326                xints = (p.y - start.y) * (end.x - start.x)
327                    / (end.y - start.y)
328                    + start.x;
329            }
330            if (start.x == end.x) || (p.x <= xints) {
331                crossings += 1;
332            }
333        }
334    }
335
336    if crossings % 2 == 1 {
337        PointPosition::Inside
338    } else {
339        PointPosition::Outside
340    }
341}
342
343
344/// Represention of a Quadtree node's cells. A node contains four Qcells.
345#[derive(Debug)]
346struct Qcell {
347    // The cell's centroid
348    centroid: Point,
349    // Half of the parent node's extent
350    extent: f64,
351    // Distance from centroid to polygon
352    distance: f64,
353    // Maximum distance to polygon within a cell
354    max_distance: f64,
355}
356
357impl Qcell {
358    fn new(x: f64, y: f64, h: f64, distance: f64, max_distance: f64) -> Qcell {
359        Qcell {
360            centroid: Point { x, y },
361            extent: h,
362            distance,
363            max_distance,
364        }
365    }
366}
367
368impl PartialOrd for Qcell {
369    fn partial_cmp(&self, other: &Qcell) -> Option<Ordering> {
370        Some(self.cmp(other))
371    }
372}
373
374impl Ord for Qcell {
375    fn cmp(&self, other: &Qcell) -> std::cmp::Ordering {
376        self.max_distance.partial_cmp(&other.max_distance).unwrap()
377    }
378}
379
380impl PartialEq for Qcell {
381    fn eq(&self, other: &Qcell) -> bool {
382        (self.max_distance - other.max_distance).abs() < COORD_PRECISION
383    }
384}
385
386impl Eq for Qcell { }
387
388/// Signed distance from a Qcell's centroid to a Polygon's outline
389/// Returned value is negative if the point is outside the polygon's exterior ring
390fn signed_distance(x: f64, y: f64, polygon: &Polygon) -> f64 {
391    let point = Point { x, y };
392    let inside = polygon.contains(&point);
393    // Use LineString distance, because Polygon distance returns 0.0 for inside
394    let distance = point.euclidean_distance_line_string(&polygon.exterior);
395    if inside {
396        distance
397    } else {
398        -distance
399    }
400}
401
402/// Add a new Quadtree node made up of four `Qcell`s to the binary heap
403fn add_quad(
404    mpq: &mut BinaryHeap<Qcell>,
405    cell: &Qcell,
406    new_height: f64,
407    polygon: &Polygon,
408) {
409    let two = 2.0_f64;
410    let centroid_x = cell.centroid.x;
411    let centroid_y = cell.centroid.y;
412    for combo in &[
413        (centroid_x - new_height, centroid_y - new_height),
414        (centroid_x + new_height, centroid_y - new_height),
415        (centroid_x - new_height, centroid_y + new_height),
416        (centroid_x + new_height, centroid_y + new_height),
417    ] {
418        let mut new_dist = signed_distance(combo.0, combo.1, polygon);
419        mpq.push(Qcell::new(
420            combo.0,
421            combo.1,
422            new_height,
423            new_dist,
424            new_dist + new_height * two.sqrt(),
425        ));
426    }
427}
428
429/// Calculate a Polygon's ideal label position by calculating its ✨pole of inaccessibility✨
430///
431/// The calculation uses an [iterative grid-based algorithm](https://github.com/mapbox/polylabel#how-the-algorithm-works).
432///
433/// # Examples
434///
435/// ```
436/// use polylabel::polylabel;
437/// extern crate geo;
438/// use self::geo::{Point, LineString, Polygon};
439///
440/// // An approximate `L` shape
441/// let coords = vec![
442///    (0.0, 0.0),
443///    (4.0, 0.0),
444///    (4.0, 1.0),
445///    (1.0, 1.0),
446///    (1.0, 4.0),
447///    (0.0, 4.0),
448///    (0.0, 0.0)];
449///
450/// let poly = Polygon::new(coords.into(), vec![]);
451///
452/// // Its centroid lies outside the polygon
453/// assert_eq!(poly.centroid(), Point::new(1.3571428571428572, 1.3571428571428572));
454///
455/// let label_position = polylabel(&poly, &1.0);
456/// // Optimum label position is inside the polygon
457/// assert_eq!(label_position, Point::new(0.5625, 0.5625));
458/// ```
459///
460pub fn polylabel(polygon: &Polygon, tolerance: f64) -> Point {
461
462    // special case for degenerate polygons
463    if polygon.area() < 0.0 {
464        return Point { x: 0.0, y: 0.0 };
465    }
466
467    let two = 2.0_f64;
468
469    // Initial best cell values
470    let centroid = polygon.centroid().unwrap();
471    let bbox = polygon.bounding_rect().unwrap();
472    let width = bbox.max.x - bbox.min.x;
473    let height = bbox.max.y - bbox.min.y;
474    let cell_size = width.min(height);
475
476    // Special case for degenerate polygons
477    if cell_size == 0.0 {
478        return Point { x: bbox.min.x, y: bbox.min.y };
479    }
480
481    let mut h = cell_size / two;
482    let distance = signed_distance(centroid.x, centroid.y, polygon);
483    let max_distance = distance + 0.0 * two.sqrt();
484
485    let mut best_cell = Qcell::new(
486        centroid.x,
487        centroid.y,
488        0.0,
489        distance,
490        max_distance,
491    );
492
493    // special case for rectangular polygons
494    let bbox_cell_dist = signed_distance(
495        bbox.min.x + width / two,
496        bbox.min.y + height / two,
497        polygon,
498    );
499    let bbox_cell = Qcell {
500        centroid: Point { x: bbox.min.x + width / two, y: bbox.min.y + height / two },
501        extent: 0.0,
502        distance: bbox_cell_dist,
503        max_distance: bbox_cell_dist + 0.0 * two.sqrt(),
504    };
505
506    if bbox_cell.distance > best_cell.distance {
507        best_cell = bbox_cell;
508    }
509
510    // Priority queue
511    let mut cell_queue: BinaryHeap<Qcell> = BinaryHeap::new();
512    // Build an initial quadtree node, which covers the Polygon
513    let mut x = bbox.min.x;
514    let mut y;
515
516    while x < bbox.max.x {
517        y = bbox.min.y;
518        while y < bbox.max.y {
519            let latest_dist = signed_distance(x + h, y + h, polygon);
520            cell_queue.push(Qcell {
521                centroid: Point { x: x + h, y: y + h },
522                extent: h,
523                distance: latest_dist,
524                max_distance: latest_dist + h * two.sqrt(),
525            });
526            y = y + cell_size;
527        }
528        x = x + cell_size;
529    }
530
531
532    // Now try to find better solutions
533    while !cell_queue.is_empty() {
534        let cell = cell_queue.pop().unwrap();
535        // Update the best cell if we find a cell with greater distance
536        if cell.distance > best_cell.distance {
537            best_cell.centroid = Point { x: cell.centroid.x, y: cell.centroid.y };
538            best_cell.extent = cell.extent;
539            best_cell.distance = cell.distance;
540            best_cell.max_distance = cell.max_distance;
541        }
542        // Bail out of this iteration if we can't find a better solution
543        if cell.max_distance - best_cell.distance <= tolerance {
544            continue;
545        }
546        // Otherwise, add a new quadtree node and start again
547        h = cell.extent / two;
548        add_quad(&mut cell_queue, &cell, h, polygon);
549    }
550
551    // We've exhausted the queue, so return the best solution we've found
552    best_cell.centroid
553}