Skip to content

Commit

Permalink
feat!: arbitrary dimensional points (#23)
Browse files Browse the repository at this point in the history
* feat: arbitrary dimensions, first draft

* refactor: math macros, loosen trait

* docs: update for new point format

* feat: tested, proper 2d point deserialization
  • Loading branch information
kade-robertson authored Mar 3, 2023
1 parent c3f87e9 commit 2aee19d
Show file tree
Hide file tree
Showing 8 changed files with 485 additions and 112 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/Cargo.lock
.vscode
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,30 @@ A Rust port of the Javascript [simplify-js](https://github.com/mourner/simplify-
use simplify_polyline::*;

fn main() {
let points = &[
Point { x: 0.0, y: 0.0 }, Point { x: 1.0, y: 1.0 },
Point { x: 2.0, y: 2.0 }, Point { x: 3.0, y: 3.0 },
Point { x: 4.0, y: 4.0 }
let points = [
Point { vec: [0.0, 0.0] }, Point { vec: [1.0, 1.0] },
Point { vec: [2.0, 2.0] }, Point { vec: [3.0, 3.0] },
Point { vec: [4.0, 4.0] }
];
// alternatively, use the point! macro
let points = &[
let points = [
point!(0.0, 0.0), point!(1.0, 1.0), point!(2.0, 2.0),
point!(3.0, 3.0), point!(4.0, 4.0)
];
// alternatively, use the points! macro
let points = points![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];

// low-quality simplification (fast)
let new_points = simplify(points, 1.0, false);
let new_points = simplify(&points, 1.0, false);
// low-quality simplification (slower)
let new_points = simplify(points, 1.0, true);
let new_points = simplify(&points, 1.0, true);
}
```

## Features

- `serde`, optional, defaults to off. Allows serializing/deserializing points.
- Note, this only works for some dimensions, and some formats. Read the docs for more info.

## Performance

Expand All @@ -41,13 +42,13 @@ Measurements taken with an AMD Ryzen 7 5800x, in Pop!\_OS 22.04.
| Test Case | simplify-polyline | simplify-js |
| --------------------------------------- | ----------------: | ----------: |
| 1118 Points, Low Quality, Tolerance 1 | 16.584 μs | 52.907 μs |
| 1118 Points, High Quality, Tolerance 1 | 30.910 μs | 85.653 μs |
| 1118 Points, High Quality, Tolerance 1 | 26.989 μs | 85.653 μs |
| 1118 Points, Low Quality, Tolerance 5 | 3.987 μs | 12.840 μs |
| 1118 Points, High Quality, Tolerance 5 | 23.215 μs | 57.901 μs |
| 1118 Points, High Quality, Tolerance 5 | 19.497 μs | 57.901 μs |
| 73752 Points, Low Quality, Tolerance 1 | 82.251 μs | 273.075 μs |
| 73752 Points, High Quality, Tolerance 1 | 1974.004 μs | 5376.344 μs |
| 73752 Points, High Quality, Tolerance 1 | 1933.700 μs | 5376.344 μs |
| 73752 Points, Low Quality, Tolerance 5 | 54.150 μs | 181.554 μs |
| 73752 Points, High Quality, Tolerance 5 | 1460.742 μs | 3921.569 μs |
| 73752 Points, High Quality, Tolerance 5 | 1458.900 μs | 3921.569 μs |

## Contributing

Expand Down
24 changes: 16 additions & 8 deletions benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,65 @@ use simplify_polyline::{simplify, Point};
pub const BENCH_FIXTURE_1118: &str = include_str!("../fixtures/bench-1118.json");
pub const BENCH_FIXTURE_73752: &str = include_str!("../fixtures/bench-73752.json");

fn fixture_1118() -> Vec<Point<2, f64>> {
serde_json::from_str::<Vec<Point<2, f64>>>(BENCH_FIXTURE_1118).unwrap()
}

fn fixture_73752() -> Vec<Point<2, f64>> {
serde_json::from_str::<Vec<Point<2, f64>>>(BENCH_FIXTURE_73752).unwrap()
}

fn simplify_hq_1118_pts(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_1118).unwrap();
let points = fixture_1118();
c.bench_function("simplify_hq_1118_pts", |b| {
b.iter(|| simplify(&points, 1.0, true))
});
}

fn simplify_lq_1118_pts(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_1118).unwrap();
let points = fixture_1118();
c.bench_function("simplify_lq_1118_pts", |b| {
b.iter(|| simplify(&points, 1.0, false))
});
}

fn simplify_hq_1118_pts_tol5(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_1118).unwrap();
let points = fixture_1118();
c.bench_function("simplify_hq_1118_pts_tol5", |b| {
b.iter(|| simplify(&points, 5.0, true))
});
}

fn simplify_lq_1118_pts_tol5(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_1118).unwrap();
let points = fixture_1118();
c.bench_function("simplify_lq_1118_pts_tol5", |b| {
b.iter(|| simplify(&points, 5.0, false))
});
}

fn simplify_hq_73752_pts(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_73752).unwrap();
let points = fixture_73752();
c.bench_function("simplify_hq_73752_pts", |b| {
b.iter(|| simplify(&points, 1.0, true))
});
}

fn simplify_lq_73752_pts(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_73752).unwrap();
let points = fixture_73752();
c.bench_function("simplify_lq_73752_pts", |b| {
b.iter(|| simplify(&points, 1.0, false))
});
}

fn simplify_hq_73752_pts_tol5(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_73752).unwrap();
let points = fixture_73752();
c.bench_function("simplify_hq_73752_pts_tol5", |b| {
b.iter(|| simplify(&points, 5.0, true))
});
}

fn simplify_lq_73752_pts_tol5(c: &mut Criterion) {
let points = serde_json::from_str::<Vec<Point<f64>>>(BENCH_FIXTURE_73752).unwrap();
let points = fixture_73752();
c.bench_function("simplify_lq_73752_pts_tol5", |b| {
b.iter(|| simplify(&points, 5.0, false))
});
Expand Down
117 changes: 33 additions & 84 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,46 @@

pub use traits::ExtendedNumOps;

/// stub
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod serde;

mod point;
mod traits;

/// A two-diemensional point, where the value of each coordinate must implement the
/// [ExtendedNumOps] trait.
///
/// Example:
/// ```
/// use simplify_polyline::*;
///
/// let point = Point { x: 1.0, y: 1.0 };
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Point<T: ExtendedNumOps> {
/// The x coordinate value.
pub x: T,
/// The y coordinate value.
pub y: T,
}
pub use point::Point;

/// Creates a [Point] struct, used for input and output for [simplify]. A type should be specified
/// as the first argument that implements the [ExtendedNumOps] trait.
///
/// Example
/// ```
/// use simplify_polyline::*;
///
/// let point = point!(1.0, 1.0);
/// ```
#[macro_export]
macro_rules! point {
($x:expr,$y:expr) => {
Point { x: $x, y: $y }
};
}
fn get_sq_seg_dist<const D: usize, T: ExtendedNumOps>(
pt: &Point<D, T>,
start: &Point<D, T>,
end: &Point<D, T>,
) -> T {
let mut intersection = *start;
let difference = end - start;

/// Creates a &[[Point]] array, used for used for input and output for [simplify]. A type should
/// be specified as the first argument that implements the [ExtendedNumOps] trait. Point values
/// should be specified as length-2 tuples, where each value matches the input type, in (x, y)
/// order.
///
/// Example
/// ```
/// use simplify_polyline::*;
///
/// let points = points![(1.0, 1.0), (2.0, 2.0), (3.0, 3.0)];
/// ```
#[macro_export]
macro_rules! points {
($(($x:expr,$y:expr)),*) => {
&[$(Point { x: $x, y: $y }),*]
};
}

fn get_sq_dist<T: ExtendedNumOps>(p1: &Point<T>, p2: &Point<T>) -> T {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;

dx * dx + dy * dy
}

fn get_sq_seg_dist<T: ExtendedNumOps>(pt: &Point<T>, start: &Point<T>, end: &Point<T>) -> T {
let (mut x, mut y, mut dx, mut dy) = (start.x, start.y, end.x - start.x, end.y - start.y);

if !dx.is_zero() || !dy.is_zero() {
let t = ((pt.x - x) * dx + (pt.y - y) * dy) / (dx * dx + dy * dy);
if !difference.is_origin() {
let t = ((pt - start) * difference).value_sum() / difference.sq_dist_origin();
if t > T::one() {
x = end.x;
y = end.y;
intersection = *end;
} else if t > T::zero() {
x = x + (dx * t);
y = y + (dy * t);
intersection = intersection + (difference * t)
}
}

dx = pt.x - x;
dy = pt.y - y;

dx * dx + dy * dy
(pt - intersection).sq_dist_origin()
}

fn simplify_radial_dist<T: ExtendedNumOps>(points: &[Point<T>], tolerance: T) -> Vec<Point<T>> {
fn simplify_radial_dist<const D: usize, T: ExtendedNumOps>(
points: &[Point<D, T>],
tolerance: T,
) -> Vec<Point<D, T>> {
let mut prev_point = points[0];
let mut new_points = vec![prev_point];
let mut point = prev_point;

for pt in points.iter().skip(1) {
point = *pt;
if get_sq_dist(pt, &prev_point) > tolerance {
if pt.sq_dist(&prev_point) > tolerance {
new_points.push(*pt);
prev_point = *pt;
}
Expand All @@ -107,12 +55,12 @@ fn simplify_radial_dist<T: ExtendedNumOps>(points: &[Point<T>], tolerance: T) ->
new_points
}

fn simplify_dp_step<T: ExtendedNumOps>(
points: &[Point<T>],
fn simplify_dp_step<const D: usize, T: ExtendedNumOps>(
points: &[Point<D, T>],
first: usize,
last: usize,
tolerance: T,
simplified: &mut Vec<Point<T>>,
simplified: &mut Vec<Point<D, T>>,
) {
let mut max_sq_dist = tolerance;
let mut max_index = 0;
Expand All @@ -136,7 +84,10 @@ fn simplify_dp_step<T: ExtendedNumOps>(
}
}

fn simplify_douglas_peucker<T: ExtendedNumOps>(points: &[Point<T>], tolerance: T) -> Vec<Point<T>> {
fn simplify_douglas_peucker<const D: usize, T: ExtendedNumOps>(
points: &[Point<D, T>],
tolerance: T,
) -> Vec<Point<D, T>> {
let mut simplified = vec![points[0]];
simplify_dp_step(points, 0, points.len() - 1, tolerance, &mut simplified);
simplified.push(points[points.len() - 1]);
Expand All @@ -156,18 +107,16 @@ fn simplify_douglas_peucker<T: ExtendedNumOps>(points: &[Point<T>], tolerance: T
/// algorithm.
/// - `false`: the list of points are first filtered using a simple radial distance algorithm,
/// and then passed to the the Douglas-Peucker algorithm for final simplification.
pub fn simplify<T: ExtendedNumOps>(
points: &[Point<T>],
tolerance: f64,
pub fn simplify<const D: usize, T: ExtendedNumOps>(
points: &[Point<D, T>],
tolerance: T,
high_quality: bool,
) -> Vec<Point<T>> {
) -> Vec<Point<D, T>> {
if points.len() <= 2 {
return points.to_vec();
}

let tolerance_t = T::from_f64(tolerance).unwrap_or_else(T::one);

let tolerance_sq = tolerance_t * tolerance_t;
let tolerance_sq = tolerance * tolerance;
let intermediate = if high_quality {
points.to_vec()
} else {
Expand Down
Loading

0 comments on commit 2aee19d

Please sign in to comment.