cheap_ruler/
lib.rs

1//! # cheap-ruler
2//!
3//! A collection of very fast approximations to common geodesic measurements.
4//! Useful for performance-sensitive code that measures things on a city scale.
5//!
6//! This is a port of the cheap-ruler JS library and cheap-ruler-cpp C++ library
7//! into safe Rust.
8//!
9//! Note: WGS84 ellipsoid is used instead of the Clarke 1866 parameters used by
10//! the FCC formulas. See cheap-ruler-cpp#13 for more information.
11
12// Temporarily permit geo_types::Coordinate until geo-types 0.8
13#![allow(deprecated)]
14
15#[macro_use]
16extern crate geo_types;
17
18use geo_types::{Coordinate, LineString, Point, Polygon};
19use num_traits::Float;
20use std::f64;
21use std::fmt;
22use std::iter;
23use std::mem;
24
25mod distance_unit;
26mod point_on_line;
27mod rect;
28
29pub use distance_unit::DistanceUnit;
30pub use point_on_line::PointOnLine;
31pub use rect::Rect;
32
33const RE: f64 = 6378.137; // equatorial radius in km
34const FE: f64 = 1.0 / 298.257223563; // flattening
35const E2: f64 = FE * (2.0 - FE);
36
37/// A collection of very fast approximations to common geodesic measurements.
38/// Useful for performance-sensitive code that measures things on a city scale.
39/// Point coordinates are in the [x = longitude, y = latitude] form.
40#[allow(clippy::derive_partial_eq_without_eq)]
41#[derive(Debug, PartialEq, Clone)]
42pub struct CheapRuler<T>
43where
44    T: Float + fmt::Debug,
45{
46    kx: T,
47    ky: T,
48}
49
50impl<T> CheapRuler<T>
51where
52    T: Float + fmt::Debug,
53{
54    pub fn new(latitude: T, distance_unit: DistanceUnit) -> Self {
55        let one = T::one();
56        let e2 = T::from(E2).unwrap();
57
58        // Curvature formulas from https://en.wikipedia.org/wiki/Earth_radius#Meridional
59        let coslat = latitude.to_radians().cos();
60        let w2 = one / (one - e2 * (one - coslat * coslat));
61        let w = w2.sqrt();
62
63        // multipliers for converting longitude and latitude degrees into distance
64        let dkx = w * coslat; // based on normal radius of curvature
65        let dky = w * w2 * (one - e2); // based on meridonal radius of curvature
66
67        let (kx, ky) = calculate_multipliers(distance_unit, dkx, dky);
68
69        Self { kx, ky }
70    }
71
72    /// Creates a ruler object from tile coordinates (y and z). Convenient in
73    /// tile-reduce scripts
74    ///
75    /// # Arguments
76    ///
77    /// * `y` - y
78    /// * `z` - z
79    /// * `distance_unit` - Unit to express distances in
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use cheap_ruler::{CheapRuler, DistanceUnit};
85    /// let cr = CheapRuler::<f64>::from_tile(1567, 12, DistanceUnit::Meters);
86    /// ```
87    pub fn from_tile(y: u32, z: u32, distance_unit: DistanceUnit) -> Self {
88        assert!(z < 32);
89
90        let n = T::from(f64::consts::PI).unwrap()
91            * (T::one()
92                - T::from(2.0).unwrap()
93                    * (T::from(y).unwrap() + T::from(0.5).unwrap())
94                    / T::from(1u32 << z).unwrap());
95        let latitude = n.sinh().atan().to_degrees();
96
97        Self::new(latitude, distance_unit)
98    }
99
100    /// Calculates the square of the approximate distance between two
101    /// geographical points
102    ///
103    /// # Arguments
104    ///
105    /// * `a` - First point
106    /// * `b` - Second point
107    pub fn square_distance(&self, a: &Point<T>, b: &Point<T>) -> T {
108        let dx = long_diff(a.x(), b.x()) * self.kx;
109        let dy = (a.y() - b.y()) * self.ky;
110        dx.powi(2) + dy.powi(2)
111    }
112
113    /// Calculates the approximate distance between two geographical points
114    ///
115    /// # Arguments
116    ///
117    /// * `a` - First point
118    /// * `b` - Second point
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use cheap_ruler::{CheapRuler, DistanceUnit};
124    /// let cr = CheapRuler::new(44.7192003, DistanceUnit::Meters);
125    /// let dist = cr.distance(
126    ///   &(14.8901816, 44.7209699).into(),
127    ///   &(14.8905188, 44.7209699).into()
128    /// );
129    /// assert!(dist < 38.0);
130    /// ```
131    pub fn distance(&self, a: &Point<T>, b: &Point<T>) -> T {
132        self.square_distance(a, b).sqrt()
133    }
134
135    /// Returns the bearing between two points in angles
136    ///
137    /// # Arguments
138    ///
139    /// * `a` - First point
140    /// * `b` - Second point
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use cheap_ruler::{CheapRuler, DistanceUnit};
146    /// let cr = CheapRuler::new(44.7192003, DistanceUnit::Meters);
147    /// let bearing = cr.bearing(
148    ///   &(14.8901816, 44.7209699).into(),
149    ///   &(14.8905188, 44.7209699).into()
150    /// );
151    /// assert_eq!(bearing, 90.0);
152    /// ```
153    pub fn bearing(&self, a: &Point<T>, b: &Point<T>) -> T {
154        let dx = long_diff(b.x(), a.x()) * self.kx;
155        let dy = (b.y() - a.y()) * self.ky;
156
157        dx.atan2(dy).to_degrees()
158    }
159
160    /// Returns a new point given distance and bearing from the starting point
161    ///
162    /// # Arguments
163    ///
164    /// * `origin` - origin point
165    /// * `dist` - distance
166    /// * `bearing` - bearing
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use cheap_ruler::{CheapRuler, DistanceUnit};
172    /// let cr = CheapRuler::new(44.7192003, DistanceUnit::Meters);
173    /// let p1 = (14.8901816, 44.7209699).into();
174    /// let p2 = (14.8905188, 44.7209699).into();
175    /// let dist = cr.distance(&p1, &p2);
176    /// let bearing = cr.bearing(&p1, &p2);
177    /// let destination = cr.destination(&p1, dist, bearing);
178    ///
179    /// assert_eq!(destination.x(), p2.x());
180    /// assert_eq!(destination.y(), p2.y());
181    /// ```
182    pub fn destination(
183        &self,
184        origin: &Point<T>,
185        dist: T,
186        bearing: T,
187    ) -> Point<T> {
188        let a = bearing.to_radians();
189        self.offset(origin, a.sin() * dist, a.cos() * dist)
190    }
191
192    /// Returns a new point given easting and northing offsets (in ruler units)
193    /// from the starting point
194    ///
195    /// # Arguments
196    ///
197    /// * `origin` - point
198    /// * `dx` - easting
199    /// * `dy` - northing
200    pub fn offset(&self, origin: &Point<T>, dx: T, dy: T) -> Point<T> {
201        (origin.x() + dx / self.kx, origin.y() + dy / self.ky).into()
202    }
203
204    /// Given a line (an array of points), returns the total line distance.
205    ///
206    /// # Arguments
207    ///
208    /// * `points` - line of points
209    ///
210    /// # Example
211    ///
212    /// ```
213    /// use cheap_ruler::{CheapRuler, DistanceUnit};
214    /// use geo_types::LineString;
215    /// let cr = CheapRuler::new(50.458, DistanceUnit::Meters);
216    /// let line_string: LineString<f64> = vec![
217    ///     (-67.031, 50.458),
218    ///     (-67.031, 50.534),
219    ///     (-66.929, 50.534),
220    ///     (-66.929, 50.458)
221    /// ].into();
222    /// let length = cr.line_distance(&line_string);
223    /// ```
224    pub fn line_distance(&self, points: &LineString<T>) -> T {
225        let line_iter = points.0.iter().copied();
226
227        let left = iter::once(None).chain(line_iter.clone().map(Some));
228        left.zip(line_iter)
229            .map(|(a, b)| match a {
230                Some(a) => self.distance(&a.into(), &b.into()),
231                None => T::zero(),
232            })
233            // avoided using Iterator's sum() so that we don't have to require T
234            // to implement std::iter::Sum.
235            .fold(T::zero(), |acc, x| acc + x)
236    }
237
238    /// Given a polygon returns the area
239    ///
240    /// * `polygon` - Polygon
241    pub fn area(&self, polygon: &Polygon<T>) -> T {
242        let exterior_sum =
243            sum_area(&polygon.exterior().points().collect::<Vec<Point<T>>>());
244        let interiors_sum = polygon
245            .interiors()
246            .iter()
247            .map(|interior| {
248                sum_area(&interior.points().collect::<Vec<Point<T>>>())
249            })
250            .fold(T::zero(), |acc, x| acc + x);
251        let sum = exterior_sum - interiors_sum;
252        (sum.abs() / T::from(2.0).unwrap()) * self.kx * self.ky
253    }
254
255    /// Returns the point at a specified distance along the line
256    ///
257    /// # Arguments
258    ///
259    /// * `line` - Line
260    /// * `dist` - Distance along the line
261    pub fn along(&self, line: &LineString<T>, dist: T) -> Option<Point<T>> {
262        let line_len = line.0.len();
263        if line_len == 0 {
264            return None;
265        }
266
267        if dist <= T::zero() {
268            return Some(line[0].into());
269        }
270
271        let last_index = line_len - 1;
272        let mut sum = T::zero();
273        for i in 0..last_index {
274            let p0 = &line[i].into();
275            let p1 = &line[i + 1].into();
276            let d = self.distance(p0, p1);
277            sum = sum + d;
278            if sum > dist {
279                return Some(interpolate(p0, p1, (dist - (sum - d)) / d));
280            }
281        }
282        Some(line[last_index].into())
283    }
284
285    /// Returns the shortest distance between a point and a line segment given
286    /// with two points.
287    ///
288    /// # Arguments
289    ///
290    /// * `p` - Point to calculate the distance from
291    /// * `start` - Start point of line segment
292    /// * `end` - End point of line segment
293    pub fn point_to_segment_distance(
294        &self,
295        p: &Point<T>,
296        start: &Point<T>,
297        end: &Point<T>,
298    ) -> T {
299        let zero = T::zero();
300        let mut x = start.x();
301        let mut y = start.y();
302        let dx = long_diff(end.x(), x) * self.kx;
303        let dy = (end.y() - y) * self.ky;
304
305        if dx != zero || dy != zero {
306            let t = (long_diff(p.x(), x) * self.kx * dx
307                + (p.y() - y) * self.ky * dy)
308                / (dx * dx + dy * dy);
309            if t > T::one() {
310                x = end.x();
311                y = end.y();
312            } else if t > zero {
313                x = x + (dx / self.kx) * t;
314                y = y + (dy / self.ky) * t;
315            }
316        }
317        self.distance(p, &point!(x: x, y: y))
318    }
319
320    /// Returns a tuple of the form (point, index, t) where point is closest
321    /// point on the line from the given point, index is the start index of the
322    /// segment with the closest point, and t is a parameter from 0 to 1 that
323    /// indicates where the closest point is on that segment
324    ///
325    /// # Arguments
326    ///
327    /// * `line` - Line to compare with point
328    /// * `point` - Point to calculate the closest point on the line
329    pub fn point_on_line(
330        &self,
331        line: &LineString<T>,
332        point: &Point<T>,
333    ) -> Option<PointOnLine<T>> {
334        let zero = T::zero();
335        let mut min_dist = T::infinity();
336        let mut min_x = zero;
337        let mut min_y = zero;
338        let mut min_i = 0;
339        let mut min_t = zero;
340
341        let line_len = line.0.len();
342        if line_len == 0 {
343            return None;
344        }
345
346        for i in 0..line_len - 1 {
347            let mut t = zero;
348            let mut x = line[i].x;
349            let mut y = line[i].y;
350            let dx = long_diff(line[i + 1].x, x) * self.kx;
351            let dy = (line[i + 1].y - y) * self.ky;
352
353            if dx != zero || dy != zero {
354                t = (long_diff(point.x(), x) * self.kx * dx
355                    + (point.y() - y) * self.ky * dy)
356                    / (dx * dx + dy * dy);
357
358                if t > T::one() {
359                    x = line[i + 1].x;
360                    y = line[i + 1].y;
361                } else if t > zero {
362                    x = x + (dx / self.kx) * t;
363                    y = y + (dy / self.ky) * t;
364                }
365            }
366
367            let d2 = self.square_distance(point, &point!(x: x, y: y));
368
369            if d2 < min_dist {
370                min_dist = d2;
371                min_x = x;
372                min_y = y;
373                min_i = i;
374                min_t = t;
375            }
376        }
377
378        Some(PointOnLine::new(
379            point!(x: min_x, y: min_y),
380            min_i,
381            T::zero().max(T::one().min(min_t)),
382        ))
383    }
384
385    /// Returns a part of the given line between the start and the stop points
386    /// (or their closest points on the line)
387    ///
388    /// # Arguments
389    ///
390    /// * `start` - Start point
391    /// * `stop` - Stop point
392    /// * `line` - Line string
393    pub fn line_slice(
394        &self,
395        start: &Point<T>,
396        stop: &Point<T>,
397        line: &LineString<T>,
398    ) -> LineString<T> {
399        let pol1 = self.point_on_line(line, start);
400        let pol2 = self.point_on_line(line, stop);
401
402        if pol1.is_none() || pol2.is_none() {
403            return line_string![];
404        }
405        let mut pol1 = pol1.unwrap();
406        let mut pol2 = pol2.unwrap();
407
408        if pol1.index() > pol2.index()
409            || pol1.index() == pol2.index() && pol1.t() > pol2.t()
410        {
411            mem::swap(&mut pol1, &mut pol2);
412        }
413
414        let mut slice = vec![pol1.point()];
415
416        let l = pol1.index() + 1;
417        let r = pol2.index();
418
419        if line[l] != slice[0].into() && l <= r {
420            slice.push(line[l].into());
421        }
422
423        let mut i = l + 1;
424        while i <= r {
425            slice.push(line[i].into());
426            i += 1;
427        }
428
429        if line[r] != pol2.point().into() {
430            slice.push(pol2.point());
431        }
432
433        slice.into()
434    }
435
436    /// Returns a part of the given line between the start and the stop points
437    /// indicated by distance along the line
438    ///
439    /// * `start` - Start distance
440    /// * `stop` - Stop distance
441    /// * `line` - Line string
442    pub fn line_slice_along(
443        &self,
444        start: T,
445        stop: T,
446        line: &LineString<T>,
447    ) -> LineString<T> {
448        let mut sum = T::zero();
449        let mut slice = vec![];
450
451        let line_len = line.0.len();
452        if line_len == 0 {
453            return slice.into();
454        }
455
456        for i in 0..line_len - 1 {
457            let p0 = line[i].into();
458            let p1 = line[i + 1].into();
459            let d = self.distance(&p0, &p1);
460
461            sum = sum + d;
462
463            if sum > start && slice.is_empty() {
464                slice.push(interpolate(&p0, &p1, (start - (sum - d)) / d));
465            }
466
467            if sum >= stop {
468                slice.push(interpolate(&p0, &p1, (stop - (sum - d)) / d));
469                return slice.into();
470            }
471
472            if sum > start {
473                slice.push(p1);
474            }
475        }
476
477        slice.into()
478    }
479
480    /// Given a point, returns a bounding rectangle created from the given point
481    /// buffered by a given distance
482    ///
483    /// # Arguments
484    ///
485    /// * `p` - Point
486    /// * `buffer` - Buffer distance
487    pub fn buffer_point(&self, p: &Point<T>, buffer: T) -> Rect<T> {
488        let v = buffer / self.ky;
489        let h = buffer / self.kx;
490
491        Rect::new(
492            Coordinate {
493                x: p.x() - h,
494                y: p.y() - v,
495            },
496            Coordinate {
497                x: p.x() + h,
498                y: p.y() + v,
499            },
500        )
501    }
502
503    /// Given a bounding box, returns the box buffered by a given distance
504    ///
505    /// # Arguments
506    ///
507    /// * `bbox` - Bounding box
508    /// * `buffer` - Buffer distance
509    pub fn buffer_bbox(&self, bbox: &Rect<T>, buffer: T) -> Rect<T> {
510        let v = buffer / self.ky;
511        let h = buffer / self.kx;
512
513        Rect::new(
514            Coordinate {
515                x: bbox.min().x - h,
516                y: bbox.min().y - v,
517            },
518            Coordinate {
519                x: bbox.max().x + h,
520                y: bbox.max().y + v,
521            },
522        )
523    }
524
525    /// Returns true if the given point is inside in the given bounding box,
526    /// otherwise false.
527    ///
528    /// # Arguments
529    ///
530    /// * `p` - Point
531    /// * `bbox` - Bounding box
532    pub fn inside_bbox(&self, p: &Point<T>, bbox: &Rect<T>) -> bool {
533        p.y() >= bbox.min().y
534            && p.y() <= bbox.max().y
535            && long_diff(p.x(), bbox.min().x) >= T::zero()
536            && long_diff(p.x(), bbox.max().x) <= T::zero()
537    }
538}
539
540pub fn interpolate<T: Float + fmt::Debug>(
541    a: &Point<T>,
542    b: &Point<T>,
543    t: T,
544) -> Point<T> {
545    let dx = long_diff(b.x(), a.x());
546    let dy = b.y() - a.y();
547    Point::new(a.x() + dx * t, a.y() + dy * t)
548}
549
550fn calculate_multipliers<T: Float>(
551    distance_unit: DistanceUnit,
552    dkx: T,
553    dky: T,
554) -> (T, T) {
555    let re = T::from(RE).unwrap();
556    let mul = distance_unit
557        .conversion_factor_kilometers::<T>()
558        .to_radians()
559        * re;
560    let kx = mul * dkx;
561    let ky = mul * dky;
562    (kx, ky)
563}
564
565fn long_diff<T: Float>(a: T, b: T) -> T {
566    let threesixty = T::from(360).unwrap();
567    let diff = a - b;
568    diff - ((diff / threesixty).round() * threesixty)
569}
570
571fn sum_area<T: Float + fmt::Debug>(line: &[Point<T>]) -> T {
572    let line_len = line.len();
573    let mut sum = T::zero();
574    let mut k = line_len - 1;
575    for j in 0..line_len {
576        sum = sum + (line[j].x() - line[k].x()) * (line[j].y() + line[k].y());
577        k = j;
578    }
579    sum
580}